test(acp): handler unit tests (admin / user / field redaction / PATCH semantics)

This commit is contained in:
2026-05-07 11:37:33 +08:00
parent e096b1a671
commit 8202a7a679
+161
View File
@@ -0,0 +1,161 @@
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)
}