package tools_test import ( "context" "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" "git.d-ma.be/mathias/gitea-mcp/internal/allowlist" "git.d-ma.be/mathias/gitea-mcp/internal/gitea" "git.d-ma.be/mathias/gitea-mcp/internal/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) // runsListJSON is a workflow_runs listing body with a single run of the given id. func runsListJSON(id int) string { return `{"total_count":1,"workflow_runs":[{"id":` + fmtInt(id) + `,"status":"queued","event":"workflow_dispatch","html_url":"http://gitea.example/mathias/myrepo/actions/runs/` + fmtInt(id) + `"}]}` } func fmtInt(i int) string { b, _ := json.Marshal(i); return string(b) } // The dispatch endpoint returns 204 with NO Location header (real Gitea). The // tool must treat that as success, forward inputs, and resolve the new run by // listing workflow_dispatch runs and picking the newest one above the // pre-dispatch baseline. func TestWorkflowRunTriggerResolvesRunViaListing(t *testing.T) { dispatched := false var gotBody []byte srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/v1/repos/mathias/myrepo/actions/workflows/ci.yml/dispatches" && r.Method == http.MethodPost: gotBody, _ = io.ReadAll(r.Body) dispatched = true w.WriteHeader(http.StatusNoContent) // no Location header case strings.HasPrefix(r.URL.Path, "/api/v1/repos/mathias/myrepo/actions/runs"): w.Header().Set("Content-Type", "application/json") if !dispatched { _, _ = w.Write([]byte(`{"total_count":0,"workflow_runs":[]}`)) // baseline: none yet return } _, _ = w.Write([]byte(runsListJSON(100))) default: http.NotFound(w, r) } })) defer srv.Close() tool := tools.NewWorkflowRunTrigger(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"mathias"}), srv.URL) out, err := tool.Call(context.Background(), json.RawMessage( `{"owner":"mathias","name":"myrepo","workflow":"ci.yml","ref":"main","inputs":{"issue_number":"36","harness":"agentsquad"}}`)) require.NoError(t, err) assert.True(t, dispatched, "expected POST dispatch") // inputs forwarded to the dispatch body var body map[string]any require.NoError(t, json.Unmarshal(gotBody, &body)) assert.Equal(t, "main", body["ref"]) inputs, ok := body["inputs"].(map[string]any) require.True(t, ok, "inputs must be present in dispatch body") assert.Equal(t, "36", inputs["issue_number"]) assert.Equal(t, "agentsquad", inputs["harness"]) // run id resolved via listing (not a Location header) var result map[string]any require.NoError(t, json.Unmarshal(out, &result)) assert.Equal(t, float64(100), result["run_id"]) assert.Contains(t, result["html_url"], "/actions/runs/100") } func TestWorkflowRunTriggerDefaultBranch(t *testing.T) { repoHit := false dispatched := false srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/api/v1/repos/mathias/myrepo" && r.Method == http.MethodGet: repoHit = true w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"name":"myrepo","full_name":"mathias/myrepo","default_branch":"main"}`)) case r.URL.Path == "/api/v1/repos/mathias/myrepo/actions/workflows/ci.yml/dispatches" && r.Method == http.MethodPost: dispatched = true w.WriteHeader(http.StatusNoContent) case strings.HasPrefix(r.URL.Path, "/api/v1/repos/mathias/myrepo/actions/runs"): w.Header().Set("Content-Type", "application/json") if !dispatched { _, _ = w.Write([]byte(`{"total_count":0,"workflow_runs":[]}`)) return } _, _ = w.Write([]byte(runsListJSON(55))) default: http.NotFound(w, r) } })) defer srv.Close() tool := tools.NewWorkflowRunTrigger(gitea.NewClient(srv.URL, "tok"), allowlist.New([]string{"mathias"}), srv.URL) out, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"mathias","name":"myrepo","workflow":"ci.yml"}`)) require.NoError(t, err) assert.True(t, repoHit, "expected GET /repo for default branch") var result map[string]any require.NoError(t, json.Unmarshal(out, &result)) assert.Equal(t, float64(55), result["run_id"]) } func TestWorkflowRunTriggerAllowlistRejects(t *testing.T) { tool := tools.NewWorkflowRunTrigger(gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}), "http://unused") _, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"evil","name":"repo","workflow":"ci.yml"}`)) require.Error(t, err) } func TestWorkflowRunTriggerRequiresWorkflow(t *testing.T) { // workflow field is present in required schema but let's test empty string fallback guard tool := tools.NewWorkflowRunTrigger(gitea.NewClient("http://unused", ""), allowlist.New([]string{"mathias"}), "http://unused") _, err := tool.Call(context.Background(), json.RawMessage(`{"owner":"mathias","name":"repo","workflow":""}`)) require.Error(t, err) assert.Contains(t, err.Error(), "workflow") }