Phase 1 test coverage for the three 0% packages plus agentci/ improvement: - git/ (0% -> 79.5%): RepoStatus methods, status parsing with real temp repos, multi-repo parallel status, Push/Pull error paths, ahead/behind with bare remote, context cancellation, GitError, IsNonFastForward, service DirtyRepos/AheadRepos filtering - forge/ (0% -> 91.2%): All SDK wrapper functions tested via httptest mock server — client creation, repos, issues, PRs, labels, webhooks, orgs, meta, config resolution, SetPRDraft raw HTTP endpoint - gitea/ (0% -> 89.2%): All SDK wrapper functions tested via httptest mock server — client creation, repos, issues, PRs, meta, config resolution - agentci/ (56% -> 94.5%): Clotho DeterminePlan all code paths, security helpers (SanitizePath, EscapeShellArg, SecureSSHCommand, MaskToken) Key findings documented in FINDINGS.md: - Forgejo SDK validates token via HTTP on NewClient() - SDK route patterns differ from public API docs (/org/ vs /orgs/) - Gitea SDK requires auth token for GitHub mirror creation - Config resolution priority verified: config file < env vars < flags Co-Authored-By: Charon <developers@lethean.io>
478 lines
14 KiB
Go
478 lines
14 KiB
Go
package forge
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
forgejo "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v2"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestNew_Good(t *testing.T) {
|
|
srv := newMockForgejoServer(t)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "test-token-123")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, client)
|
|
assert.NotNil(t, client.API())
|
|
assert.Equal(t, srv.URL, client.URL())
|
|
assert.Equal(t, "test-token-123", client.Token())
|
|
}
|
|
|
|
func TestNew_Bad_InvalidURL(t *testing.T) {
|
|
// The Forgejo SDK may reject certain URL formats.
|
|
_, err := New("://invalid-url", "token")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestClient_API_Good(t *testing.T) {
|
|
client, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
assert.NotNil(t, client.API(), "API() should return the underlying SDK client")
|
|
}
|
|
|
|
func TestClient_URL_Good(t *testing.T) {
|
|
client, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
assert.Equal(t, srv.URL, client.URL())
|
|
}
|
|
|
|
func TestClient_Token_Good(t *testing.T) {
|
|
client, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
assert.Equal(t, "test-token", client.Token())
|
|
}
|
|
|
|
func TestClient_GetCurrentUser_Good(t *testing.T) {
|
|
client, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
user, err := client.GetCurrentUser()
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-user", user.UserName)
|
|
}
|
|
|
|
func TestClient_GetCurrentUser_Bad_ServerError(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/api/v1/user", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.GetCurrentUser()
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to get current user")
|
|
}
|
|
|
|
func TestClient_SetPRDraft_Good(t *testing.T) {
|
|
client, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
err := client.SetPRDraft("owner", "repo", 1, true)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestClient_SetPRDraft_Good_Undraft(t *testing.T) {
|
|
client, srv := newTestClient(t)
|
|
defer srv.Close()
|
|
|
|
err := client.SetPRDraft("owner", "repo", 1, false)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func TestClient_SetPRDraft_Bad_ServerError(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method == http.MethodPatch {
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
err = client.SetPRDraft("owner", "repo", 1, true)
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "unexpected status 403")
|
|
}
|
|
|
|
func TestClient_SetPRDraft_Bad_ConnectionRefused(t *testing.T) {
|
|
// Use a closed server to simulate connection errors.
|
|
srv := newMockForgejoServer(t)
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
srv.Close() // Close the server.
|
|
|
|
err = client.SetPRDraft("owner", "repo", 1, true)
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestClient_SetPRDraft_URLConstruction(t *testing.T) {
|
|
// Verify the URL is constructed correctly by checking the request path.
|
|
var capturedPath string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
capturedPath = r.URL.Path
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"number": 42})
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
_ = client.SetPRDraft("my-org", "my-repo", 42, true)
|
|
assert.Equal(t, "/api/v1/repos/my-org/my-repo/pulls/42", capturedPath)
|
|
}
|
|
|
|
func TestClient_SetPRDraft_AuthHeader(t *testing.T) {
|
|
// Verify the authorisation header is set correctly.
|
|
var capturedAuth string
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
capturedAuth = r.Header.Get("Authorization")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusOK)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"number": 1})
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "my-secret-token")
|
|
require.NoError(t, err)
|
|
|
|
_ = client.SetPRDraft("owner", "repo", 1, true)
|
|
assert.Equal(t, "token my-secret-token", capturedAuth)
|
|
}
|
|
|
|
// --- PRMeta and Comment struct tests ---
|
|
|
|
func TestPRMeta_Fields(t *testing.T) {
|
|
meta := &PRMeta{
|
|
Number: 42,
|
|
Title: "Test PR",
|
|
State: "open",
|
|
Author: "testuser",
|
|
Branch: "feature/test",
|
|
BaseBranch: "main",
|
|
Labels: []string{"bug", "urgent"},
|
|
Assignees: []string{"dev1", "dev2"},
|
|
IsMerged: false,
|
|
CommentCount: 5,
|
|
}
|
|
|
|
assert.Equal(t, int64(42), meta.Number)
|
|
assert.Equal(t, "Test PR", meta.Title)
|
|
assert.Equal(t, "open", meta.State)
|
|
assert.Equal(t, "testuser", meta.Author)
|
|
assert.Equal(t, "feature/test", meta.Branch)
|
|
assert.Equal(t, "main", meta.BaseBranch)
|
|
assert.Equal(t, []string{"bug", "urgent"}, meta.Labels)
|
|
assert.Equal(t, []string{"dev1", "dev2"}, meta.Assignees)
|
|
assert.False(t, meta.IsMerged)
|
|
assert.Equal(t, 5, meta.CommentCount)
|
|
}
|
|
|
|
func TestComment_Fields(t *testing.T) {
|
|
comment := Comment{
|
|
ID: 123,
|
|
Author: "reviewer",
|
|
Body: "LGTM",
|
|
}
|
|
|
|
assert.Equal(t, int64(123), comment.ID)
|
|
assert.Equal(t, "reviewer", comment.Author)
|
|
assert.Equal(t, "LGTM", comment.Body)
|
|
}
|
|
|
|
// --- MergePullRequest merge style mapping ---
|
|
|
|
func TestMergePullRequest_StyleMapping(t *testing.T) {
|
|
// We can't easily test the SDK call, but we can verify the method
|
|
// errors when the server returns failure. This exercises the style mapping code.
|
|
tests := []struct {
|
|
name string
|
|
method string
|
|
}{
|
|
{name: "merge", method: "merge"},
|
|
{name: "squash", method: "squash"},
|
|
{name: "rebase", method: "rebase"},
|
|
{name: "default (unknown)", method: "unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
// Return 405 to trigger an error so we know the code executed.
|
|
http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
err = client.MergePullRequest("owner", "repo", 1, tt.method)
|
|
assert.Error(t, err, "merge should fail against mock server for method %s", tt.method)
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- ListIssuesOpts defaulting ---
|
|
|
|
func TestListIssuesOpts_Defaults(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
opts ListIssuesOpts
|
|
expectedState string
|
|
expectedLimit int
|
|
expectedPage int
|
|
}{
|
|
{
|
|
name: "all defaults",
|
|
opts: ListIssuesOpts{},
|
|
expectedState: "open",
|
|
expectedLimit: 50,
|
|
expectedPage: 1,
|
|
},
|
|
{
|
|
name: "closed state",
|
|
opts: ListIssuesOpts{State: "closed"},
|
|
expectedState: "closed",
|
|
expectedLimit: 50,
|
|
expectedPage: 1,
|
|
},
|
|
{
|
|
name: "all state",
|
|
opts: ListIssuesOpts{State: "all"},
|
|
expectedState: "all",
|
|
expectedLimit: 50,
|
|
expectedPage: 1,
|
|
},
|
|
{
|
|
name: "custom limit and page",
|
|
opts: ListIssuesOpts{Page: 3, Limit: 25},
|
|
expectedState: "open",
|
|
expectedLimit: 25,
|
|
expectedPage: 3,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Verify the opts struct stores values correctly.
|
|
if tt.opts.State == "" {
|
|
tt.opts.State = "open"
|
|
}
|
|
assert.Equal(t, tt.expectedState, tt.opts.State)
|
|
|
|
limit := tt.opts.Limit
|
|
if limit == 0 {
|
|
limit = 50
|
|
}
|
|
assert.Equal(t, tt.expectedLimit, limit)
|
|
|
|
page := tt.opts.Page
|
|
if page == 0 {
|
|
page = 1
|
|
}
|
|
assert.Equal(t, tt.expectedPage, page)
|
|
})
|
|
}
|
|
}
|
|
|
|
// --- ForkRepo error handling ---
|
|
|
|
func TestClient_ForkRepo_Good_WithOrg(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
var capturedBody map[string]any
|
|
mux.HandleFunc("/api/v1/repos/owner/repo/forks", func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewDecoder(r.Body).Decode(&capturedBody)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": 1,
|
|
"name": "repo",
|
|
"full_name": "target-org/repo",
|
|
"owner": map[string]any{"login": "target-org"},
|
|
})
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
fork, err := client.ForkRepo("owner", "repo", "target-org")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, fork)
|
|
assert.Equal(t, "target-org", capturedBody["organization"])
|
|
}
|
|
|
|
func TestClient_ForkRepo_Good_WithoutOrg(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
var capturedBody map[string]any
|
|
mux.HandleFunc("/api/v1/repos/owner/repo/forks", func(w http.ResponseWriter, r *http.Request) {
|
|
_ = json.NewDecoder(r.Body).Decode(&capturedBody)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
w.WriteHeader(http.StatusAccepted)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": 2,
|
|
"name": "repo",
|
|
"full_name": "user/repo",
|
|
"owner": map[string]any{"login": "user"},
|
|
})
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
fork, err := client.ForkRepo("owner", "repo", "")
|
|
require.NoError(t, err)
|
|
assert.NotNil(t, fork)
|
|
// When org is empty, the Organization pointer is nil.
|
|
// The SDK may or may not include it in the JSON; just verify the fork succeeded.
|
|
}
|
|
|
|
func TestClient_ForkRepo_Bad_ServerError(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "Server Error", http.StatusInternalServerError)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.ForkRepo("owner", "repo", "")
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to fork")
|
|
}
|
|
|
|
// --- CreatePullRequest error handling ---
|
|
|
|
func TestClient_CreatePullRequest_Bad_ServerError(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "Server Error", http.StatusInternalServerError)
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
_, err = client.CreatePullRequest("owner", "repo", forgejo.CreatePullRequestOption{
|
|
Head: "feature",
|
|
Base: "main",
|
|
Title: "Test PR",
|
|
})
|
|
assert.Error(t, err)
|
|
assert.Contains(t, err.Error(), "failed to create pull request")
|
|
}
|
|
|
|
// --- commentPageSize constant test ---
|
|
|
|
func TestCommentPageSize(t *testing.T) {
|
|
assert.Equal(t, 50, commentPageSize, "comment page size should be 50")
|
|
}
|
|
|
|
// --- ListPullRequests state mapping ---
|
|
|
|
func TestListPullRequests_StateMapping(t *testing.T) {
|
|
// Verify state mapping via error path (server returns error).
|
|
tests := []struct {
|
|
name string
|
|
state string
|
|
}{
|
|
{name: "open", state: "open"},
|
|
{name: "closed", state: "closed"},
|
|
{name: "all", state: "all"},
|
|
{name: "default", state: ""},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
mux := http.NewServeMux()
|
|
mux.HandleFunc("/api/v1/version", func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]string{"version": "1.21.0"})
|
|
})
|
|
|
|
var capturedState string
|
|
mux.HandleFunc(fmt.Sprintf("/api/v1/repos/owner/repo/pulls"), func(w http.ResponseWriter, r *http.Request) {
|
|
capturedState = r.URL.Query().Get("state")
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode([]any{})
|
|
})
|
|
srv := httptest.NewServer(mux)
|
|
defer srv.Close()
|
|
|
|
client, err := New(srv.URL, "token")
|
|
require.NoError(t, err)
|
|
|
|
_, _ = client.ListPullRequests("owner", "repo", tt.state)
|
|
// The state parameter was passed to the SDK and sent to the server.
|
|
assert.NotEmpty(t, capturedState)
|
|
})
|
|
}
|
|
}
|