11
This commit is contained in:
@@ -0,0 +1,495 @@
|
||||
package gitea
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const pageSize = 50
|
||||
|
||||
// Client calls the Gitea REST API with a fixed host and token.
|
||||
type Client struct {
|
||||
Host string
|
||||
Token string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// New returns a Client with a 30-second timeout.
|
||||
func New(host, token string) *Client {
|
||||
return &Client{
|
||||
Host: normalizeHost(host),
|
||||
Token: token,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeHost ensures the host is a scheme-qualified base URL without a trailing slash.
|
||||
func normalizeHost(host string) string {
|
||||
host = strings.TrimRight(host, "/")
|
||||
if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") {
|
||||
host = "https://" + host
|
||||
}
|
||||
return host
|
||||
}
|
||||
|
||||
// APIError is returned for any non-2xx response so callers can inspect the
|
||||
// HTTP status (e.g. 401 = token invalid, 404 = not found).
|
||||
type APIError struct {
|
||||
Status int
|
||||
Method string
|
||||
URL string
|
||||
Body string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("gitea API %s %s: status %d: %s", e.Method, e.URL, e.Status, e.Body)
|
||||
}
|
||||
|
||||
// User is the minimal Gitea user shape used by this client.
|
||||
type User struct {
|
||||
Login string `json:"login"`
|
||||
ID int `json:"id"`
|
||||
}
|
||||
|
||||
// Issue mirrors the Gitea issue API response fields used by this application.
|
||||
type Issue struct {
|
||||
ID int `json:"id"`
|
||||
Number int `json:"number"`
|
||||
Title string `json:"title"`
|
||||
State string `json:"state"`
|
||||
Comments int `json:"comments"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
Body string `json:"body"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
User *struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"user"`
|
||||
Assignees []struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"assignees"`
|
||||
Labels []struct {
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
} `json:"labels"`
|
||||
PullRequest *struct{} `json:"pull_request"` // non-nil when issue is a PR
|
||||
}
|
||||
|
||||
// Comment mirrors the Gitea issue comment API response.
|
||||
type Comment struct {
|
||||
ID int `json:"id"`
|
||||
Body string `json:"body"`
|
||||
IssueURL string `json:"issue_url"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
// PullRequest mirrors the Gitea pull request API response fields used here.
|
||||
type PullRequest struct {
|
||||
Number int `json:"number"`
|
||||
Merged bool `json:"merged"`
|
||||
State string `json:"state"`
|
||||
MergedAt string `json:"merged_at"`
|
||||
HtmlURL string `json:"html_url"`
|
||||
Body string `json:"body"`
|
||||
Base struct {
|
||||
Ref string `json:"ref"`
|
||||
} `json:"base"`
|
||||
}
|
||||
|
||||
// PullRequestFile mirrors one file entry from the PR files endpoint.
|
||||
type PullRequestFile struct {
|
||||
Filename string `json:"filename"`
|
||||
Status string `json:"status"`
|
||||
Additions int `json:"additions"`
|
||||
Deletions int `json:"deletions"`
|
||||
}
|
||||
|
||||
// PullRequestCommit mirrors one commit entry from the PR commits endpoint.
|
||||
type PullRequestCommit struct {
|
||||
SHA string `json:"sha"`
|
||||
Commit struct {
|
||||
Message string `json:"message"`
|
||||
Author struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"author"`
|
||||
} `json:"commit"`
|
||||
}
|
||||
|
||||
// base returns the API base URL.
|
||||
func (c *Client) base() string {
|
||||
return c.Host + "/api/v1"
|
||||
}
|
||||
|
||||
// do executes an HTTP request, attaches auth headers, and returns the response.
|
||||
// The caller is responsible for closing resp.Body.
|
||||
func (c *Client) do(ctx context.Context, method, rawURL string, body io.Reader) (*http.Response, error) {
|
||||
req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("build request %s %s: %w", method, rawURL, err)
|
||||
}
|
||||
req.Header.Set("Authorization", "token "+c.Token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
if body != nil {
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("http %s %s: %w", method, rawURL, err)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// checkStatus reads the response body and returns an *APIError for non-2xx.
|
||||
// It always closes resp.Body.
|
||||
|
||||
// decode reads JSON from resp.Body into dst, closes the body, and handles
|
||||
// non-2xx as an *APIError. On success resp.Body is consumed and closed.
|
||||
func decode(resp *http.Response, dst any) error {
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{
|
||||
Status: resp.StatusCode,
|
||||
Method: resp.Request.Method,
|
||||
URL: resp.Request.URL.String(),
|
||||
Body: string(b),
|
||||
}
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(dst); err != nil {
|
||||
return fmt.Errorf("decode response from %s: %w", resp.Request.URL, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// paginated fetches pages until a page shorter than pageSize is returned,
|
||||
// calling collect with each batch.
|
||||
func (c *Client) paginated(ctx context.Context, base *url.URL, collect func([]json.RawMessage) error) error {
|
||||
for page := 1; ; page++ {
|
||||
q := base.Query()
|
||||
q.Set("limit", strconv.Itoa(pageSize))
|
||||
q.Set("page", strconv.Itoa(page))
|
||||
base.RawQuery = q.Encode()
|
||||
|
||||
resp, err := c.do(ctx, http.MethodGet, base.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var batch []json.RawMessage
|
||||
if err := decode(resp, &batch); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(batch) == 0 {
|
||||
break
|
||||
}
|
||||
if err := collect(batch); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(batch) < pageSize {
|
||||
break
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCurrentUser fetches the authenticated user's profile.
|
||||
func (c *Client) GetCurrentUser(ctx context.Context) (*User, error) {
|
||||
resp, err := c.do(ctx, http.MethodGet, c.base()+"/user", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var u User
|
||||
if err := decode(resp, &u); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &u, nil
|
||||
}
|
||||
|
||||
// ListIssuesByFilter returns all issues in owner/repo matching the given filter
|
||||
// query parameter (e.g. "assigned_by" or "created_by") and its value.
|
||||
func (c *Client) ListIssuesByFilter(ctx context.Context, owner, repo, filterKey, filterVal string) ([]Issue, error) {
|
||||
u, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/issues", c.base(), owner, repo))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse url: %w", err)
|
||||
}
|
||||
q := u.Query()
|
||||
q.Set("type", "issues")
|
||||
q.Set("state", "all")
|
||||
q.Set(filterKey, filterVal)
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
var out []Issue
|
||||
err = c.paginated(ctx, u, func(batch []json.RawMessage) error {
|
||||
for _, raw := range batch {
|
||||
var iss Issue
|
||||
if err := json.Unmarshal(raw, &iss); err != nil {
|
||||
return fmt.Errorf("unmarshal issue: %w", err)
|
||||
}
|
||||
out = append(out, iss)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// ListAllRepoComments returns every comment across all issues in owner/repo.
|
||||
func (c *Client) ListAllRepoComments(ctx context.Context, owner, repo string) ([]Comment, error) {
|
||||
u, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/issues/comments", c.base(), owner, repo))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse url: %w", err)
|
||||
}
|
||||
var out []Comment
|
||||
err = c.paginated(ctx, u, func(batch []json.RawMessage) error {
|
||||
for _, raw := range batch {
|
||||
var cm Comment
|
||||
if err := json.Unmarshal(raw, &cm); err != nil {
|
||||
return fmt.Errorf("unmarshal comment: %w", err)
|
||||
}
|
||||
out = append(out, cm)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// GetIssue returns a single issue by number. Returns (nil, nil) on 404.
|
||||
func (c *Client) GetIssue(ctx context.Context, owner, repo string, number int) (*Issue, error) {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d", c.base(), owner, repo, number)
|
||||
resp, err := c.do(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
resp.Body.Close()
|
||||
return nil, nil
|
||||
}
|
||||
var iss Issue
|
||||
if err := decode(resp, &iss); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &iss, nil
|
||||
}
|
||||
|
||||
// GetPullRequest returns a pull request by number.
|
||||
func (c *Client) GetPullRequest(ctx context.Context, owner, repo string, number int) (*PullRequest, error) {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/pulls/%d", c.base(), owner, repo, number)
|
||||
resp, err := c.do(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pr PullRequest
|
||||
if err := decode(resp, &pr); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &pr, nil
|
||||
}
|
||||
|
||||
// ListIssueComments returns all comments on a specific issue.
|
||||
func (c *Client) ListIssueComments(ctx context.Context, owner, repo string, number int) ([]Comment, error) {
|
||||
u, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/issues/%d/comments", c.base(), owner, repo, number))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse url: %w", err)
|
||||
}
|
||||
var out []Comment
|
||||
err = c.paginated(ctx, u, func(batch []json.RawMessage) error {
|
||||
for _, raw := range batch {
|
||||
var cm Comment
|
||||
if err := json.Unmarshal(raw, &cm); err != nil {
|
||||
return fmt.Errorf("unmarshal comment: %w", err)
|
||||
}
|
||||
out = append(out, cm)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// PostIssueComment posts a new comment on the given issue and returns the created comment.
|
||||
func (c *Client) PostIssueComment(ctx context.Context, owner, repo string, number int, body string) (*Comment, error) {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/comments", c.base(), owner, repo, number)
|
||||
payload, err := json.Marshal(map[string]string{"body": body})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal comment body: %w", err)
|
||||
}
|
||||
resp, err := c.do(ctx, http.MethodPost, rawURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cm Comment
|
||||
if err := decode(resp, &cm); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cm, nil
|
||||
}
|
||||
|
||||
// GetDependencies returns the issues that the given issue depends on,
|
||||
// preserving the order returned by the API (first element is the front-loaded prerequisite).
|
||||
func (c *Client) GetDependencies(ctx context.Context, owner, repo string, number int) ([]Issue, error) {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/dependencies", c.base(), owner, repo, number)
|
||||
resp, err := c.do(ctx, http.MethodGet, rawURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var issues []Issue
|
||||
if err := decode(resp, &issues); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return issues, nil
|
||||
}
|
||||
|
||||
// CloseIssue sets the issue state to closed via PATCH.
|
||||
func (c *Client) CloseIssue(ctx context.Context, owner, repo string, number int) error {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d", c.base(), owner, repo, number)
|
||||
payload, err := json.Marshal(map[string]string{"state": "closed"})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal close body: %w", err)
|
||||
}
|
||||
resp, err := c.do(ctx, http.MethodPatch, rawURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{Status: resp.StatusCode, Method: http.MethodPatch, URL: rawURL, Body: string(b)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteIssue hard-deletes the issue from Gitea.
|
||||
func (c *Client) DeleteIssue(ctx context.Context, owner, repo string, number int) error {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d", c.base(), owner, repo, number)
|
||||
resp, err := c.do(ctx, http.MethodDelete, rawURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{Status: resp.StatusCode, Method: http.MethodDelete, URL: rawURL, Body: string(b)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ClosePullRequest closes a pull request by patching its state to "closed".
|
||||
// Gitea treats PRs as issues for state changes: PATCH /repos/{owner}/{repo}/issues/{prNum}
|
||||
func (c *Client) ClosePullRequest(ctx context.Context, owner, repo string, prNumber int) error {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d", c.base(), owner, repo, prNumber)
|
||||
payload, _ := json.Marshal(map[string]string{"state": "closed"})
|
||||
resp, err := c.do(ctx, http.MethodPatch, rawURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{Status: resp.StatusCode, Method: http.MethodPatch, URL: rawURL, Body: string(b)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteBranch deletes a remote branch.
|
||||
// DELETE /repos/{owner}/{repo}/branches/{branch}
|
||||
func (c *Client) DeleteBranch(ctx context.Context, owner, repo, branch string) error {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/branches/%s", c.base(), owner, repo, url.PathEscape(branch))
|
||||
resp, err := c.do(ctx, http.MethodDelete, rawURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{Status: resp.StatusCode, Method: http.MethodDelete, URL: rawURL, Body: string(b)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddDependency makes issue number depend on dependsOn.
|
||||
// POST /api/v1/repos/{owner}/{repo}/issues/{number}/dependencies body: {"index":dependsOn}
|
||||
func (c *Client) AddDependency(ctx context.Context, owner, repo string, number, dependsOn int) error {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/dependencies", c.base(), owner, repo, number)
|
||||
payload, err := json.Marshal(map[string]int{"index": dependsOn})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal add dependency: %w", err)
|
||||
}
|
||||
resp, err := c.do(ctx, http.MethodPost, rawURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{Status: resp.StatusCode, Method: http.MethodPost, URL: rawURL, Body: string(b)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveDependency removes the dependency of issue number on dependsOn.
|
||||
// DELETE /api/v1/repos/{owner}/{repo}/issues/{number}/dependencies body: {"index":dependsOn}
|
||||
func (c *Client) RemoveDependency(ctx context.Context, owner, repo string, number, dependsOn int) error {
|
||||
rawURL := fmt.Sprintf("%s/repos/%s/%s/issues/%d/dependencies", c.base(), owner, repo, number)
|
||||
payload, err := json.Marshal(map[string]int{"index": dependsOn})
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal remove dependency: %w", err)
|
||||
}
|
||||
resp, err := c.do(ctx, http.MethodDelete, rawURL, bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
return &APIError{Status: resp.StatusCode, Method: http.MethodDelete, URL: rawURL, Body: string(b)}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListPullRequestFiles returns all files changed in a pull request.
|
||||
func (c *Client) ListPullRequestFiles(ctx context.Context, owner, repo string, number int) ([]PullRequestFile, error) {
|
||||
u, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/pulls/%d/files", c.base(), owner, repo, number))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse url: %w", err)
|
||||
}
|
||||
var out []PullRequestFile
|
||||
err = c.paginated(ctx, u, func(batch []json.RawMessage) error {
|
||||
for _, raw := range batch {
|
||||
var f PullRequestFile
|
||||
if err := json.Unmarshal(raw, &f); err != nil {
|
||||
return fmt.Errorf("unmarshal pr file: %w", err)
|
||||
}
|
||||
out = append(out, f)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
|
||||
// ListPullRequestCommits returns all commits of a pull request, following pagination.
|
||||
func (c *Client) ListPullRequestCommits(ctx context.Context, owner, repo string, number int) ([]PullRequestCommit, error) {
|
||||
u, err := url.Parse(fmt.Sprintf("%s/repos/%s/%s/pulls/%d/commits", c.base(), owner, repo, number))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse url: %w", err)
|
||||
}
|
||||
var out []PullRequestCommit
|
||||
err = c.paginated(ctx, u, func(batch []json.RawMessage) error {
|
||||
for _, raw := range batch {
|
||||
var commit PullRequestCommit
|
||||
if err := json.Unmarshal(raw, &commit); err != nil {
|
||||
return fmt.Errorf("unmarshal pr commit: %w", err)
|
||||
}
|
||||
out = append(out, commit)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return out, err
|
||||
}
|
||||
@@ -0,0 +1,707 @@
|
||||
package gitea_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"superwork-tui/internal/gitea"
|
||||
)
|
||||
|
||||
// newServer starts an httptest server using the given handler and returns a
|
||||
// *gitea.Client pointed at it with a fixed token.
|
||||
func newServer(t *testing.T, h http.Handler) (*httptest.Server, *gitea.Client) {
|
||||
t.Helper()
|
||||
srv := httptest.NewServer(h)
|
||||
t.Cleanup(srv.Close)
|
||||
return srv, gitea.New(srv.URL, "test-token")
|
||||
}
|
||||
|
||||
// mustJSON marshals v or fatals the test.
|
||||
func mustJSON(t *testing.T, v any) []byte {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Fatalf("mustJSON: %v", err)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// --- Auth header -----------------------------------------------------------
|
||||
|
||||
func TestAuthHeader(t *testing.T) {
|
||||
var gotAuth string
|
||||
srv, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotAuth = r.Header.Get("Authorization")
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"login":"alice"}`)
|
||||
}))
|
||||
_ = srv
|
||||
|
||||
u, err := c.GetCurrentUser(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if u.Login != "alice" {
|
||||
t.Errorf("login = %q, want alice", u.Login)
|
||||
}
|
||||
if gotAuth != "token test-token" {
|
||||
t.Errorf("Authorization = %q, want %q", gotAuth, "token test-token")
|
||||
}
|
||||
}
|
||||
|
||||
// --- 401 → APIError --------------------------------------------------------
|
||||
|
||||
func TestGetCurrentUser_401(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
}))
|
||||
|
||||
_, err := c.GetCurrentUser(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err is %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != 401 {
|
||||
t.Errorf("Status = %d, want 401", apiErr.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetIssue 404 → nil ----------------------------------------------------
|
||||
|
||||
func TestGetIssue_404_ReturnsNil(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
|
||||
issue, err := c.GetIssue(context.Background(), "owner", "repo", 99)
|
||||
if err != nil {
|
||||
t.Fatalf("expected nil error on 404, got: %v", err)
|
||||
}
|
||||
if issue != nil {
|
||||
t.Errorf("expected nil issue on 404, got %+v", issue)
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetIssue 200 ----------------------------------------------------------
|
||||
|
||||
func TestGetIssue_200(t *testing.T) {
|
||||
payload := gitea.Issue{
|
||||
Number: 7,
|
||||
Title: "hello",
|
||||
State: "open",
|
||||
Body: "body text",
|
||||
HtmlURL: "https://gitea.example.com/owner/repo/issues/7",
|
||||
}
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(payload)
|
||||
}))
|
||||
|
||||
issue, err := c.GetIssue(context.Background(), "owner", "repo", 7)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if issue == nil {
|
||||
t.Fatal("expected non-nil issue")
|
||||
}
|
||||
if issue.Number != 7 || issue.Title != "hello" {
|
||||
t.Errorf("issue = %+v", issue)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Pagination: ListAllRepoComments crosses 50-item boundary --------------
|
||||
|
||||
func TestListAllRepoComments_Pagination(t *testing.T) {
|
||||
// Page 1: 50 comments, page 2: 3 comments → total 53.
|
||||
page1 := make([]gitea.Comment, 50)
|
||||
for i := range page1 {
|
||||
page1[i] = gitea.Comment{ID: i + 1, Body: fmt.Sprintf("c%d", i+1)}
|
||||
}
|
||||
page2 := []gitea.Comment{
|
||||
{ID: 51, Body: "c51"},
|
||||
{ID: 52, Body: "c52"},
|
||||
{ID: 53, Body: "c53"},
|
||||
}
|
||||
|
||||
var pagesSeen []string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
p := r.URL.Query().Get("page")
|
||||
pagesSeen = append(pagesSeen, p)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if p == "1" || p == "" {
|
||||
json.NewEncoder(w).Encode(page1)
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(page2)
|
||||
}
|
||||
}))
|
||||
|
||||
comments, err := c.ListAllRepoComments(context.Background(), "owner", "repo")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(comments) != 53 {
|
||||
t.Errorf("len(comments) = %d, want 53", len(comments))
|
||||
}
|
||||
// Must have fetched exactly 2 pages.
|
||||
if len(pagesSeen) != 2 {
|
||||
t.Errorf("pages fetched = %v, want 2 pages", pagesSeen)
|
||||
}
|
||||
if comments[52].ID != 53 {
|
||||
t.Errorf("last comment ID = %d, want 53", comments[52].ID)
|
||||
}
|
||||
}
|
||||
|
||||
// --- ListIssuesByFilter uses correct query params --------------------------
|
||||
|
||||
func TestListIssuesByFilter_QueryParams(t *testing.T) {
|
||||
var capturedURL string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedURL = r.URL.RawQuery
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode([]gitea.Issue{{Number: 1, Title: "t1"}})
|
||||
}))
|
||||
|
||||
issues, err := c.ListIssuesByFilter(context.Background(), "owner", "repo", "assigned_by", "alice")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(issues) != 1 {
|
||||
t.Errorf("len = %d, want 1", len(issues))
|
||||
}
|
||||
for _, want := range []string{"type=issues", "state=all", "assigned_by=alice", "limit=50", "page=1"} {
|
||||
if !strings.Contains(capturedURL, want) {
|
||||
t.Errorf("query %q missing param %q", capturedURL, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- PostIssueComment sends correct body -----------------------------------
|
||||
|
||||
func TestPostIssueComment_Body(t *testing.T) {
|
||||
var gotBody map[string]string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
t.Errorf("method = %s, want POST", r.Method)
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Errorf("decode body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
json.NewEncoder(w).Encode(gitea.Comment{ID: 99, Body: "hello world"})
|
||||
}))
|
||||
|
||||
comment, err := c.PostIssueComment(context.Background(), "owner", "repo", 5, "hello world")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotBody["body"] != "hello world" {
|
||||
t.Errorf("posted body = %q, want %q", gotBody["body"], "hello world")
|
||||
}
|
||||
if comment == nil || comment.ID != 99 {
|
||||
t.Errorf("returned comment = %+v", comment)
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetDependencies returns issues in order --------------------------------
|
||||
|
||||
func TestGetDependencies_Order(t *testing.T) {
|
||||
deps := []gitea.Issue{
|
||||
{Number: 10, Title: "dep-one"},
|
||||
{Number: 20, Title: "dep-two"},
|
||||
{Number: 30, Title: "dep-three"},
|
||||
}
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.HasSuffix(r.URL.Path, "/dependencies") {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(deps)
|
||||
}))
|
||||
|
||||
got, err := c.GetDependencies(context.Background(), "owner", "repo", 5)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("len = %d, want 3", len(got))
|
||||
}
|
||||
wantNums := []int{10, 20, 30}
|
||||
for i, w := range wantNums {
|
||||
if got[i].Number != w {
|
||||
t.Errorf("dep[%d].Number = %d, want %d", i, got[i].Number, w)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- ListIssueComments pagination ------------------------------------------
|
||||
|
||||
func TestListIssueComments_Pagination(t *testing.T) {
|
||||
page1 := make([]gitea.Comment, 50)
|
||||
for i := range page1 {
|
||||
page1[i] = gitea.Comment{ID: i + 1}
|
||||
}
|
||||
page2 := []gitea.Comment{{ID: 51}, {ID: 52}}
|
||||
|
||||
callCount := 0
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if callCount == 1 {
|
||||
json.NewEncoder(w).Encode(page1)
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(page2)
|
||||
}
|
||||
}))
|
||||
|
||||
comments, err := c.ListIssueComments(context.Background(), "owner", "repo", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(comments) != 52 {
|
||||
t.Errorf("len = %d, want 52", len(comments))
|
||||
}
|
||||
}
|
||||
|
||||
// --- ListPullRequestFiles pagination ---------------------------------------
|
||||
|
||||
func TestListPullRequestFiles_Pagination(t *testing.T) {
|
||||
page1 := make([]gitea.PullRequestFile, 50)
|
||||
for i := range page1 {
|
||||
page1[i] = gitea.PullRequestFile{Filename: fmt.Sprintf("file%d.go", i)}
|
||||
}
|
||||
page2 := []gitea.PullRequestFile{{Filename: "extra.go"}}
|
||||
|
||||
callCount := 0
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if callCount == 1 {
|
||||
json.NewEncoder(w).Encode(page1)
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(page2)
|
||||
}
|
||||
}))
|
||||
|
||||
files, err := c.ListPullRequestFiles(context.Background(), "owner", "repo", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(files) != 51 {
|
||||
t.Errorf("len = %d, want 51", len(files))
|
||||
}
|
||||
}
|
||||
|
||||
// --- ListPullRequestCommits ------------------------------------------------
|
||||
|
||||
func TestListPullRequestCommits(t *testing.T) {
|
||||
var gotPath string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `[
|
||||
{"sha":"aaa111","commit":{"message":"first commit","author":{"name":"Alice"}}},
|
||||
{"sha":"bbb222","commit":{"message":"second commit","author":{"name":"Bob"}}}
|
||||
]`)
|
||||
}))
|
||||
|
||||
commits, err := c.ListPullRequestCommits(context.Background(), "owner", "repo", 7)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if want := "/api/v1/repos/owner/repo/pulls/7/commits"; gotPath != want {
|
||||
t.Errorf("path = %q, want %q", gotPath, want)
|
||||
}
|
||||
if len(commits) != 2 {
|
||||
t.Fatalf("len = %d, want 2", len(commits))
|
||||
}
|
||||
if commits[0].SHA != "aaa111" {
|
||||
t.Errorf("commits[0].SHA = %q, want aaa111", commits[0].SHA)
|
||||
}
|
||||
if commits[0].Commit.Message != "first commit" {
|
||||
t.Errorf("commits[0].Commit.Message = %q, want %q", commits[0].Commit.Message, "first commit")
|
||||
}
|
||||
if commits[1].Commit.Author.Name != "Bob" {
|
||||
t.Errorf("commits[1].Commit.Author.Name = %q, want Bob", commits[1].Commit.Author.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPullRequestCommits_Pagination(t *testing.T) {
|
||||
page1 := make([]gitea.PullRequestCommit, 50)
|
||||
for i := range page1 {
|
||||
page1[i].SHA = fmt.Sprintf("sha%d", i)
|
||||
}
|
||||
page2 := []gitea.PullRequestCommit{{SHA: "extra"}}
|
||||
|
||||
callCount := 0
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
callCount++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if callCount == 1 {
|
||||
json.NewEncoder(w).Encode(page1)
|
||||
} else {
|
||||
json.NewEncoder(w).Encode(page2)
|
||||
}
|
||||
}))
|
||||
|
||||
commits, err := c.ListPullRequestCommits(context.Background(), "owner", "repo", 1)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(commits) != 51 {
|
||||
t.Errorf("len = %d, want 51", len(commits))
|
||||
}
|
||||
}
|
||||
|
||||
// --- GetPullRequest --------------------------------------------------------
|
||||
|
||||
func TestGetPullRequest(t *testing.T) {
|
||||
payload := map[string]any{
|
||||
"number": 42,
|
||||
"merged": true,
|
||||
"state": "closed",
|
||||
"merged_at": "2024-01-15T10:00:00Z",
|
||||
"html_url": "https://gitea.example.com/owner/repo/pulls/42",
|
||||
"body": "Closes #7",
|
||||
"base": map[string]any{"ref": "release/v2"},
|
||||
}
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(payload)
|
||||
}))
|
||||
|
||||
pr, err := c.GetPullRequest(context.Background(), "owner", "repo", 42)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if pr.Number != 42 || !pr.Merged || pr.State != "closed" {
|
||||
t.Errorf("pr = %+v", pr)
|
||||
}
|
||||
if pr.Body != "Closes #7" {
|
||||
t.Errorf("pr.Body = %q", pr.Body)
|
||||
}
|
||||
if pr.Base.Ref != "release/v2" {
|
||||
t.Errorf("pr.Base.Ref = %q, want %q", pr.Base.Ref, "release/v2")
|
||||
}
|
||||
}
|
||||
|
||||
// --- APIError.Error() contains status and URL ------------------------------
|
||||
|
||||
func TestAPIError_ErrorString(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprint(w, "forbidden")
|
||||
}))
|
||||
|
||||
_, err := c.GetCurrentUser(context.Background())
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
s := err.Error()
|
||||
if !strings.Contains(s, "403") {
|
||||
t.Errorf("error string %q does not contain 403", s)
|
||||
}
|
||||
}
|
||||
|
||||
// --- URL path construction -------------------------------------------------
|
||||
|
||||
func TestGetIssue_URLPath(t *testing.T) {
|
||||
var gotPath string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotPath = r.URL.Path
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(gitea.Issue{Number: 3})
|
||||
}))
|
||||
|
||||
_, err := c.GetIssue(context.Background(), "myowner", "myrepo", 3)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
want := "/api/v1/repos/myowner/myrepo/issues/3"
|
||||
if gotPath != want {
|
||||
t.Errorf("path = %q, want %q", gotPath, want)
|
||||
}
|
||||
}
|
||||
|
||||
// --- CloseIssue sends PATCH with state=closed body ----------------------------
|
||||
|
||||
func TestCloseIssue_MethodPathBody(t *testing.T) {
|
||||
var gotMethod, gotPath string
|
||||
var gotBody map[string]string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Errorf("decode body: %v", err)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
fmt.Fprint(w, `{"number":5,"state":"closed"}`)
|
||||
}))
|
||||
|
||||
if err := c.CloseIssue(context.Background(), "owner", "repo", 5); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotMethod != http.MethodPatch {
|
||||
t.Errorf("method = %s, want PATCH", gotMethod)
|
||||
}
|
||||
if gotPath != "/api/v1/repos/owner/repo/issues/5" {
|
||||
t.Errorf("path = %s, want /api/v1/repos/owner/repo/issues/5", gotPath)
|
||||
}
|
||||
if gotBody["state"] != "closed" {
|
||||
t.Errorf("body state = %q, want closed", gotBody["state"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCloseIssue_APIError(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
fmt.Fprint(w, `{"message":"issue already closed"}`)
|
||||
}))
|
||||
|
||||
err := c.CloseIssue(context.Background(), "owner", "repo", 5)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err type = %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != http.StatusUnprocessableEntity {
|
||||
t.Errorf("Status = %d, want %d", apiErr.Status, http.StatusUnprocessableEntity)
|
||||
}
|
||||
if !strings.Contains(apiErr.Body, "already closed") {
|
||||
t.Errorf("Body = %q, want 'already closed'", apiErr.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DeleteIssue sends DELETE on correct path --------------------------------
|
||||
|
||||
func TestDeleteIssue_MethodPath(t *testing.T) {
|
||||
var gotMethod, gotPath string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotPath = r.URL.Path
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
if err := c.DeleteIssue(context.Background(), "owner", "repo", 7); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotMethod != http.MethodDelete {
|
||||
t.Errorf("method = %s, want DELETE", gotMethod)
|
||||
}
|
||||
if gotPath != "/api/v1/repos/owner/repo/issues/7" {
|
||||
t.Errorf("path = %s, want /api/v1/repos/owner/repo/issues/7", gotPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteIssue_APIError(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusForbidden)
|
||||
fmt.Fprint(w, `{"message":"only admins can delete issues"}`)
|
||||
}))
|
||||
|
||||
err := c.DeleteIssue(context.Background(), "owner", "repo", 7)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err type = %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != http.StatusForbidden {
|
||||
t.Errorf("Status = %d, want 403", apiErr.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- AddDependency sends POST with correct path and body -------------------
|
||||
|
||||
func TestAddDependency_MethodPathBody(t *testing.T) {
|
||||
var gotMethod, gotPath string
|
||||
var gotBody map[string]int
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Errorf("decode body: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusCreated)
|
||||
}))
|
||||
|
||||
if err := c.AddDependency(context.Background(), "owner", "repo", 5, 3); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotMethod != http.MethodPost {
|
||||
t.Errorf("method = %s, want POST", gotMethod)
|
||||
}
|
||||
if gotPath != "/api/v1/repos/owner/repo/issues/5/dependencies" {
|
||||
t.Errorf("path = %s, want /api/v1/repos/owner/repo/issues/5/dependencies", gotPath)
|
||||
}
|
||||
if gotBody["index"] != 3 {
|
||||
t.Errorf("body index = %d, want 3", gotBody["index"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddDependency_APIError(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnprocessableEntity)
|
||||
fmt.Fprint(w, `{"message":"dependency cycle"}`)
|
||||
}))
|
||||
|
||||
err := c.AddDependency(context.Background(), "owner", "repo", 5, 3)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err type = %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != http.StatusUnprocessableEntity {
|
||||
t.Errorf("Status = %d, want 422", apiErr.Status)
|
||||
}
|
||||
if !strings.Contains(apiErr.Body, "cycle") {
|
||||
t.Errorf("Body = %q, want 'cycle'", apiErr.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// --- RemoveDependency sends DELETE with correct path and body ---------------
|
||||
|
||||
func TestRemoveDependency_MethodPathBody(t *testing.T) {
|
||||
var gotMethod, gotPath string
|
||||
var gotBody map[string]int
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
gotMethod = r.Method
|
||||
gotPath = r.URL.Path
|
||||
if err := json.NewDecoder(r.Body).Decode(&gotBody); err != nil {
|
||||
t.Errorf("decode body: %v", err)
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
|
||||
if err := c.RemoveDependency(context.Background(), "owner", "repo", 5, 3); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if gotMethod != http.MethodDelete {
|
||||
t.Errorf("method = %s, want DELETE", gotMethod)
|
||||
}
|
||||
if gotPath != "/api/v1/repos/owner/repo/issues/5/dependencies" {
|
||||
t.Errorf("path = %s, want /api/v1/repos/owner/repo/issues/5/dependencies", gotPath)
|
||||
}
|
||||
if gotBody["index"] != 3 {
|
||||
t.Errorf("body index = %d, want 3", gotBody["index"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveDependency_APIError(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
fmt.Fprint(w, `{"message":"dependency not found"}`)
|
||||
}))
|
||||
|
||||
err := c.RemoveDependency(context.Background(), "owner", "repo", 5, 3)
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err type = %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != http.StatusNotFound {
|
||||
t.Errorf("Status = %d, want 404", apiErr.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify mustJSON doesn't appear in production (compile-time check via blank import).
|
||||
var _ = mustJSON
|
||||
|
||||
// --- ClosePullRequest -------------------------------------------------------
|
||||
|
||||
func TestClosePullRequest_200(t *testing.T) {
|
||||
var method, path string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
method = r.Method
|
||||
path = r.URL.Path
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
err := c.ClosePullRequest(context.Background(), "owner", "repo", 42)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if method != http.MethodPatch {
|
||||
t.Errorf("method = %q, want PATCH", method)
|
||||
}
|
||||
if path != "/api/v1/repos/owner/repo/issues/42" {
|
||||
t.Errorf("path = %q, want /api/v1/repos/owner/repo/issues/42", path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestClosePullRequest_422(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "unprocessable", http.StatusUnprocessableEntity)
|
||||
}))
|
||||
err := c.ClosePullRequest(context.Background(), "owner", "repo", 1)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err is %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != 422 {
|
||||
t.Errorf("Status = %d, want 422", apiErr.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- DeleteBranch -----------------------------------------------------------
|
||||
|
||||
func TestDeleteBranch_204(t *testing.T) {
|
||||
var method, rawPath string
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
method = r.Method
|
||||
// Use RawPath when set (encoded), fall back to Path.
|
||||
rawPath = r.URL.RawPath
|
||||
if rawPath == "" {
|
||||
rawPath = r.URL.Path
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
err := c.DeleteBranch(context.Background(), "owner", "repo", "feature/my-branch")
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if method != http.MethodDelete {
|
||||
t.Errorf("method = %q, want DELETE", method)
|
||||
}
|
||||
if rawPath != "/api/v1/repos/owner/repo/branches/feature%2Fmy-branch" {
|
||||
t.Errorf("rawPath = %q, want /api/v1/repos/owner/repo/branches/feature%%2Fmy-branch", rawPath)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteBranch_404(t *testing.T) {
|
||||
_, c := newServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
err := c.DeleteBranch(context.Background(), "owner", "repo", "gone")
|
||||
if err == nil {
|
||||
t.Fatal("expected error on 404")
|
||||
}
|
||||
var apiErr *gitea.APIError
|
||||
if !errors.As(err, &apiErr) {
|
||||
t.Fatalf("err is %T, want *gitea.APIError", err)
|
||||
}
|
||||
if apiErr.Status != 404 {
|
||||
t.Errorf("Status = %d, want 404", apiErr.Status)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package gitea_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"superwork-tui/internal/gitea"
|
||||
)
|
||||
|
||||
func TestNewNormalizesHost(t *testing.T) {
|
||||
cases := []struct {
|
||||
in string
|
||||
want string
|
||||
}{
|
||||
{"gitea.example.cn", "https://gitea.example.cn"},
|
||||
{"https://gitea.example.cn", "https://gitea.example.cn"},
|
||||
{"https://gitea.example.cn/", "https://gitea.example.cn"},
|
||||
{"http://localhost:3000", "http://localhost:3000"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := gitea.New(c.in, "tok").Host; got != c.want {
|
||||
t.Errorf("New(%q).Host = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user