package main import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "testing" "time" "git.cin.su/m/Assistant/contracts/authority" commitcontract "git.cin.su/m/Assistant/contracts/commit" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/work" "git.cin.su/m/Assistant/platform/ids" ) type fakeCreatorCore struct { session identity.CreatorSession target work.TaskTarget binding identity.PrincipalBinding sessionErr error } func (f fakeCreatorCore) Session(context.Context, string) (identity.CreatorSession, error) { return f.session, f.sessionErr } func (f fakeCreatorCore) Task(context.Context, work.TaskID) (work.TaskTarget, error) { return f.target, nil } func (f fakeCreatorCore) Binding(context.Context, identity.ActorID, identity.PrincipalID) (identity.PrincipalBinding, error) { return f.binding, nil } type fakeCreatorContinuity struct { item continuity.ProspectiveItem err error } func (f fakeCreatorContinuity) Item(context.Context, string, continuity.ProspectiveItemID) (continuity.ProspectiveItem, error) { return f.item, f.err } type fakeCreatorAuthority struct { grant authority.AuthorityGrant scope authority.Scope } func (f fakeCreatorAuthority) LoadGrant(context.Context, authority.GrantID) (authority.AuthorityGrant, authority.Scope, error) { return f.grant, f.scope, nil } func TestCreatorCommandDerivesAuthorityAndActionFieldsServerSide(t *testing.T) { handler, repository, executor := validCreatorCommandHandler() mux := http.NewServeMux() registerCreatorCommandRoutes(mux, handler) request := httptest.NewRequest(http.MethodPost, "/v1/creator/continuity/satisfy-delivered", strings.NewReader(`{ "Target":{"TaskID":"task-1","GoalID":"goal-1","ScopeID":"scope-1","Revision":3}, "ProspectiveItemID":"prospective-1","ExpectedRevision":4,"EvidenceRef":" artifact://evidence/1 " }`)) request.Header.Set("Authorization", "Bearer creator-session") response := httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != http.StatusCreated { t.Fatalf("status=%d body=%s", response.Code, response.Body.String()) } var result creatorActionResponse if err := json.NewDecoder(response.Body).Decode(&result); err != nil { t.Fatal(err) } if result.State != commitcontract.ActionSucceeded || executor.calls != 1 { t.Fatalf("result=%#v calls=%d", result, executor.calls) } action := repository.action if action.Proposal.ActorID != "creator-actor" || action.PrincipalID != "creator-principal" || action.GrantID != creatorGrantID || action.Proposal.CapabilityID != commitcontract.CapabilityContinuityTransition || action.Proposal.Operation != commitcontract.OperationSatisfyDelivered || action.Proposal.ContinuitySubject.FromState != continuity.ProspectiveDelivered || action.Proposal.ContinuitySubject.ToState != continuity.ProspectiveSatisfied || action.Proposal.ContinuitySubject.EvidenceRef != "artifact://evidence/1" { t.Fatalf("server-derived envelope is invalid: %#v", action) } } func TestCreatorCommandRejectsSessionForeignOwnerStaleStateAndRevokedGrant(t *testing.T) { tests := []struct { name string mutate func(*creatorCommandHandler) status int }{ {name: "session", mutate: func(h *creatorCommandHandler) { core := h.core.(fakeCreatorCore) core.sessionErr = errCreatorSessionDenied h.core = core }, status: http.StatusUnauthorized}, {name: "foreign owner", mutate: func(h *creatorCommandHandler) { source := h.continuity.(fakeCreatorContinuity) source.item.OwnerPrincipalID = "foreign" h.continuity = source }, status: http.StatusForbidden}, {name: "stale state", mutate: func(h *creatorCommandHandler) { source := h.continuity.(fakeCreatorContinuity) source.item.State = continuity.ProspectiveArmed h.continuity = source }, status: http.StatusConflict}, {name: "revoked grant", mutate: func(h *creatorCommandHandler) { source := h.authority.(fakeCreatorAuthority) source.grant.Active = false h.authority = source }, status: http.StatusConflict}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { handler, repository, executor := validCreatorCommandHandler() test.mutate(&handler) mux := http.NewServeMux() registerCreatorCommandRoutes(mux, handler) request := httptest.NewRequest(http.MethodPost, "/v1/creator/continuity/satisfy-delivered", strings.NewReader(`{"Target":{"TaskID":"task-1","GoalID":"goal-1","ScopeID":"scope-1","Revision":3},"ProspectiveItemID":"prospective-1","ExpectedRevision":4,"EvidenceRef":"evidence"}`)) request.Header.Set("Authorization", "Bearer creator-session") response := httptest.NewRecorder() mux.ServeHTTP(response, request) if response.Code != test.status || executor.calls != 0 || repository.action.ID != "" { t.Fatalf("status=%d calls=%d action=%#v", response.Code, executor.calls, repository.action) } }) } } func validCreatorCommandHandler() (creatorCommandHandler, *fakeExecutionRepository, *knownExecutor) { session := identity.CreatorSession{ActorID: "creator-actor", PrincipalID: "creator-principal", ExpiresAtUnix: 1000} target := work.TaskTarget{TaskID: "task-1", GoalID: "goal-1", ScopeID: "scope-1", Revision: 3} core := fakeCreatorCore{session: session, target: target, binding: identity.PrincipalBinding{ActorID: session.ActorID, PrincipalID: session.PrincipalID, Active: true}} item := continuity.ProspectiveItem{ID: "prospective-1", OwnerPrincipalID: session.PrincipalID, ScopeID: target.ScopeID, GoalID: target.GoalID, State: continuity.ProspectiveDelivered, Revision: 4} grant := authority.AuthorityGrant{ID: creatorGrantID, ActorID: session.ActorID, PrincipalID: session.PrincipalID, ScopeID: target.ScopeID, Class: authority.GrantClassCreator, Revision: 2, Active: true} repository := &fakeExecutionRepository{} executor := &knownExecutor{} identities := []ids.ID{"intent-generated", "authorized-generated", "attempt-generated"} return creatorCommandHandler{ core: core, continuity: fakeCreatorContinuity{item: item}, authority: fakeCreatorAuthority{grant: grant, scope: authority.Scope{ID: target.ScopeID, Global: true}}, repository: repository, executor: executor, reconciler: executor, newID: func() (ids.ID, error) { if len(identities) == 0 { return "", errors.New("identity exhaustion") } value := identities[0] identities = identities[1:] return value, nil }, now: func() time.Time { return time.UnixMilli(10) }, }, repository, executor }