Implements slices 4 and 5 of the OpenSpec change
mcp-resource-templates.
Slice 4 — repo + owner
- operation/repo/resources_repo.go registers
forgejo://repo/{owner}/{repo}: identity + counts only,
no embedded lists
- operation/user/resources_owner.go registers
forgejo://owner/{owner}: resolves user first, falls back to
org on 404; payload includes kind ("user" | "org") + omitempty
kind-specific counts
- Both wired from RegisterCoreResources
Slice 5 — issue + comment
- operation/issue/resources.go registers two templates from one
RegisterIssueResources entry point:
- forgejo://repo/{owner}/{repo}/issue/{index}: metadata + JSON
primary + text/markdown sidecar + bounded recent_comments
(cap 30, sentinel names list_issue_comments)
- forgejo://repo/{owner}/{repo}/{kind}/{index}/comment/{id}:
kind ∈ {issue, pr}; PR comments go through the Forgejo
issue-comment API
- operation/resource/errors.go extended with -32602
invalid-params mapping for parser errors ("kind must be ...",
"index must be numeric"); errors_test.go covers the new path
Tests
- 11 new test cases across operation/repo/, operation/user/,
operation/issue/ (forgejo.SetClientForTesting seam)
- resource pkg up to 21 PASS lines (added -32602 case)
- Full suite green
Tasks 4.1-4.5 and 5.1-5.6 ticked
(35/47 in tasks.md; 1.11 manual client check + slices 6-7 remain).
Driven by team:dev-loop. Continues forgejo-mcp-13x.
131 lines
3.6 KiB
Go
131 lines
3.6 KiB
Go
package repo
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"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 setupRepoMockServer(t *testing.T, statusCode 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(statusCode)
|
|
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 makeRepoResourceRequest(owner, repo string) mcp.ReadResourceRequest {
|
|
return mcp.ReadResourceRequest{
|
|
Params: mcp.ReadResourceParams{
|
|
URI: "forgejo://repo/" + owner + "/" + repo,
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestRepoResourceHandler_HappyPath(t *testing.T) {
|
|
fakeRepo := map[string]interface{}{
|
|
"id": 1,
|
|
"name": "forgejo-mcp",
|
|
"full_name": "goern/forgejo-mcp",
|
|
"owner": map[string]interface{}{
|
|
"login": "goern",
|
|
},
|
|
"description": "MCP server for Forgejo",
|
|
"html_url": "https://codeberg.org/goern/forgejo-mcp",
|
|
"default_branch": "main",
|
|
"fork": false,
|
|
"archived": false,
|
|
"private": false,
|
|
"stars_count": 42,
|
|
"forks_count": 7,
|
|
"watchers_count": 15,
|
|
"open_issues_count": 3,
|
|
"open_pr_counter": 1,
|
|
"size": 512,
|
|
"has_issues": true,
|
|
"has_wiki": false,
|
|
"has_pull_requests": true,
|
|
}
|
|
srv := setupRepoMockServer(t, http.StatusOK, fakeRepo)
|
|
defer srv.Close()
|
|
|
|
req := makeRepoResourceRequest("goern", "forgejo-mcp")
|
|
contents, err := repoResourceHandler(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if len(contents) != 1 {
|
|
t.Fatalf("expected 1 content block, got %d", len(contents))
|
|
}
|
|
|
|
block, ok := contents[0].(mcp.TextResourceContents)
|
|
if !ok {
|
|
t.Fatal("content must be TextResourceContents")
|
|
}
|
|
if block.MIMEType != "application/json" {
|
|
t.Errorf("MIME type: got %q, want application/json", block.MIMEType)
|
|
}
|
|
|
|
var payload repoResourcePayload
|
|
if err := json.Unmarshal([]byte(block.Text), &payload); err != nil {
|
|
t.Fatalf("response is not valid JSON: %v", err)
|
|
}
|
|
if payload.Name != "forgejo-mcp" {
|
|
t.Errorf("expected name=forgejo-mcp, got %q", payload.Name)
|
|
}
|
|
if payload.StarsCount != 42 {
|
|
t.Errorf("expected stars=42, got %d", payload.StarsCount)
|
|
}
|
|
if payload.HasIssues != true {
|
|
t.Error("expected has_issues=true")
|
|
}
|
|
}
|
|
|
|
func TestRepoResourceHandler_403(t *testing.T) {
|
|
srv := setupRepoMockServer(t, http.StatusForbidden, map[string]string{"message": "Forbidden"})
|
|
defer srv.Close()
|
|
|
|
req := makeRepoResourceRequest("goern", "private-repo")
|
|
_, err := repoResourceHandler(context.Background(), req)
|
|
if err == nil {
|
|
t.Fatal("expected error for 403")
|
|
}
|
|
if re, ok := err.(*resource.ResourceError); ok {
|
|
if re.Code != -32002 {
|
|
t.Errorf("expected code -32002, got %d", re.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRepoResourceHandler_404(t *testing.T) {
|
|
srv := setupRepoMockServer(t, http.StatusNotFound, map[string]string{"message": "Not Found"})
|
|
defer srv.Close()
|
|
|
|
req := makeRepoResourceRequest("goern", "no-such-repo")
|
|
_, err := repoResourceHandler(context.Background(), req)
|
|
if err == nil {
|
|
t.Fatal("expected error for 404")
|
|
}
|
|
if re, ok := err.(*resource.ResourceError); ok {
|
|
if re.Code != -32003 {
|
|
t.Errorf("expected code -32003, got %d", re.Code)
|
|
}
|
|
}
|
|
}
|