You've already forked agentic-coding-workflow
672 lines
21 KiB
Go
672 lines
21 KiB
Go
package mcp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
mcpsdk "github.com/modelcontextprotocol/go-sdk/mcp"
|
|
|
|
"github.com/yan1h/agent-coding-workflow/internal/infra/errs"
|
|
"github.com/yan1h/agent-coding-workflow/internal/project"
|
|
"github.com/yan1h/agent-coding-workflow/internal/user"
|
|
"github.com/yan1h/agent-coding-workflow/internal/workspace"
|
|
)
|
|
|
|
// ===== fakes =====
|
|
|
|
// fakeUserSvc implements user.Service for tests.
|
|
type fakeUserSvc struct{ users map[uuid.UUID]*user.User }
|
|
|
|
func (s *fakeUserSvc) Login(_ context.Context, _, _, _ string) (string, *user.User, error) {
|
|
return "", nil, nil
|
|
}
|
|
func (s *fakeUserSvc) Logout(_ context.Context, _ string) error { return nil }
|
|
func (s *fakeUserSvc) ResolveSession(_ context.Context, _ string) (uuid.UUID, bool, error) {
|
|
return uuid.Nil, false, nil
|
|
}
|
|
func (s *fakeUserSvc) Get(_ context.Context, id uuid.UUID) (*user.User, error) {
|
|
if u, ok := s.users[id]; ok {
|
|
return u, nil
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "user")
|
|
}
|
|
func (s *fakeUserSvc) Bootstrap(_ context.Context, _, _ string) (*user.User, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeUserSvc) ListAdmins(_ context.Context) ([]*user.User, error) { return nil, nil }
|
|
|
|
func (s *fakeUserSvc) ListUsers(_ context.Context) ([]*user.User, error) { return nil, nil }
|
|
func (s *fakeUserSvc) CreateUser(_ context.Context, _ user.CreateUserInput) (*user.User, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeUserSvc) UpdateProfile(_ context.Context, _ uuid.UUID, _ string) (*user.User, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeUserSvc) SetAdmin(_ context.Context, _, _ uuid.UUID, _ bool) (*user.User, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeUserSvc) SetEnabled(_ context.Context, _, _ uuid.UUID, _ bool) (*user.User, error) {
|
|
return nil, nil
|
|
}
|
|
func (s *fakeUserSvc) AdminResetPassword(_ context.Context, _ uuid.UUID) (string, error) {
|
|
return "", nil
|
|
}
|
|
func (s *fakeUserSvc) ChangePassword(_ context.Context, _ uuid.UUID, _, _ string) error { return nil }
|
|
func (s *fakeUserSvc) DeleteUser(_ context.Context, _, _ uuid.UUID) error { return nil }
|
|
|
|
// fakeProjectSvc implements project.ProjectService.
|
|
type fakeProjectSvc struct {
|
|
projects []*project.Project
|
|
slugMap map[string]*project.Project
|
|
idMap map[uuid.UUID]*project.Project
|
|
}
|
|
|
|
func newFakeProjectSvc(ps ...*project.Project) *fakeProjectSvc {
|
|
f := &fakeProjectSvc{slugMap: map[string]*project.Project{}, idMap: map[uuid.UUID]*project.Project{}}
|
|
for _, p := range ps {
|
|
f.projects = append(f.projects, p)
|
|
f.slugMap[p.Slug] = p
|
|
f.idMap[p.ID] = p
|
|
}
|
|
return f
|
|
}
|
|
func (f *fakeProjectSvc) Create(_ context.Context, c project.Caller, in project.CreateProjectInput) (*project.Project, error) {
|
|
v := in.Visibility
|
|
if v == "" {
|
|
v = project.VisibilityPrivate
|
|
}
|
|
p := &project.Project{
|
|
ID: uuid.New(), Slug: in.Slug, Name: in.Name, Description: in.Description,
|
|
Visibility: v, OwnerID: c.UserID, CreatedAt: time.Now(),
|
|
}
|
|
f.projects = append(f.projects, p)
|
|
f.slugMap[p.Slug] = p
|
|
f.idMap[p.ID] = p
|
|
return p, nil
|
|
}
|
|
func (f *fakeProjectSvc) Get(_ context.Context, _ project.Caller, slug string) (*project.Project, error) {
|
|
if p, ok := f.slugMap[slug]; ok {
|
|
return p, nil
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "project")
|
|
}
|
|
func (f *fakeProjectSvc) GetByID(_ context.Context, _ project.Caller, id uuid.UUID) (*project.Project, error) {
|
|
if p, ok := f.idMap[id]; ok {
|
|
return p, nil
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "project")
|
|
}
|
|
func (f *fakeProjectSvc) List(_ context.Context, _ project.Caller, _ bool) ([]*project.Project, error) {
|
|
return f.projects, nil
|
|
}
|
|
func (f *fakeProjectSvc) Update(ctx context.Context, c project.Caller, slug string, in project.UpdateProjectInput) (*project.Project, error) {
|
|
p, err := f.Get(ctx, c, slug)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Name != nil {
|
|
p.Name = *in.Name
|
|
}
|
|
if in.Description != nil {
|
|
p.Description = *in.Description
|
|
}
|
|
if in.Visibility != nil {
|
|
p.Visibility = *in.Visibility
|
|
}
|
|
return p, nil
|
|
}
|
|
func (f *fakeProjectSvc) Archive(ctx context.Context, c project.Caller, slug string) error {
|
|
p, err := f.Get(ctx, c, slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
now := time.Now()
|
|
p.ArchivedAt = &now
|
|
return nil
|
|
}
|
|
func (f *fakeProjectSvc) Unarchive(ctx context.Context, c project.Caller, slug string) error {
|
|
p, err := f.Get(ctx, c, slug)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
p.ArchivedAt = nil
|
|
return nil
|
|
}
|
|
|
|
// fakeRequirementSvc implements project.RequirementService.
|
|
type fakeRequirementSvc struct {
|
|
items []*project.Requirement
|
|
}
|
|
|
|
func (f *fakeRequirementSvc) Create(_ context.Context, _ project.Caller, _ string, in project.CreateRequirementInput) (*project.Requirement, error) {
|
|
r := &project.Requirement{
|
|
ID: uuid.New(), Number: len(f.items) + 1,
|
|
Title: in.Title, Description: in.Description,
|
|
Phase: project.PhasePlanning, Status: project.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
f.items = append(f.items, r)
|
|
return r, nil
|
|
}
|
|
func (f *fakeRequirementSvc) Get(_ context.Context, _ project.Caller, slug string, number int) (*project.Requirement, error) {
|
|
for _, r := range f.items {
|
|
if r.Number == number {
|
|
return r, nil
|
|
}
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "requirement")
|
|
}
|
|
func (f *fakeRequirementSvc) List(_ context.Context, _ project.Caller, _ string, _ project.RequirementFilter) ([]*project.Requirement, error) {
|
|
return f.items, nil
|
|
}
|
|
func (f *fakeRequirementSvc) Update(_ context.Context, _ project.Caller, slug string, num int, in project.UpdateRequirementInput) (*project.Requirement, error) {
|
|
r, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Title != nil {
|
|
r.Title = *in.Title
|
|
}
|
|
if in.Description != nil {
|
|
r.Description = *in.Description
|
|
}
|
|
return r, nil
|
|
}
|
|
func (f *fakeRequirementSvc) ChangePhase(_ context.Context, _ project.Caller, slug string, num int, to project.Phase) (*project.Requirement, error) {
|
|
r, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r.Phase = to
|
|
return r, nil
|
|
}
|
|
func (f *fakeRequirementSvc) Close(_ context.Context, _ project.Caller, slug string, num int) error {
|
|
r, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Status = project.StatusClosed
|
|
return nil
|
|
}
|
|
func (f *fakeRequirementSvc) Reopen(_ context.Context, _ project.Caller, slug string, num int) error {
|
|
r, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.Status = project.StatusOpen
|
|
return nil
|
|
}
|
|
|
|
// fakeIssueSvc implements project.IssueService.
|
|
type fakeIssueSvc struct {
|
|
items []*project.Issue
|
|
}
|
|
|
|
func (f *fakeIssueSvc) Create(_ context.Context, _ project.Caller, _ string, in project.CreateIssueInput) (*project.Issue, error) {
|
|
i := &project.Issue{
|
|
ID: uuid.New(), Number: len(f.items) + 1,
|
|
Title: in.Title, Description: in.Description,
|
|
Status: project.StatusOpen,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
f.items = append(f.items, i)
|
|
return i, nil
|
|
}
|
|
func (f *fakeIssueSvc) Get(_ context.Context, _ project.Caller, slug string, number int) (*project.Issue, error) {
|
|
for _, i := range f.items {
|
|
if i.Number == number {
|
|
return i, nil
|
|
}
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "issue")
|
|
}
|
|
func (f *fakeIssueSvc) List(_ context.Context, _ project.Caller, _ string, _ project.IssueFilter) ([]*project.Issue, error) {
|
|
return f.items, nil
|
|
}
|
|
func (f *fakeIssueSvc) Update(_ context.Context, _ project.Caller, slug string, num int, in project.UpdateIssueInput) (*project.Issue, error) {
|
|
i, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Title != nil {
|
|
i.Title = *in.Title
|
|
}
|
|
if in.Description != nil {
|
|
i.Description = *in.Description
|
|
}
|
|
return i, nil
|
|
}
|
|
func (f *fakeIssueSvc) Assign(_ context.Context, _ project.Caller, slug string, num int, assignee *uuid.UUID) (*project.Issue, error) {
|
|
i, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
i.AssigneeID = assignee
|
|
return i, nil
|
|
}
|
|
func (f *fakeIssueSvc) Close(_ context.Context, _ project.Caller, slug string, num int) error {
|
|
i, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i.Status = project.StatusClosed
|
|
return nil
|
|
}
|
|
func (f *fakeIssueSvc) Reopen(_ context.Context, _ project.Caller, slug string, num int) error {
|
|
i, err := f.Get(context.Background(), project.Caller{}, slug, num)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
i.Status = project.StatusOpen
|
|
return nil
|
|
}
|
|
|
|
// fakeWorkspaceSvc implements workspace.WorkspaceService.
|
|
type fakeWorkspaceSvc struct {
|
|
items []*workspace.Workspace
|
|
branches []string
|
|
}
|
|
|
|
func (f *fakeWorkspaceSvc) Create(_ context.Context, _ workspace.Caller, _ string, in workspace.CreateWorkspaceInput) (*workspace.Workspace, error) {
|
|
w := &workspace.Workspace{
|
|
ID: uuid.New(), ProjectID: testPID,
|
|
Slug: in.Slug, Name: in.Name, Description: in.Description,
|
|
GitRemoteURL: in.GitRemoteURL, DefaultBranch: in.DefaultBranch,
|
|
SyncStatus: workspace.SyncStatusIdle, CreatedAt: time.Now(),
|
|
}
|
|
f.items = append(f.items, w)
|
|
return w, nil
|
|
}
|
|
func (f *fakeWorkspaceSvc) Get(_ context.Context, _ workspace.Caller, id uuid.UUID) (*workspace.Workspace, error) {
|
|
for _, w := range f.items {
|
|
if w.ID == id {
|
|
return w, nil
|
|
}
|
|
}
|
|
return nil, errs.New(errs.CodeNotFound, "workspace")
|
|
}
|
|
func (f *fakeWorkspaceSvc) List(_ context.Context, _ workspace.Caller, _ string) ([]*workspace.Workspace, error) {
|
|
return f.items, nil
|
|
}
|
|
func (f *fakeWorkspaceSvc) Update(ctx context.Context, c workspace.Caller, id uuid.UUID, in workspace.UpdateWorkspaceInput) (*workspace.Workspace, error) {
|
|
w, err := f.Get(ctx, c, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if in.Name != nil {
|
|
w.Name = *in.Name
|
|
}
|
|
if in.Description != nil {
|
|
w.Description = *in.Description
|
|
}
|
|
if in.DefaultBranch != nil {
|
|
w.DefaultBranch = *in.DefaultBranch
|
|
}
|
|
return w, nil
|
|
}
|
|
func (f *fakeWorkspaceSvc) Delete(ctx context.Context, c workspace.Caller, id uuid.UUID) error {
|
|
if _, err := f.Get(ctx, c, id); err != nil {
|
|
return err
|
|
}
|
|
kept := f.items[:0]
|
|
for _, w := range f.items {
|
|
if w.ID != id {
|
|
kept = append(kept, w)
|
|
}
|
|
}
|
|
f.items = kept
|
|
return nil
|
|
}
|
|
func (f *fakeWorkspaceSvc) Sync(ctx context.Context, c workspace.Caller, id uuid.UUID) (*workspace.Workspace, error) {
|
|
return f.Get(ctx, c, id)
|
|
}
|
|
func (f *fakeWorkspaceSvc) UpsertCredential(_ context.Context, _ workspace.Caller, _ uuid.UUID, _ workspace.UpsertCredentialInput) (*workspace.Credential, error) {
|
|
panic("not implemented")
|
|
}
|
|
func (f *fakeWorkspaceSvc) GetCredentialMeta(_ context.Context, _ workspace.Caller, _ uuid.UUID) (*workspace.Credential, error) {
|
|
panic("not implemented")
|
|
}
|
|
func (f *fakeWorkspaceSvc) ListBranches(_ context.Context, _ workspace.Caller, _ uuid.UUID) ([]string, error) {
|
|
return f.branches, nil
|
|
}
|
|
|
|
// ===== test helpers =====
|
|
|
|
var (
|
|
testUID = uuid.MustParse("aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa")
|
|
testPID = uuid.MustParse("bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb")
|
|
testWSID = uuid.MustParse("cccccccc-cccc-cccc-cccc-cccccccccccc")
|
|
)
|
|
|
|
// testDeps assembles ServerDeps with fake services.
|
|
func testDeps() ServerDeps {
|
|
proj := &project.Project{
|
|
ID: testPID, Slug: "test-project", Name: "Test Project",
|
|
Visibility: project.VisibilityPrivate, OwnerID: testUID,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
uSvc := &fakeUserSvc{users: map[uuid.UUID]*user.User{
|
|
testUID: {ID: testUID, IsAdmin: true},
|
|
}}
|
|
return ServerDeps{
|
|
Caller: NewCallerResolver(uSvc),
|
|
Projects: newFakeProjectSvc(proj),
|
|
Reqs: &fakeRequirementSvc{},
|
|
Issues: &fakeIssueSvc{},
|
|
Workspaces: &fakeWorkspaceSvc{},
|
|
}
|
|
}
|
|
|
|
// callTool wires up an MCP server+client via in-memory transport,
|
|
// injects an AuthSession into the server's receiving middleware,
|
|
// and calls a tool by name.
|
|
// Each call creates a fresh server to avoid middleware accumulation.
|
|
func callTool(ctx context.Context, deps ServerDeps, toolName string, args map[string]any) (*mcpsdk.CallToolResult, error) {
|
|
// Extract AuthSession from caller's ctx and inject into server-side via middleware.
|
|
sess, _ := FromContext(ctx)
|
|
|
|
srv := NewMCPServer(deps)
|
|
srv.AddReceivingMiddleware(func(next mcpsdk.MethodHandler) mcpsdk.MethodHandler {
|
|
return func(ctx context.Context, method string, req mcpsdk.Request) (mcpsdk.Result, error) {
|
|
if sess != nil {
|
|
ctx = context.WithValue(ctx, AuthContextKey, sess)
|
|
}
|
|
return next(ctx, method, req)
|
|
}
|
|
})
|
|
|
|
client := mcpsdk.NewClient(&mcpsdk.Implementation{Name: "test-client", Version: "0.0.1"}, nil)
|
|
t1, t2 := mcpsdk.NewInMemoryTransports()
|
|
if _, err := srv.Connect(ctx, t1, nil); err != nil {
|
|
return nil, err
|
|
}
|
|
session, err := client.Connect(ctx, t2, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer session.Close()
|
|
|
|
return session.CallTool(ctx, &mcpsdk.CallToolParams{
|
|
Name: toolName,
|
|
Arguments: args,
|
|
})
|
|
}
|
|
|
|
// unmarshalStructured extracts the StructuredContent from a CallToolResult.
|
|
func unmarshalStructured(t *testing.T, result *mcpsdk.CallToolResult, out any) {
|
|
t.Helper()
|
|
data, err := json.Marshal(result.StructuredContent)
|
|
require.NoError(t, err)
|
|
require.NoError(t, json.Unmarshal(data, out))
|
|
}
|
|
|
|
// ===== tests =====
|
|
|
|
func TestListProjects(t *testing.T) {
|
|
deps := testDeps()
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_projects", map[string]any{})
|
|
require.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
|
|
var out listProjectsOut
|
|
unmarshalStructured(t, result, &out)
|
|
require.Len(t, out.Projects, 1)
|
|
assert.Equal(t, "test-project", out.Projects[0].Slug)
|
|
}
|
|
|
|
func TestListWorkspaces(t *testing.T) {
|
|
ws := &workspace.Workspace{
|
|
ID: testWSID, Slug: "ws-1", Name: "Workspace 1",
|
|
DefaultBranch: "main", SyncStatus: workspace.SyncStatusIdle,
|
|
}
|
|
deps := testDeps()
|
|
deps.Workspaces = &fakeWorkspaceSvc{items: []*workspace.Workspace{ws}}
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_workspaces", map[string]any{
|
|
"project_slug": "test-project",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
|
|
var out listWorkspacesOut
|
|
unmarshalStructured(t, result, &out)
|
|
require.Len(t, out.Workspaces, 1)
|
|
assert.Equal(t, "ws-1", out.Workspaces[0].Slug)
|
|
}
|
|
|
|
func TestCreateAndGetIssue(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project",
|
|
"title": "Bug report",
|
|
"description": "Something broke",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
|
|
var createOut issueCreateOut
|
|
unmarshalStructured(t, result, &createOut)
|
|
assert.True(t, createOut.OK)
|
|
assert.Equal(t, "Bug report", createOut.Issue.Title)
|
|
|
|
// Get
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "get_issue", map[string]any{
|
|
"project_slug": "test-project",
|
|
"number": float64(1),
|
|
})
|
|
require.NoError(t, err)
|
|
var getOut issueDetail
|
|
unmarshalStructured(t, result, &getOut)
|
|
assert.Equal(t, "Bug report", getOut.Title)
|
|
}
|
|
|
|
func TestUpdateIssue(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project", "title": "Original",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "update_issue", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1), "title": "Updated",
|
|
})
|
|
require.NoError(t, err)
|
|
var out issueUpdateOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.True(t, out.OK)
|
|
assert.Equal(t, "Updated", out.Issue.Title)
|
|
}
|
|
|
|
func TestCloseReopenIssue(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project", "title": "To close",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "close_issue", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1),
|
|
})
|
|
require.NoError(t, err)
|
|
var out okOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.True(t, out.OK)
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "reopen_issue", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1),
|
|
})
|
|
require.NoError(t, err)
|
|
unmarshalStructured(t, result, &out)
|
|
assert.True(t, out.OK)
|
|
}
|
|
|
|
func TestAssignIssue(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project", "title": "Assign me",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
assigneeID := uuid.New().String()
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "assign_issue", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1), "assignee_id": assigneeID,
|
|
})
|
|
require.NoError(t, err)
|
|
var out okOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.True(t, out.OK)
|
|
}
|
|
|
|
func TestCreateAndGetRequirement(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
|
|
"project_slug": "test-project", "title": "Auth module", "description": "Need auth",
|
|
})
|
|
require.NoError(t, err)
|
|
var createOut requirementCreateOut
|
|
unmarshalStructured(t, result, &createOut)
|
|
assert.True(t, createOut.OK)
|
|
assert.Equal(t, "Auth module", createOut.Requirement.Title)
|
|
assert.Equal(t, "planning", createOut.Requirement.Phase)
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "get_requirement", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1),
|
|
})
|
|
require.NoError(t, err)
|
|
var getOut requirementDetail
|
|
unmarshalStructured(t, result, &getOut)
|
|
assert.Equal(t, "Auth module", getOut.Title)
|
|
}
|
|
|
|
func TestUpdateRequirement(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
|
|
"project_slug": "test-project", "title": "V1",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "update_requirement", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1), "title": "V2",
|
|
})
|
|
require.NoError(t, err)
|
|
var out requirementCreateOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.Equal(t, "V2", out.Requirement.Title)
|
|
}
|
|
|
|
func TestCloseReopenRequirement(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
|
|
"project_slug": "test-project", "title": "Close me",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "close_requirement", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1),
|
|
})
|
|
require.NoError(t, err)
|
|
var out okOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.True(t, out.OK)
|
|
|
|
result, err = callTool(ctxWithAuth(Scope{}), deps, "reopen_requirement", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1),
|
|
})
|
|
require.NoError(t, err)
|
|
unmarshalStructured(t, result, &out)
|
|
assert.True(t, out.OK)
|
|
}
|
|
|
|
func TestSetRequirementPhase(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, err := callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
|
|
"project_slug": "test-project", "title": "Phase test",
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "set_requirement_phase", map[string]any{
|
|
"project_slug": "test-project", "number": float64(1), "phase": "auditing",
|
|
})
|
|
require.NoError(t, err)
|
|
var out requirementCreateOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.Equal(t, "auditing", out.Requirement.Phase)
|
|
}
|
|
|
|
func TestListRequirements(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
|
|
"project_slug": "test-project", "title": "R1",
|
|
})
|
|
_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_requirement", map[string]any{
|
|
"project_slug": "test-project", "title": "R2",
|
|
})
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_requirements", map[string]any{
|
|
"project_slug": "test-project",
|
|
})
|
|
require.NoError(t, err)
|
|
var out listRequirementsOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.Len(t, out.Requirements, 2)
|
|
}
|
|
|
|
func TestListIssues(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project", "title": "I1",
|
|
})
|
|
_, _ = callTool(ctxWithAuth(Scope{}), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project", "title": "I2",
|
|
})
|
|
|
|
result, err := callTool(ctxWithAuth(Scope{}), deps, "list_issues", map[string]any{
|
|
"project_slug": "test-project",
|
|
})
|
|
require.NoError(t, err)
|
|
var out listIssuesOut
|
|
unmarshalStructured(t, result, &out)
|
|
assert.Len(t, out.Issues, 2)
|
|
}
|
|
|
|
func TestToolScopeCheck(t *testing.T) {
|
|
deps := testDeps()
|
|
|
|
// Restricted scope: only list_projects
|
|
restricted := Scope{Tools: []string{"list_projects"}}
|
|
result, err := callTool(ctxWithAuth(restricted), deps, "list_projects", map[string]any{})
|
|
require.NoError(t, err)
|
|
assert.False(t, result.IsError)
|
|
|
|
result, err = callTool(ctxWithAuth(restricted), deps, "create_issue", map[string]any{
|
|
"project_slug": "test-project", "title": "Blocked",
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, result.IsError)
|
|
}
|
|
|
|
// ctxWithAuth returns a context with an AuthSession for the test admin user.
|
|
func ctxWithAuth(scope Scope) context.Context {
|
|
return context.WithValue(context.Background(), AuthContextKey, &AuthSession{
|
|
UserID: testUID.String(),
|
|
TokenID: uuid.NewString(),
|
|
Scope: scope,
|
|
})
|
|
}
|