package tools_test import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "testing" "gitea.d-ma.be/mathias/gitea-mcp/internal/allowlist" "gitea.d-ma.be/mathias/gitea-mcp/internal/gitea" "gitea.d-ma.be/mathias/gitea-mcp/internal/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func newRepoUpdateTool(srvURL string) *tools.RepoUpdate { return tools.NewRepoUpdate(gitea.NewClient(srvURL, "tok"), allowlist.New([]string{"mathias"})) } // TestRepoUpdateArchive: happy path — set archived=true. func TestRepoUpdateArchive(t *testing.T) { var patchedBody []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { require.Equal(t, http.MethodPatch, r.Method) require.Equal(t, "/api/v1/repos/mathias/old-svc", r.URL.Path) patchedBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"name":"old-svc","full_name":"mathias/old-svc","default_branch":"main","template":false,"private":false}`)) })) defer srv.Close() tool := newRepoUpdateTool(srv.URL) result, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"old-svc","archived":true}`, )) require.NoError(t, err) // Wire payload only contains the field that was actually set. var sent map[string]any require.NoError(t, json.Unmarshal(patchedBody, &sent)) assert.Equal(t, true, sent["archived"]) assert.NotContains(t, sent, "description") assert.NotContains(t, sent, "private") assert.NotContains(t, sent, "website") assert.NotContains(t, sent, "template") var repo gitea.Repo require.NoError(t, json.Unmarshal(result, &repo)) assert.Equal(t, "mathias/old-svc", repo.FullName) } // TestRepoUpdateMultipleFields: set description + template flag in one call. func TestRepoUpdateMultipleFields(t *testing.T) { var patchedBody []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { patchedBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"name":"template-go-agent","full_name":"mathias/template-go-agent","description":"Go agent template","template":true}`)) })) defer srv.Close() tool := newRepoUpdateTool(srv.URL) _, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"template-go-agent","description":"Go agent template","template":true}`, )) require.NoError(t, err) var sent map[string]any require.NoError(t, json.Unmarshal(patchedBody, &sent)) assert.Equal(t, "Go agent template", sent["description"]) assert.Equal(t, true, sent["template"]) assert.NotContains(t, sent, "archived") assert.NotContains(t, sent, "private") } // TestRepoUpdateNoFieldsRejected: zero updatable fields → validation error before network. func TestRepoUpdateNoFieldsRejected(t *testing.T) { tool := tools.NewRepoUpdate( gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}), ) _, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"some-repo"}`, )) require.Error(t, err) assert.ErrorIs(t, err, gitea.ErrValidation) } // TestRepoUpdateMakePublic: private=false requires confirm= as a safety // gate (kept from main #21 during the v02-patch merge). With confirm matching, the // patch goes through. func TestRepoUpdateMakePublic(t *testing.T) { var patchedBody []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { patchedBody, _ = io.ReadAll(r.Body) w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"name":"open-repo","full_name":"mathias/open-repo","private":false}`)) })) defer srv.Close() tool := newRepoUpdateTool(srv.URL) _, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"open-repo","private":false,"confirm":"open-repo"}`, )) require.NoError(t, err) var sent map[string]any require.NoError(t, json.Unmarshal(patchedBody, &sent)) assert.Equal(t, false, sent["private"]) } // TestRepoUpdateMakePublicWithoutConfirm: confirm gate blocks private=false without confirmation. func TestRepoUpdateMakePublicWithoutConfirm(t *testing.T) { tool := tools.NewRepoUpdate( gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}), ) _, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"open-repo","private":false}`, )) require.Error(t, err) assert.Contains(t, err.Error(), "confirm") } // TestRepoUpdateAllowlistRejects: owner outside allowlist denied without network call. func TestRepoUpdateAllowlistRejects(t *testing.T) { tool := tools.NewRepoUpdate( gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}), ) _, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"evil","name":"some-repo","archived":true}`, )) require.Error(t, err) } // TestRepoUpdateUpstreamError: server 500 propagates as ErrUpstream. func TestRepoUpdateUpstreamError(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"message":"internal"}`)) })) defer srv.Close() tool := newRepoUpdateTool(srv.URL) _, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"some-repo","archived":true}`, )) require.Error(t, err) assert.ErrorIs(t, err, gitea.ErrUpstream) }