You've already forked agentic-coding-workflow
test(acp): handler unit tests (admin / user / field redaction / PATCH semantics)
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user