feat: initial commit of Git Automation CLI
- Add comprehensive Git workflow automation tools - Include branch management utilities - Add commit helpers with conventional commit support - Implement GitHub integration for PR management - Add configuration management system - Include comprehensive test coverage - Add professional documentation and examples
This commit is contained in:
337
internal/github/client.go
Normal file
337
internal/github/client.go
Normal file
@@ -0,0 +1,337 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/iwasforcedtobehere/git-automation-cli/internal/config"
|
||||
)
|
||||
|
||||
// Client represents a GitHub API client
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
token string
|
||||
organization string
|
||||
repository string
|
||||
}
|
||||
|
||||
// NewClient creates a new GitHub API client
|
||||
func NewClient(ctx context.Context) (*Client, error) {
|
||||
// Get configuration
|
||||
token := config.GlobalConfig.GitHubToken
|
||||
if token == "" {
|
||||
return nil, fmt.Errorf("GitHub token not configured. Use 'gitauto config set github.token <token>' to set it")
|
||||
}
|
||||
|
||||
baseURL := config.GlobalConfig.GitHubURL
|
||||
if baseURL == "" {
|
||||
baseURL = "https://api.github.com"
|
||||
}
|
||||
|
||||
// Create HTTP client with timeout
|
||||
httpClient := &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
return &Client{
|
||||
httpClient: httpClient,
|
||||
baseURL: baseURL,
|
||||
token: token,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetRepository sets the organization and repository for the client
|
||||
func (c *Client) SetRepository(org, repo string) {
|
||||
c.organization = org
|
||||
c.repository = repo
|
||||
}
|
||||
|
||||
// makeRequest makes an HTTP request to the GitHub API
|
||||
func (c *Client) makeRequest(ctx context.Context, method, path string, body interface{}) (*http.Response, error) {
|
||||
var reqBody io.Reader
|
||||
if body != nil {
|
||||
jsonBody, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal request body: %w", err)
|
||||
}
|
||||
reqBody = bytes.NewReader(jsonBody)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("%s%s", c.baseURL, path)
|
||||
req, err := http.NewRequestWithContext(ctx, method, url, reqBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
// Set headers
|
||||
req.Header.Set("Authorization", fmt.Sprintf("token %s", c.token))
|
||||
req.Header.Set("Accept", "application/vnd.github.v3+json")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// Make the request
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to make request: %w", err)
|
||||
}
|
||||
|
||||
// Check for errors
|
||||
if resp.StatusCode >= 400 {
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("GitHub API error (%d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetUser gets the authenticated user
|
||||
func (c *Client) GetUser(ctx context.Context) (*User, error) {
|
||||
resp, err := c.makeRequest(ctx, "GET", "/user", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var user User
|
||||
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetRepository gets a repository
|
||||
func (c *Client) GetRepository(ctx context.Context, owner, repo string) (*Repository, error) {
|
||||
path := fmt.Sprintf("/repos/%s/%s", owner, repo)
|
||||
resp, err := c.makeRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var repository Repository
|
||||
if err := json.NewDecoder(resp.Body).Decode(&repository); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &repository, nil
|
||||
}
|
||||
|
||||
// CreatePullRequest creates a new pull request
|
||||
func (c *Client) CreatePullRequest(ctx context.Context, pr *PullRequestRequest) (*PullRequest, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/pulls", c.organization, c.repository)
|
||||
resp, err := c.makeRequest(ctx, "POST", path, pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var pullRequest PullRequest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&pullRequest); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &pullRequest, nil
|
||||
}
|
||||
|
||||
// GetPullRequests gets pull requests for a repository
|
||||
func (c *Client) GetPullRequests(ctx context.Context, state string) ([]PullRequest, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/pulls?state=%s", c.organization, c.repository, state)
|
||||
resp, err := c.makeRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var pullRequests []PullRequest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&pullRequests); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return pullRequests, nil
|
||||
}
|
||||
|
||||
// GetPullRequest gets a specific pull request
|
||||
func (c *Client) GetPullRequest(ctx context.Context, number int) (*PullRequest, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/pulls/%d", c.organization, c.repository, number)
|
||||
resp, err := c.makeRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var pullRequest PullRequest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&pullRequest); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &pullRequest, nil
|
||||
}
|
||||
|
||||
// UpdatePullRequest updates a pull request
|
||||
func (c *Client) UpdatePullRequest(ctx context.Context, number int, pr *PullRequestRequest) (*PullRequest, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/pulls/%d", c.organization, c.repository, number)
|
||||
resp, err := c.makeRequest(ctx, "PATCH", path, pr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var pullRequest PullRequest
|
||||
if err := json.NewDecoder(resp.Body).Decode(&pullRequest); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &pullRequest, nil
|
||||
}
|
||||
|
||||
// MergePullRequest merges a pull request
|
||||
func (c *Client) MergePullRequest(ctx context.Context, number int, mergeRequest *MergePullRequestRequest) (*MergePullRequestResponse, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/pulls/%d/merge", c.organization, c.repository, number)
|
||||
resp, err := c.makeRequest(ctx, "PUT", path, mergeRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var mergeResponse MergePullRequestResponse
|
||||
if err := json.NewDecoder(resp.Body).Decode(&mergeResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &mergeResponse, nil
|
||||
}
|
||||
|
||||
// CreateIssue creates a new issue
|
||||
func (c *Client) CreateIssue(ctx context.Context, issue *IssueRequest) (*Issue, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/issues", c.organization, c.repository)
|
||||
resp, err := c.makeRequest(ctx, "POST", path, issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var createdIssue Issue
|
||||
if err := json.NewDecoder(resp.Body).Decode(&createdIssue); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return &createdIssue, nil
|
||||
}
|
||||
|
||||
// GetIssues gets issues for a repository
|
||||
func (c *Client) GetIssues(ctx context.Context, state string) ([]Issue, error) {
|
||||
if c.organization == "" || c.repository == "" {
|
||||
return nil, fmt.Errorf("organization and repository must be set")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf("/repos/%s/%s/issues?state=%s", c.organization, c.repository, state)
|
||||
resp, err := c.makeRequest(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var issues []Issue
|
||||
if err := json.NewDecoder(resp.Body).Decode(&issues); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode response: %w", err)
|
||||
}
|
||||
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// GetRepositoryFromRemote tries to extract the repository information from the git remote
|
||||
func (c *Client) GetRepositoryFromRemote(ctx context.Context, remote string) (string, string, error) {
|
||||
// This would typically call git to get the remote URL
|
||||
// For now, we'll implement a placeholder
|
||||
// In a real implementation, this would use the git package to get the remote URL
|
||||
|
||||
// Placeholder implementation
|
||||
// In a real implementation, this would:
|
||||
// 1. Get the remote URL using git remote get-url <remote>
|
||||
// 2. Parse the URL to extract owner and repo
|
||||
// 3. Return the owner and repo
|
||||
|
||||
return "", "", fmt.Errorf("not implemented yet")
|
||||
}
|
||||
|
||||
// ValidateToken checks if the GitHub token is valid
|
||||
func (c *Client) ValidateToken(ctx context.Context) error {
|
||||
// Try to get the authenticated user
|
||||
_, err := c.GetUser(ctx)
|
||||
return err
|
||||
}
|
||||
|
||||
// IsGitHubURL checks if a URL is a GitHub URL
|
||||
func IsGitHubURL(url string) bool {
|
||||
return strings.Contains(url, "github.com")
|
||||
}
|
||||
|
||||
// ParseGitHubURL parses a GitHub URL to extract owner and repo
|
||||
func ParseGitHubURL(url string) (string, string, error) {
|
||||
// Remove protocol and .git suffix if present
|
||||
url = strings.TrimPrefix(url, "https://")
|
||||
url = strings.TrimPrefix(url, "http://")
|
||||
url = strings.TrimPrefix(url, "git@")
|
||||
url = strings.TrimSuffix(url, ".git")
|
||||
|
||||
// Split by : or /
|
||||
var parts []string
|
||||
if strings.Contains(url, ":") {
|
||||
parts = strings.Split(url, ":")
|
||||
if len(parts) != 2 {
|
||||
return "", "", fmt.Errorf("invalid GitHub URL format")
|
||||
}
|
||||
parts = strings.Split(parts[1], "/")
|
||||
} else {
|
||||
parts = strings.Split(url, "/")
|
||||
}
|
||||
|
||||
// Filter out empty strings
|
||||
var filteredParts []string
|
||||
for _, part := range parts {
|
||||
if part != "" {
|
||||
filteredParts = append(filteredParts, part)
|
||||
}
|
||||
}
|
||||
|
||||
if len(filteredParts) < 2 {
|
||||
return "", "", fmt.Errorf("invalid GitHub URL format")
|
||||
}
|
||||
|
||||
owner := filteredParts[len(filteredParts)-2]
|
||||
repo := filteredParts[len(filteredParts)-1]
|
||||
|
||||
return owner, repo, nil
|
||||
}
|
970
internal/github/client_test.go
Normal file
970
internal/github/client_test.go
Normal file
@@ -0,0 +1,970 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupTestServer(t *testing.T, handler http.HandlerFunc) (*httptest.Server, *Client) {
|
||||
t.Helper()
|
||||
|
||||
// Create a test server
|
||||
server := httptest.NewServer(handler)
|
||||
|
||||
// Create a client with the test server URL
|
||||
client := &Client{
|
||||
baseURL: server.URL,
|
||||
httpClient: server.Client(),
|
||||
token: "test-token",
|
||||
}
|
||||
|
||||
return server, client
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
// We can't easily test the config path without modifying global state,
|
||||
// so we'll just test that it doesn't fail with a valid config
|
||||
// This would require setting up the config package first
|
||||
}
|
||||
|
||||
func TestIsGitHubURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "GitHub HTTPS URL",
|
||||
url: "https://github.com/user/repo.git",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "GitHub SSH URL",
|
||||
url: "git@github.com:user/repo.git",
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "Non-GitHub HTTPS URL",
|
||||
url: "https://example.com/user/repo.git",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Non-GitHub SSH URL",
|
||||
url: "git@example.com:user/repo.git",
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
url: "not-a-url",
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := IsGitHubURL(tt.url); got != tt.want {
|
||||
t.Errorf("IsGitHubURL(%q) = %v, want %v", tt.url, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseGitHubURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
url string
|
||||
owner string
|
||||
repo string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "GitHub HTTPS URL",
|
||||
url: "https://github.com/user/repo.git",
|
||||
owner: "user",
|
||||
repo: "repo",
|
||||
},
|
||||
{
|
||||
name: "GitHub HTTPS URL without .git",
|
||||
url: "https://github.com/user/repo",
|
||||
owner: "user",
|
||||
repo: "repo",
|
||||
},
|
||||
{
|
||||
name: "GitHub SSH URL",
|
||||
url: "git@github.com:user/repo.git",
|
||||
owner: "user",
|
||||
repo: "repo",
|
||||
},
|
||||
{
|
||||
name: "Invalid URL",
|
||||
url: "not-a-url",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
owner, repo, err := ParseGitHubURL(tt.url)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ParseGitHubURL(%q) error = %v, wantErr %v", tt.url, err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr {
|
||||
if owner != tt.owner {
|
||||
t.Errorf("ParseGitHubURL(%q) owner = %v, want %v", tt.url, owner, tt.owner)
|
||||
}
|
||||
if repo != tt.repo {
|
||||
t.Errorf("ParseGitHubURL(%q) repo = %v, want %v", tt.url, repo, tt.repo)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePullRequest(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("Expected POST request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/pulls" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/pulls', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(`{
|
||||
"id": 1,
|
||||
"number": 123,
|
||||
"state": "open",
|
||||
"title": "Test PR",
|
||||
"body": "Test PR body",
|
||||
"html_url": "https://github.com/owner/repo/pull/123",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
},
|
||||
"head": {
|
||||
"label": "owner:feature",
|
||||
"ref": "feature",
|
||||
"sha": "abc123"
|
||||
},
|
||||
"base": {
|
||||
"label": "owner:main",
|
||||
"ref": "main",
|
||||
"sha": "def456"
|
||||
}
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Create a pull request
|
||||
pr := &PullRequestRequest{
|
||||
Title: "Test PR",
|
||||
Body: "Test PR body",
|
||||
Head: "feature",
|
||||
Base: "main",
|
||||
}
|
||||
|
||||
pullRequest, err := client.CreatePullRequest(context.Background(), pr)
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePullRequest failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if pullRequest.ID != 1 {
|
||||
t.Errorf("Expected ID to be 1, got %d", pullRequest.ID)
|
||||
}
|
||||
|
||||
if pullRequest.Number != 123 {
|
||||
t.Errorf("Expected Number to be 123, got %d", pullRequest.Number)
|
||||
}
|
||||
|
||||
if pullRequest.Title != "Test PR" {
|
||||
t.Errorf("Expected Title to be 'Test PR', got '%s'", pullRequest.Title)
|
||||
}
|
||||
|
||||
if pullRequest.Body != "Test PR body" {
|
||||
t.Errorf("Expected Body to be 'Test PR body', got '%s'", pullRequest.Body)
|
||||
}
|
||||
|
||||
if pullRequest.HTMLURL != "https://github.com/owner/repo/pull/123" {
|
||||
t.Errorf("Expected HTMLURL to be 'https://github.com/owner/repo/pull/123', got '%s'", pullRequest.HTMLURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPullRequests(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/pulls" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/pulls', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check request query
|
||||
if r.URL.Query().Get("state") != "open" {
|
||||
t.Errorf("Expected state query to be 'open', got '%s'", r.URL.Query().Get("state"))
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`[
|
||||
{
|
||||
"id": 1,
|
||||
"number": 123,
|
||||
"state": "open",
|
||||
"title": "Test PR 1",
|
||||
"body": "Test PR body 1",
|
||||
"html_url": "https://github.com/owner/repo/pull/123",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
},
|
||||
"head": {
|
||||
"label": "owner:feature1",
|
||||
"ref": "feature1",
|
||||
"sha": "abc123"
|
||||
},
|
||||
"base": {
|
||||
"label": "owner:main",
|
||||
"ref": "main",
|
||||
"sha": "def456"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"number": 124,
|
||||
"state": "open",
|
||||
"title": "Test PR 2",
|
||||
"body": "Test PR body 2",
|
||||
"html_url": "https://github.com/owner/repo/pull/124",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
},
|
||||
"head": {
|
||||
"label": "owner:feature2",
|
||||
"ref": "feature2",
|
||||
"sha": "ghi789"
|
||||
},
|
||||
"base": {
|
||||
"label": "owner:main",
|
||||
"ref": "main",
|
||||
"sha": "def456"
|
||||
}
|
||||
}
|
||||
]`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Get pull requests
|
||||
pullRequests, err := client.GetPullRequests(context.Background(), "open")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPullRequests failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if len(pullRequests) != 2 {
|
||||
t.Errorf("Expected 2 pull requests, got %d", len(pullRequests))
|
||||
}
|
||||
|
||||
if pullRequests[0].ID != 1 {
|
||||
t.Errorf("Expected first PR ID to be 1, got %d", pullRequests[0].ID)
|
||||
}
|
||||
|
||||
if pullRequests[0].Number != 123 {
|
||||
t.Errorf("Expected first PR Number to be 123, got %d", pullRequests[0].Number)
|
||||
}
|
||||
|
||||
if pullRequests[0].Title != "Test PR 1" {
|
||||
t.Errorf("Expected first PR Title to be 'Test PR 1', got '%s'", pullRequests[0].Title)
|
||||
}
|
||||
|
||||
if pullRequests[1].ID != 2 {
|
||||
t.Errorf("Expected second PR ID to be 2, got %d", pullRequests[1].ID)
|
||||
}
|
||||
|
||||
if pullRequests[1].Number != 124 {
|
||||
t.Errorf("Expected second PR Number to be 124, got %d", pullRequests[1].Number)
|
||||
}
|
||||
|
||||
if pullRequests[1].Title != "Test PR 2" {
|
||||
t.Errorf("Expected second PR Title to be 'Test PR 2', got '%s'", pullRequests[1].Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergePullRequest(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodPut {
|
||||
t.Errorf("Expected PUT request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/pulls/123/merge" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/pulls/123/merge', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"sha": "merged123",
|
||||
"merged": true,
|
||||
"message": "Pull Request successfully merged"
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Merge a pull request
|
||||
mergeRequest := &MergePullRequestRequest{
|
||||
MergeMethod: "merge",
|
||||
}
|
||||
|
||||
mergeResponse, err := client.MergePullRequest(context.Background(), 123, mergeRequest)
|
||||
if err != nil {
|
||||
t.Fatalf("MergePullRequest failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if mergeResponse.SHA != "merged123" {
|
||||
t.Errorf("Expected SHA to be 'merged123', got '%s'", mergeResponse.SHA)
|
||||
}
|
||||
|
||||
if !mergeResponse.Merged {
|
||||
t.Errorf("Expected Merged to be true, got false")
|
||||
}
|
||||
|
||||
if mergeResponse.Message != "Pull Request successfully merged" {
|
||||
t.Errorf("Expected Message to be 'Pull Request successfully merged', got '%s'", mergeResponse.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUser(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/user" {
|
||||
t.Errorf("Expected path to be '/user', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"login": "testuser",
|
||||
"id": 1,
|
||||
"node_id": "MDQ6VXNlcjE=",
|
||||
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/testuser",
|
||||
"html_url": "https://github.com/testuser",
|
||||
"followers_url": "https://api.github.com/users/testuser/followers",
|
||||
"following_url": "https://api.github.com/users/testuser/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/testuser/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/testuser/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/testuser/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/testuser/orgs",
|
||||
"repos_url": "https://api.github.com/users/testuser/repos",
|
||||
"events_url": "https://api.github.com/users/testuser/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/testuser/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false,
|
||||
"name": "Test User",
|
||||
"company": null,
|
||||
"blog": "",
|
||||
"location": "San Francisco",
|
||||
"email": "testuser@example.com",
|
||||
"hireable": null,
|
||||
"bio": null,
|
||||
"twitter_username": null,
|
||||
"public_repos": 2,
|
||||
"public_gists": 1,
|
||||
"followers": 1,
|
||||
"following": 0,
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z"
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Get user
|
||||
user, err := client.GetUser(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("GetUser failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if user.Login != "testuser" {
|
||||
t.Errorf("Expected Login to be 'testuser', got '%s'", user.Login)
|
||||
}
|
||||
|
||||
if user.ID != 1 {
|
||||
t.Errorf("Expected ID to be 1, got %d", user.ID)
|
||||
}
|
||||
|
||||
if user.Name != "Test User" {
|
||||
t.Errorf("Expected Name to be 'Test User', got '%s'", user.Name)
|
||||
}
|
||||
|
||||
if user.Email != "testuser@example.com" {
|
||||
t.Errorf("Expected Email to be 'testuser@example.com', got '%s'", user.Email)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepository(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"id": 1,
|
||||
"node_id": "MDEwOlJlcG9zaXRvcnkx",
|
||||
"name": "repo",
|
||||
"full_name": "owner/repo",
|
||||
"private": false,
|
||||
"owner": {
|
||||
"login": "owner",
|
||||
"id": 1,
|
||||
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
|
||||
"gravatar_id": "",
|
||||
"url": "https://api.github.com/users/owner",
|
||||
"html_url": "https://github.com/owner",
|
||||
"followers_url": "https://api.github.com/users/owner/followers",
|
||||
"following_url": "https://api.github.com/users/owner/following{/other_user}",
|
||||
"gists_url": "https://api.github.com/users/owner/gists{/gist_id}",
|
||||
"starred_url": "https://api.github.com/users/owner/starred{/owner}{/repo}",
|
||||
"subscriptions_url": "https://api.github.com/users/owner/subscriptions",
|
||||
"organizations_url": "https://api.github.com/users/owner/orgs",
|
||||
"repos_url": "https://api.github.com/users/owner/repos",
|
||||
"events_url": "https://api.github.com/users/owner/events{/privacy}",
|
||||
"received_events_url": "https://api.github.com/users/owner/received_events",
|
||||
"type": "User",
|
||||
"site_admin": false
|
||||
},
|
||||
"html_url": "https://github.com/owner/repo",
|
||||
"description": "Test repository",
|
||||
"fork": false,
|
||||
"url": "https://api.github.com/repos/owner/repo",
|
||||
"forks_url": "https://api.github.com/repos/owner/repo/forks",
|
||||
"keys_url": "https://api.github.com/repos/owner/repo/keys{/key_id}",
|
||||
"collaborators_url": "https://api.github.com/repos/owner/repo/collaborators{/collaborator}",
|
||||
"teams_url": "https://api.github.com/repos/owner/repo/teams",
|
||||
"hooks_url": "https://api.github.com/repos/owner/repo/hooks",
|
||||
"issue_events_url": "https://api.github.com/repos/owner/repo/issues/events{/number}",
|
||||
"events_url": "https://api.github.com/repos/owner/repo/events",
|
||||
"assignees_url": "https://api.github.com/repos/owner/repo/assignees{/user}",
|
||||
"branches_url": "https://api.github.com/repos/owner/repo/branches{/branch}",
|
||||
"tags_url": "https://api.github.com/repos/owner/repo/tags",
|
||||
"blobs_url": "https://api.github.com/repos/owner/repo/git/blobs{/sha}",
|
||||
"git_tags_url": "https://api.github.com/repos/owner/repo/git/tags{/sha}",
|
||||
"git_refs_url": "https://api.github.com/repos/owner/repo/git/refs{/sha}",
|
||||
"trees_url": "https://api.github.com/repos/owner/repo/git/trees{/sha}",
|
||||
"statuses_url": "https://api.github.com/repos/owner/repo/statuses/{sha}",
|
||||
"languages_url": "https://api.github.com/repos/owner/repo/languages",
|
||||
"stargazers_url": "https://api.github.com/repos/owner/repo/stargazers",
|
||||
"contributors_url": "https://api.github.com/repos/owner/repo/contributors",
|
||||
"subscribers_url": "https://api.github.com/repos/owner/repo/subscribers",
|
||||
"subscription_url": "https://api.github.com/repos/owner/repo/subscription",
|
||||
"commits_url": "https://api.github.com/repos/owner/repo/commits{/sha}",
|
||||
"git_commits_url": "https://api.github.com/repos/owner/repo/git/commits{/sha}",
|
||||
"comments_url": "https://api.github.com/repos/owner/repo/comments{/number}",
|
||||
"issue_comment_url": "https://api.github.com/repos/owner/repo/issues/comments{/number}",
|
||||
"contents_url": "https://api.github.com/repos/owner/repo/contents/{+path}",
|
||||
"compare_url": "https://api.github.com/repos/owner/repo/compare/{base}...{head}",
|
||||
"merges_url": "https://api.github.com/repos/owner/repo/merges",
|
||||
"archive_url": "https://api.github.com/repos/owner/repo/{archive_format}{/ref}",
|
||||
"downloads_url": "https://api.github.com/repos/owner/repo/downloads",
|
||||
"issues_url": "https://api.github.com/repos/owner/repo/issues{/number}",
|
||||
"pulls_url": "https://api.github.com/repos/owner/repo/pulls{/number}",
|
||||
"milestones_url": "https://api.github.com/repos/owner/repo/milestones{/number}",
|
||||
"notifications_url": "https://api.github.com/repos/owner/repo/notifications{?since,all,participating}",
|
||||
"labels_url": "https://api.github.com/repos/owner/repo/labels{/name}",
|
||||
"releases_url": "https://api.github.com/repos/owner/repo/releases{/id}",
|
||||
"deployments_url": "https://api.github.com/repos/owner/repo/deployments",
|
||||
"created_at": "2023-01-01T00:00:00Z",
|
||||
"updated_at": "2023-01-01T00:00:00Z",
|
||||
"pushed_at": "2023-01-01T00:00:00Z",
|
||||
"git_url": "git://github.com/owner/repo.git",
|
||||
"ssh_url": "git@github.com:owner/repo.git",
|
||||
"clone_url": "https://github.com/owner/repo.git",
|
||||
"svn_url": "https://github.com/owner/repo",
|
||||
"homepage": null,
|
||||
"size": 108,
|
||||
"stargazers_count": 0,
|
||||
"watchers_count": 0,
|
||||
"language": "Go",
|
||||
"has_issues": true,
|
||||
"has_projects": true,
|
||||
"has_wiki": true,
|
||||
"has_pages": false,
|
||||
"has_downloads": true,
|
||||
"archived": false,
|
||||
"disabled": false,
|
||||
"open_issues_count": 0,
|
||||
"license": null,
|
||||
"forks_count": 0,
|
||||
"open_issues": 0,
|
||||
"watchers": 0,
|
||||
"default_branch": "main"
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Get repository
|
||||
repo, err := client.GetRepository(context.Background(), "owner", "repo")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRepository failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if repo.Name != "repo" {
|
||||
t.Errorf("Expected Name to be 'repo', got '%s'", repo.Name)
|
||||
}
|
||||
|
||||
if repo.FullName != "owner/repo" {
|
||||
t.Errorf("Expected FullName to be 'owner/repo', got '%s'", repo.FullName)
|
||||
}
|
||||
|
||||
if repo.Description != "Test repository" {
|
||||
t.Errorf("Expected Description to be 'Test repository', got '%s'", repo.Description)
|
||||
}
|
||||
|
||||
if repo.Language != "Go" {
|
||||
t.Errorf("Expected Language to be 'Go', got '%s'", repo.Language)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPullRequest(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/pulls/123" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/pulls/123', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"id": 1,
|
||||
"number": 123,
|
||||
"state": "open",
|
||||
"title": "Test PR",
|
||||
"body": "Test PR body",
|
||||
"html_url": "https://github.com/owner/repo/pull/123",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
},
|
||||
"head": {
|
||||
"label": "owner:feature",
|
||||
"ref": "feature",
|
||||
"sha": "abc123"
|
||||
},
|
||||
"base": {
|
||||
"label": "owner:main",
|
||||
"ref": "main",
|
||||
"sha": "def456"
|
||||
}
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Get pull request
|
||||
pullRequest, err := client.GetPullRequest(context.Background(), 123)
|
||||
if err != nil {
|
||||
t.Fatalf("GetPullRequest failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if pullRequest.ID != 1 {
|
||||
t.Errorf("Expected ID to be 1, got %d", pullRequest.ID)
|
||||
}
|
||||
|
||||
if pullRequest.Number != 123 {
|
||||
t.Errorf("Expected Number to be 123, got %d", pullRequest.Number)
|
||||
}
|
||||
|
||||
if pullRequest.Title != "Test PR" {
|
||||
t.Errorf("Expected Title to be 'Test PR', got '%s'", pullRequest.Title)
|
||||
}
|
||||
|
||||
if pullRequest.Body != "Test PR body" {
|
||||
t.Errorf("Expected Body to be 'Test PR body', got '%s'", pullRequest.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePullRequest(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodPatch {
|
||||
t.Errorf("Expected PATCH request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/pulls/123" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/pulls/123', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"id": 1,
|
||||
"number": 123,
|
||||
"state": "open",
|
||||
"title": "Updated Test PR",
|
||||
"body": "Updated Test PR body",
|
||||
"html_url": "https://github.com/owner/repo/pull/123",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
},
|
||||
"head": {
|
||||
"label": "owner:feature",
|
||||
"ref": "feature",
|
||||
"sha": "abc123"
|
||||
},
|
||||
"base": {
|
||||
"label": "owner:main",
|
||||
"ref": "main",
|
||||
"sha": "def456"
|
||||
}
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Update pull request
|
||||
pr := &PullRequestRequest{
|
||||
Title: "Updated Test PR",
|
||||
Body: "Updated Test PR body",
|
||||
}
|
||||
|
||||
pullRequest, err := client.UpdatePullRequest(context.Background(), 123, pr)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdatePullRequest failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if pullRequest.ID != 1 {
|
||||
t.Errorf("Expected ID to be 1, got %d", pullRequest.ID)
|
||||
}
|
||||
|
||||
if pullRequest.Number != 123 {
|
||||
t.Errorf("Expected Number to be 123, got %d", pullRequest.Number)
|
||||
}
|
||||
|
||||
if pullRequest.Title != "Updated Test PR" {
|
||||
t.Errorf("Expected Title to be 'Updated Test PR', got '%s'", pullRequest.Title)
|
||||
}
|
||||
|
||||
if pullRequest.Body != "Updated Test PR body" {
|
||||
t.Errorf("Expected Body to be 'Updated Test PR body', got '%s'", pullRequest.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateIssue(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("Expected POST request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/issues" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/issues', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
w.Write([]byte(`{
|
||||
"id": 1,
|
||||
"number": 123,
|
||||
"state": "open",
|
||||
"title": "Test Issue",
|
||||
"body": "Test Issue body",
|
||||
"html_url": "https://github.com/owner/repo/issues/123",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
}
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Create issue
|
||||
issue := &IssueRequest{
|
||||
Title: "Test Issue",
|
||||
Body: "Test Issue body",
|
||||
}
|
||||
|
||||
createdIssue, err := client.CreateIssue(context.Background(), issue)
|
||||
if err != nil {
|
||||
t.Fatalf("CreateIssue failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if createdIssue.ID != 1 {
|
||||
t.Errorf("Expected ID to be 1, got %d", createdIssue.ID)
|
||||
}
|
||||
|
||||
if createdIssue.Number != 123 {
|
||||
t.Errorf("Expected Number to be 123, got %d", createdIssue.Number)
|
||||
}
|
||||
|
||||
if createdIssue.Title != "Test Issue" {
|
||||
t.Errorf("Expected Title to be 'Test Issue', got '%s'", createdIssue.Title)
|
||||
}
|
||||
|
||||
if createdIssue.Body != "Test Issue body" {
|
||||
t.Errorf("Expected Body to be 'Test Issue body', got '%s'", createdIssue.Body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetIssues(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/repos/owner/repo/issues" {
|
||||
t.Errorf("Expected path to be '/repos/owner/repo/issues', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check request query
|
||||
if r.URL.Query().Get("state") != "open" {
|
||||
t.Errorf("Expected state query to be 'open', got '%s'", r.URL.Query().Get("state"))
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`[
|
||||
{
|
||||
"id": 1,
|
||||
"number": 123,
|
||||
"state": "open",
|
||||
"title": "Test Issue 1",
|
||||
"body": "Test Issue body 1",
|
||||
"html_url": "https://github.com/owner/repo/issues/123",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"number": 124,
|
||||
"state": "open",
|
||||
"title": "Test Issue 2",
|
||||
"body": "Test Issue body 2",
|
||||
"html_url": "https://github.com/owner/repo/issues/124",
|
||||
"user": {
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
}
|
||||
}
|
||||
]`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Set repository on client
|
||||
client.SetRepository("owner", "repo")
|
||||
|
||||
// Get issues
|
||||
issues, err := client.GetIssues(context.Background(), "open")
|
||||
if err != nil {
|
||||
t.Fatalf("GetIssues failed: %v", err)
|
||||
}
|
||||
|
||||
// Check response
|
||||
if len(issues) != 2 {
|
||||
t.Errorf("Expected 2 issues, got %d", len(issues))
|
||||
}
|
||||
|
||||
if issues[0].ID != 1 {
|
||||
t.Errorf("Expected first issue ID to be 1, got %d", issues[0].ID)
|
||||
}
|
||||
|
||||
if issues[0].Number != 123 {
|
||||
t.Errorf("Expected first issue Number to be 123, got %d", issues[0].Number)
|
||||
}
|
||||
|
||||
if issues[0].Title != "Test Issue 1" {
|
||||
t.Errorf("Expected first issue Title to be 'Test Issue 1', got '%s'", issues[0].Title)
|
||||
}
|
||||
|
||||
if issues[1].ID != 2 {
|
||||
t.Errorf("Expected second issue ID to be 2, got %d", issues[1].ID)
|
||||
}
|
||||
|
||||
if issues[1].Number != 124 {
|
||||
t.Errorf("Expected second issue Number to be 124, got %d", issues[1].Number)
|
||||
}
|
||||
|
||||
if issues[1].Title != "Test Issue 2" {
|
||||
t.Errorf("Expected second issue Title to be 'Test Issue 2', got '%s'", issues[1].Title)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateToken(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// Check request method
|
||||
if r.Method != http.MethodGet {
|
||||
t.Errorf("Expected GET request, got %s", r.Method)
|
||||
}
|
||||
|
||||
// Check request path
|
||||
if r.URL.Path != "/user" {
|
||||
t.Errorf("Expected path to be '/user', got '%s'", r.URL.Path)
|
||||
}
|
||||
|
||||
// Check authorization header
|
||||
if r.Header.Get("Authorization") != "token test-token" {
|
||||
t.Errorf("Expected Authorization header to be 'token test-token', got '%s'", r.Header.Get("Authorization"))
|
||||
}
|
||||
|
||||
// Write response
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{
|
||||
"login": "testuser",
|
||||
"id": 1
|
||||
}`))
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Validate token
|
||||
err := client.ValidateToken(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateToken failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRepositoryFromRemote(t *testing.T) {
|
||||
// Create a test server
|
||||
server, client := setupTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
// This function doesn't make HTTP requests, so we don't need to check anything
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
// Get repository from remote
|
||||
owner, repo, err := client.GetRepositoryFromRemote(context.Background(), "origin")
|
||||
if err == nil {
|
||||
t.Error("Expected GetRepositoryFromRemote to return an error, got nil")
|
||||
}
|
||||
|
||||
if owner != "" {
|
||||
t.Errorf("Expected owner to be empty, got '%s'", owner)
|
||||
}
|
||||
|
||||
if repo != "" {
|
||||
t.Errorf("Expected repo to be empty, got '%s'", repo)
|
||||
}
|
||||
}
|
305
internal/github/models.go
Normal file
305
internal/github/models.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package github
|
||||
|
||||
import "time"
|
||||
|
||||
// User represents a GitHub user
|
||||
type User struct {
|
||||
Login string `json:"login"`
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
GravatarID string `json:"gravatar_id"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
FollowersURL string `json:"followers_url"`
|
||||
FollowingURL string `json:"following_url"`
|
||||
GistsURL string `json:"gists_url"`
|
||||
StarredURL string `json:"starred_url"`
|
||||
SubscriptionsURL string `json:"subscriptions_url"`
|
||||
OrganizationsURL string `json:"organizations_url"`
|
||||
ReposURL string `json:"repos_url"`
|
||||
EventsURL string `json:"events_url"`
|
||||
ReceivedEventsURL string `json:"received_events_url"`
|
||||
Type string `json:"type"`
|
||||
SiteAdmin bool `json:"site_admin"`
|
||||
Name string `json:"name"`
|
||||
Company string `json:"company"`
|
||||
Blog string `json:"blog"`
|
||||
Location string `json:"location"`
|
||||
Email string `json:"email"`
|
||||
Hireable bool `json:"hireable"`
|
||||
Bio string `json:"bio"`
|
||||
TwitterUsername string `json:"twitter_username"`
|
||||
PublicRepos int `json:"public_repos"`
|
||||
PublicGists int `json:"public_gists"`
|
||||
Followers int `json:"followers"`
|
||||
Following int `json:"following"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// Repository represents a GitHub repository
|
||||
type Repository struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Private bool `json:"private"`
|
||||
Owner User `json:"owner"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Description string `json:"description"`
|
||||
Fork bool `json:"fork"`
|
||||
URL string `json:"url"`
|
||||
ForksURL string `json:"forks_url"`
|
||||
KeysURL string `json:"keys_url"`
|
||||
CollaboratorsURL string `json:"collaborators_url"`
|
||||
TeamsURL string `json:"teams_url"`
|
||||
HooksURL string `json:"hooks_url"`
|
||||
IssueEventsURL string `json:"issue_events_url"`
|
||||
EventsURL string `json:"events_url"`
|
||||
AssigneesURL string `json:"assignees_url"`
|
||||
BranchesURL string `json:"branches_url"`
|
||||
TagsURL string `json:"tags_url"`
|
||||
BlobsURL string `json:"blobs_url"`
|
||||
GitTagsURL string `json:"git_tags_url"`
|
||||
GitRefsURL string `json:"git_refs_url"`
|
||||
TreesURL string `json:"trees_url"`
|
||||
StatusesURL string `json:"statuses_url"`
|
||||
LanguagesURL string `json:"languages_url"`
|
||||
StargazersURL string `json:"stargazers_url"`
|
||||
ContributorsURL string `json:"contributors_url"`
|
||||
SubscribersURL string `json:"subscribers_url"`
|
||||
SubscriptionURL string `json:"subscription_url"`
|
||||
CommitsURL string `json:"commits_url"`
|
||||
GitCommitsURL string `json:"git_commits_url"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
IssueCommentURL string `json:"issue_comment_url"`
|
||||
ContentsURL string `json:"contents_url"`
|
||||
CompareURL string `json:"compare_url"`
|
||||
MergesURL string `json:"merges_url"`
|
||||
ArchiveURL string `json:"archive_url"`
|
||||
DownloadsURL string `json:"downloads_url"`
|
||||
IssuesURL string `json:"issues_url"`
|
||||
PullsURL string `json:"pulls_url"`
|
||||
MilestonesURL string `json:"milestones_url"`
|
||||
NotificationsURL string `json:"notifications_url"`
|
||||
LabelsURL string `json:"labels_url"`
|
||||
ReleasesURL string `json:"releases_url"`
|
||||
DeploymentsURL string `json:"deployments_url"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
PushedAt time.Time `json:"pushed_at"`
|
||||
GitURL string `json:"git_url"`
|
||||
SSHURL string `json:"ssh_url"`
|
||||
CloneURL string `json:"clone_url"`
|
||||
SVNURL string `json:"svn_url"`
|
||||
Homepage string `json:"homepage"`
|
||||
Size int `json:"size"`
|
||||
StargazersCount int `json:"stargazers_count"`
|
||||
WatchersCount int `json:"watchers_count"`
|
||||
Language string `json:"language"`
|
||||
HasIssues bool `json:"has_issues"`
|
||||
HasProjects bool `json:"has_projects"`
|
||||
HasWiki bool `json:"has_wiki"`
|
||||
HasPages bool `json:"has_pages"`
|
||||
HasDownloads bool `json:"has_downloads"`
|
||||
Archived bool `json:"archived"`
|
||||
Disabled bool `json:"disabled"`
|
||||
OpenIssuesCount int `json:"open_issues_count"`
|
||||
License struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
SPDXID string `json:"spdx_id"`
|
||||
URL string `json:"url"`
|
||||
NodeID string `json:"node_id"`
|
||||
} `json:"license"`
|
||||
ForksCount int `json:"forks_count"`
|
||||
OpenIssues int `json:"open_issues"`
|
||||
Watchers int `json:"watchers"`
|
||||
DefaultBranch string `json:"default_branch"`
|
||||
Permissions struct {
|
||||
Admin bool `json:"admin"`
|
||||
Push bool `json:"push"`
|
||||
Pull bool `json:"pull"`
|
||||
} `json:"permissions"`
|
||||
AllowRebaseMerge bool `json:"allow_rebase_merge"`
|
||||
TempCloneToken string `json:"temp_clone_token"`
|
||||
AllowSquashMerge bool `json:"allow_squash_merge"`
|
||||
AllowMergeCommit bool `json:"allow_merge_commit"`
|
||||
Deleted bool `json:"deleted"`
|
||||
AutoInit bool `json:"auto_init"`
|
||||
IsTemplate bool `json:"is_template"`
|
||||
}
|
||||
|
||||
// PullRequest represents a GitHub pull request
|
||||
type PullRequest struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Number int `json:"number"`
|
||||
State string `json:"state"`
|
||||
Locked bool `json:"locked"`
|
||||
Title string `json:"title"`
|
||||
User User `json:"user"`
|
||||
Body string `json:"body"`
|
||||
Labels []Label `json:"labels"`
|
||||
Milestone *Milestone `json:"milestone"`
|
||||
ActiveLockReason string `json:"active_lock_reason"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ClosedAt *time.Time `json:"closed_at"`
|
||||
MergedAt *time.Time `json:"merged_at"`
|
||||
MergeCommitSha string `json:"merge_commit_sha"`
|
||||
Assignee *User `json:"assignee"`
|
||||
Assignees []User `json:"assignees"`
|
||||
RequestedReviewers []User `json:"requested_reviewers"`
|
||||
RequestedTeams []Team `json:"requested_teams"`
|
||||
Head PRBranchRef `json:"head"`
|
||||
Base PRBranchRef `json:"base"`
|
||||
AuthorAssociation string `json:"author_association"`
|
||||
Draft bool `json:"draft"`
|
||||
CommitsURL string `json:"commits_url"`
|
||||
ReviewCommentsURL string `json:"review_comments_url"`
|
||||
IssueCommentURL string `json:"issue_comment_url"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
StatusesURL string `json:"statuses_url"`
|
||||
Merged bool `json:"merged"`
|
||||
Mergeable bool `json:"mergeable"`
|
||||
Rebaseable bool `json:"rebaseable"`
|
||||
MergeableState string `json:"mergeable_state"`
|
||||
MergedBy *User `json:"merged_by"`
|
||||
Comments int `json:"comments"`
|
||||
ReviewComments int `json:"review_comments"`
|
||||
MaintainerCanModify bool `json:"maintainer_can_modify"`
|
||||
Commits int `json:"commits"`
|
||||
Additions int `json:"additions"`
|
||||
Deletions int `json:"deletions"`
|
||||
ChangedFiles int `json:"changed_files"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
IssueURL string `json:"issue_url"`
|
||||
}
|
||||
|
||||
// PRBranchRef represents a head or base reference in a pull request
|
||||
type PRBranchRef struct {
|
||||
Label string `json:"label"`
|
||||
Ref string `json:"ref"`
|
||||
SHA string `json:"sha"`
|
||||
User User `json:"user"`
|
||||
Repo *Repository `json:"repo"`
|
||||
}
|
||||
|
||||
// PullRequestRequest represents a request to create or update a pull request
|
||||
type PullRequestRequest struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Head string `json:"head"`
|
||||
Base string `json:"base"`
|
||||
Draft bool `json:"draft,omitempty"`
|
||||
}
|
||||
|
||||
// MergePullRequestRequest represents a request to merge a pull request
|
||||
type MergePullRequestRequest struct {
|
||||
CommitMessage string `json:"commit_message,omitempty"`
|
||||
SHA string `json:"sha,omitempty"`
|
||||
MergeMethod string `json:"merge_method,omitempty"`
|
||||
}
|
||||
|
||||
// MergePullRequestResponse represents the response from merging a pull request
|
||||
type MergePullRequestResponse struct {
|
||||
SHA string `json:"sha"`
|
||||
Merged bool `json:"merged"`
|
||||
Message string `json:"message"`
|
||||
Author *User `json:"author,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
}
|
||||
|
||||
// Issue represents a GitHub issue
|
||||
type Issue struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
URL string `json:"url"`
|
||||
RepositoryURL string `json:"repository_url"`
|
||||
LabelsURL string `json:"labels_url"`
|
||||
CommentsURL string `json:"comments_url"`
|
||||
EventsURL string `json:"events_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Number int `json:"number"`
|
||||
State string `json:"state"`
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
User User `json:"user"`
|
||||
Labels []Label `json:"labels"`
|
||||
Assignee *User `json:"assignee"`
|
||||
Assignees []User `json:"assignees"`
|
||||
Milestone *Milestone `json:"milestone"`
|
||||
Locked bool `json:"locked"`
|
||||
ActiveLockReason string `json:"active_lock_reason"`
|
||||
Comments int `json:"comments"`
|
||||
PullRequest *struct {
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
DiffURL string `json:"diff_url"`
|
||||
PatchURL string `json:"patch_url"`
|
||||
} `json:"pull_request"`
|
||||
ClosedAt *time.Time `json:"closed_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ClosedBy *User `json:"closed_by"`
|
||||
AuthorAssociation string `json:"author_association"`
|
||||
}
|
||||
|
||||
// IssueRequest represents a request to create an issue
|
||||
type IssueRequest struct {
|
||||
Title string `json:"title"`
|
||||
Body string `json:"body"`
|
||||
Assignees []string `json:"assignees,omitempty"`
|
||||
Labels []string `json:"labels,omitempty"`
|
||||
Milestone int `json:"milestone,omitempty"`
|
||||
}
|
||||
|
||||
// Label represents a GitHub label
|
||||
type Label struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
Default bool `json:"default"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Milestone represents a GitHub milestone
|
||||
type Milestone struct {
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
LabelsURL string `json:"labels_url"`
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Number int `json:"number"`
|
||||
State string `json:"state"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Creator User `json:"creator"`
|
||||
OpenIssues int `json:"open_issues"`
|
||||
ClosedIssues int `json:"closed_issues"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ClosedAt *time.Time `json:"closed_at"`
|
||||
DueOn *time.Time `json:"due_on"`
|
||||
}
|
||||
|
||||
// Team represents a GitHub team
|
||||
type Team struct {
|
||||
ID int64 `json:"id"`
|
||||
NodeID string `json:"node_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Description string `json:"description"`
|
||||
Privacy string `json:"privacy"`
|
||||
URL string `json:"url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
MembersURL string `json:"members_url"`
|
||||
RepositoriesURL string `json:"repositories_url"`
|
||||
Permission string `json:"permission"`
|
||||
Parent *Team `json:"parent"`
|
||||
}
|
Reference in New Issue
Block a user