package acp_test import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/yan1h/agent-coding-workflow/internal/acp" ) // fakeResolver is a stub middleware.SessionResolver that accepts the literal "valid" token. type fakeResolver struct{ uid uuid.UUID } func (f fakeResolver) ResolveSession(_ context.Context, token string) (uuid.UUID, bool, error) { if token == "valid" { return f.uid, true, nil } return uuid.Nil, false, nil } // fakeAdminLookup is a stub AdminLookup keyed by user ID. type fakeAdminLookup struct{ admin map[uuid.UUID]bool } func (f fakeAdminLookup) IsAdmin(_ context.Context, uid uuid.UUID) (bool, error) { return f.admin[uid], nil } // mountHandlerWithRepo lets a test share one fakeAgentKindRepo across multiple // mounts (e.g. one as admin, one as non-admin) so the data set is consistent. func mountHandlerWithRepo(t *testing.T, callerUID uuid.UUID, isAdmin bool, repo *fakeAgentKindRepo) chi.Router { t.Helper() if repo == nil { repo = newFakeAgentKindRepo() } enc := testEncryptor(t) svc := acp.NewAgentKindService(repo, enc, &agentKindRecordingRecorder{}) r := chi.NewRouter() h := acp.NewHandler( svc, fakeResolver{uid: callerUID}, fakeAdminLookup{admin: map[uuid.UUID]bool{callerUID: isAdmin}}, enc, ) h.Mount(r) return r } func doJSON(t *testing.T, r chi.Router, method, path, token string, body any) (*httptest.ResponseRecorder, map[string]any) { t.Helper() var buf bytes.Buffer if body != nil { _ = json.NewEncoder(&buf).Encode(body) } req := httptest.NewRequest(method, path, &buf) if token != "" { req.Header.Set("Authorization", "Bearer "+token) } rr := httptest.NewRecorder() r.ServeHTTP(rr, req) var out map[string]any if rr.Body.Len() > 0 { _ = json.Unmarshal(rr.Body.Bytes(), &out) } return rr, out } func TestHandler_AgentKind_Admin_CreateGetList(t *testing.T) { t.Parallel() uid := uuid.New() r := mountHandlerWithRepo(t, uid, true, nil) // Create rr, body := doJSON(t, r, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{ "name": "claude_code", "display_name": "Claude Code", "binary_path": "claude-code", "args": []string{"--acp"}, "env": map[string]string{"K": "v"}, "enabled": true, }) require.Equal(t, http.StatusCreated, rr.Code, "body=%s", rr.Body.String()) id := body["id"].(string) assert.Equal(t, "claude_code", body["name"]) envKeys := body["env_keys"].([]any) assert.Equal(t, []any{"K"}, envKeys) // Get rr, body = doJSON(t, r, "GET", "/api/v1/acp/agent-kinds/"+id, "valid", nil) require.Equal(t, http.StatusOK, rr.Code) assert.Equal(t, "Claude Code", body["display_name"]) // List rr, _ = doJSON(t, r, "GET", "/api/v1/acp/agent-kinds", "valid", nil) require.Equal(t, http.StatusOK, rr.Code) } func TestHandler_AgentKind_NonAdmin_CannotWrite_FieldsRedacted(t *testing.T) { t.Parallel() uid := uuid.New() repo := newFakeAgentKindRepo() // Admin creates one kind via the handler so audit + repo state are consistent. adminR := mountHandlerWithRepo(t, uid, true, repo) rr, _ := doJSON(t, adminR, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{ "name": "claude_code", "display_name": "Claude Code", "binary_path": "claude-code", "args": []string{"--acp"}, "env": map[string]string{"K": "v"}, "enabled": true, }) require.Equal(t, http.StatusCreated, rr.Code) // Same UID but treated as non-admin (fakeAdminLookup says false). userR := mountHandlerWithRepo(t, uid, false, repo) // List as non-admin: returns AgentKindPublicDTO array (no binary_path / env_keys). rr2 := httptest.NewRecorder() req2 := httptest.NewRequest("GET", "/api/v1/acp/agent-kinds", nil) req2.Header.Set("Authorization", "Bearer valid") userR.ServeHTTP(rr2, req2) require.Equal(t, http.StatusOK, rr2.Code) var arr []map[string]any require.NoError(t, json.Unmarshal(rr2.Body.Bytes(), &arr)) require.Len(t, arr, 1) assert.Equal(t, "claude_code", arr[0]["name"]) assert.NotContains(t, arr[0], "binary_path") assert.NotContains(t, arr[0], "env_keys") assert.NotContains(t, arr[0], "args") // Non-admin POST → 403 Forbidden from the service layer. rr3, _ := doJSON(t, userR, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{ "name": "x", "display_name": "x", "binary_path": "x", }) assert.Equal(t, http.StatusForbidden, rr3.Code) } func TestHandler_AgentKind_Update_PatchSemantics(t *testing.T) { t.Parallel() uid := uuid.New() r := mountHandlerWithRepo(t, uid, true, nil) rr, body := doJSON(t, r, "POST", "/api/v1/acp/agent-kinds", "valid", map[string]any{ "name": "k", "display_name": "K", "binary_path": "/x", }) require.Equal(t, http.StatusCreated, rr.Code, "body=%s", rr.Body.String()) id := body["id"].(string) // PATCH display_name only — other fields preserved. rr, body = doJSON(t, r, "PATCH", "/api/v1/acp/agent-kinds/"+id, "valid", map[string]any{ "display_name": "Renamed", }) require.Equal(t, http.StatusOK, rr.Code, "body=%s", rr.Body.String()) assert.Equal(t, "Renamed", body["display_name"]) assert.Equal(t, "/x", body["binary_path"]) // DELETE rr, _ = doJSON(t, r, "DELETE", "/api/v1/acp/agent-kinds/"+id, "valid", nil) assert.Equal(t, http.StatusNoContent, rr.Code) }