Files
Assistant/cmd/commit-runtime/creator_command_test.go
2026-08-16 16:24:02 +03:00

153 lines
6.4 KiB
Go

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
}