forgejo-mcp/operation/branchprotection/resources_branchprotection_test.go
Christoph Görn dd9116fbca
fix: 🚨 clear golangci-lint v2 backlog (closes bead forgejo-mcp-hc9)
- errorlint: fmt.Errorf %v→%w across all handlers (errors now wrap the
  underlying SDK/HTTP error for errors.Is/As); errors.Is for
  context.Canceled in cmd
- staticcheck quickfixes: Fprintf over WriteString(Sprintf), tagged
  switches, omit inferred 'any'; QF1001 resolved by hoisting hex checks
  into isHex predicates (normalizeColor, validateSHA)
- govet: reflect.Ptr → reflect.Pointer in pkg/ptr
- errcheck: explicit '_ =' for fs.Parse (ExitOnError) and deferred
  zap Sync
- repair four test assertions the autofix broke (it dropped the
  're.Code != X' half of combined type-assert checks, inverting the
  test); all errors.As sites use 'var re *resource.ResourceError'

golangci-lint: 0 issues. staticcheck, vet, tests: clean.
2026-06-11 00:03:36 +02:00

158 lines
5.2 KiB
Go

package branchprotection
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"codeberg.org/goern/forgejo-mcp/v2/operation/resource"
"codeberg.org/goern/forgejo-mcp/v2/pkg/forgejo"
forgejo_sdk "codeberg.org/mvdkleijn/forgejo-sdk/forgejo/v3"
"github.com/mark3labs/mcp-go/mcp"
)
func setupBPResourceServer(t *testing.T, status int, body interface{}) *httptest.Server {
t.Helper()
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if body != nil {
_ = json.NewEncoder(w).Encode(body)
}
}))
client, err := forgejo_sdk.NewClient(srv.URL, forgejo_sdk.SetForgejoVersion("7.0.0"))
if err != nil {
t.Fatalf("creating test client: %v", err)
}
forgejo.SetClientForTesting(client)
return srv
}
func readResourceReq(uri string) mcp.ReadResourceRequest {
return mcp.ReadResourceRequest{Params: mcp.ReadResourceParams{URI: uri}}
}
func resourceText(t *testing.T, contents []mcp.ResourceContents) string {
t.Helper()
if len(contents) != 1 {
t.Fatalf("expected 1 content block, got %d", len(contents))
}
tc, ok := contents[0].(mcp.TextResourceContents)
if !ok {
t.Fatalf("expected TextResourceContents, got %T", contents[0])
}
return tc.Text
}
func TestBranchProtectionsResource_HappyPath(t *testing.T) {
srv := setupBPResourceServer(t, http.StatusOK, []map[string]interface{}{
{"rule_name": "main", "branch_name": "main", "required_approvals": 1},
{"rule_name": "release/*", "branch_name": "release/*"},
})
defer srv.Close()
contents, err := branchProtectionsResourceHandler(context.Background(),
readResourceReq("forgejo://repo/goern/forgejo-mcp/branch_protections"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var payload bpCollectionPayload
if err := json.Unmarshal([]byte(resourceText(t, contents)), &payload); err != nil {
t.Fatalf("unmarshal payload: %v", err)
}
if payload.TotalCount != 2 || len(payload.BranchProtections) != 2 {
t.Errorf("expected 2 rules, got total=%d shown=%d", payload.TotalCount, len(payload.BranchProtections))
}
if payload.Truncated {
t.Error("did not expect truncation for 2 rules")
}
}
func TestBranchProtectionsResource_Truncation(t *testing.T) {
rules := make([]map[string]interface{}, resource.EmbeddedListCap+1)
for i := range rules {
rules[i] = map[string]interface{}{"rule_name": fmt.Sprintf("rule-%d", i), "branch_name": fmt.Sprintf("b-%d", i)}
}
srv := setupBPResourceServer(t, http.StatusOK, rules)
defer srv.Close()
contents, err := branchProtectionsResourceHandler(context.Background(),
readResourceReq("forgejo://repo/goern/forgejo-mcp/branch_protections"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var payload bpCollectionPayload
if err := json.Unmarshal([]byte(resourceText(t, contents)), &payload); err != nil {
t.Fatalf("unmarshal payload: %v", err)
}
if !payload.Truncated {
t.Error("expected truncation when over EmbeddedListCap")
}
if len(payload.BranchProtections) != resource.EmbeddedListCap {
t.Errorf("expected %d shown rules, got %d", resource.EmbeddedListCap, len(payload.BranchProtections))
}
if payload.ListTool != "list_branch_protections" {
t.Errorf("expected list_tool sentinel, got %q", payload.ListTool)
}
}
func TestBranchProtectionsResource_NotFound(t *testing.T) {
srv := setupBPResourceServer(t, http.StatusNotFound, map[string]string{"message": "Not Found"})
defer srv.Close()
_, err := branchProtectionsResourceHandler(context.Background(),
readResourceReq("forgejo://repo/goern/missing/branch_protections"))
if err == nil {
t.Fatal("expected error for 404")
}
var re *resource.ResourceError
ok := errors.As(err, &re)
if !ok {
t.Fatalf("expected *resource.ResourceError, got %T", err)
}
if re.Code != -32003 {
t.Errorf("expected code -32003 (not found), got %d", re.Code)
}
}
func TestBranchProtectionResource_HappyPath(t *testing.T) {
srv := setupBPResourceServer(t, http.StatusOK, map[string]interface{}{
"rule_name": "main", "branch_name": "main",
"status_check_contexts": []string{"ci/build"}, "required_approvals": 2,
})
defer srv.Close()
contents, err := branchProtectionResourceHandler(context.Background(),
readResourceReq("forgejo://repo/goern/forgejo-mcp/branch_protection/main"))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
var bp forgejo_sdk.BranchProtection
if err := json.Unmarshal([]byte(resourceText(t, contents)), &bp); err != nil {
t.Fatalf("unmarshal payload: %v", err)
}
if bp.RuleName != "main" || bp.RequiredApprovals != 2 || len(bp.StatusCheckContexts) != 1 {
t.Errorf("unexpected protection state: %+v", bp)
}
}
func TestBranchProtectionResource_MalformedURI(t *testing.T) {
// No server needed: parse fails before any client call.
_, err := branchProtectionResourceHandler(context.Background(),
readResourceReq("forgejo://repo/goern/forgejo-mcp/branch_protection"))
if err == nil {
t.Fatal("expected error for malformed URI (missing rule)")
}
var re *resource.ResourceError
ok := errors.As(err, &re)
if !ok {
t.Fatalf("expected *resource.ResourceError, got %T", err)
}
if re.Code != -32602 {
t.Errorf("expected code -32602 (invalid params), got %d", re.Code)
}
}