Non-author triggers were denied by dutyAuthorizer because AutoCommitUserID was the triggering user. Record that user on OnBehalfOf and surface the duty mismatch in the task error.
179 lines
6.3 KiB
Go
179 lines
6.3 KiB
Go
package bizdev
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/app/docflow"
|
|
"chat-agent-system/internal/app/uow"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/docgov"
|
|
"chat-agent-system/internal/docrevision"
|
|
)
|
|
|
|
type recordingMaterializer struct {
|
|
inner AgentOutputMaterializer
|
|
got docrevision.AgentMaterializeRequest
|
|
err error
|
|
}
|
|
|
|
func (r *recordingMaterializer) MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) {
|
|
r.got = req
|
|
if r.err != nil {
|
|
return nil, r.err
|
|
}
|
|
if r.inner == nil {
|
|
return nil, errors.New("recording materializer has no inner")
|
|
}
|
|
return r.inner.MaterializeAgentOutput(ctx, req)
|
|
}
|
|
|
|
func TestSquadCommitActor_UsesAuthorWhenTriggerDiffers(t *testing.T) {
|
|
author, behalf := squadCommitActor(74, &docgov.DocumentRecord{AuthorID: 4})
|
|
if author != 4 {
|
|
t.Fatalf("commit actor want 4 got %d", author)
|
|
}
|
|
if behalf == nil || *behalf != 74 {
|
|
t.Fatalf("on behalf want 74 got %v", behalf)
|
|
}
|
|
same, behalfSame := squadCommitActor(4, &docgov.DocumentRecord{AuthorID: 4})
|
|
if same != 4 || behalfSame != nil {
|
|
t.Fatalf("author trigger must not proxy: actor=%d behalf=%v", same, behalfSame)
|
|
}
|
|
missing, behalfMissing := squadCommitActor(74, &docgov.DocumentRecord{})
|
|
if missing != 74 || behalfMissing != nil {
|
|
t.Fatalf("unset author must keep trigger: actor=%d behalf=%v", missing, behalfMissing)
|
|
}
|
|
}
|
|
|
|
func TestExecuteTask_NonAuthorTriggerMaterializesAsAuthor(t *testing.T) {
|
|
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
|
ctx := context.Background()
|
|
const (
|
|
projectID = int64(6022)
|
|
authorID = int64(4)
|
|
triggerID = int64(74)
|
|
docID = "doc_proj6022_bd_intake"
|
|
)
|
|
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", authorID, "admin"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := &recordingMaterializer{inner: revMaterializer{rev: svc.docRev}}
|
|
svc.SetAgentMaterializer(rec)
|
|
|
|
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
|
TaskKey: "intake", ProjectID: projectID, ProjectName: "测试项目",
|
|
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
|
|
Timeout: 10 * time.Second, DocID: docID, UserID: triggerID,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ExecuteTask: %v reason=%v", err, res)
|
|
}
|
|
if res == nil || !res.Success {
|
|
t.Fatalf("non-author trigger must succeed, got %#v", res)
|
|
}
|
|
if rec.got.AutoCommitUserID != authorID {
|
|
t.Fatalf("AutoCommitUserID want %d got %d", authorID, rec.got.AutoCommitUserID)
|
|
}
|
|
if rec.got.OnBehalfOf == nil || *rec.got.OnBehalfOf != triggerID {
|
|
t.Fatalf("OnBehalfOf want %d got %v", triggerID, rec.got.OnBehalfOf)
|
|
}
|
|
}
|
|
|
|
func TestExecuteTask_AuthorTriggerDoesNotSetOnBehalfOf(t *testing.T) {
|
|
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
|
ctx := context.Background()
|
|
const (
|
|
projectID = int64(6023)
|
|
authorID = int64(4)
|
|
docID = "doc_proj6023_bd_intake"
|
|
)
|
|
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", 0, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
rec := &recordingMaterializer{inner: revMaterializer{rev: svc.docRev}}
|
|
svc.SetAgentMaterializer(rec)
|
|
|
|
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
|
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
|
|
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
|
|
Timeout: 10 * time.Second, DocID: docID, UserID: authorID,
|
|
})
|
|
if err != nil || res == nil || !res.Success {
|
|
t.Fatalf("author trigger failed: err=%v res=%#v", err, res)
|
|
}
|
|
if rec.got.AutoCommitUserID != authorID || rec.got.OnBehalfOf != nil {
|
|
t.Fatalf("author path changed: user=%d behalf=%v", rec.got.AutoCommitUserID, rec.got.OnBehalfOf)
|
|
}
|
|
}
|
|
|
|
func TestExecuteTask_NonAuthorPassesDutyAuthorizer(t *testing.T) {
|
|
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
|
ctx := context.Background()
|
|
const (
|
|
projectID = int64(6024)
|
|
authorID = int64(4)
|
|
triggerID = int64(74)
|
|
docID = "doc_proj6024_bd_intake"
|
|
)
|
|
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, projectID, "bd_intake", "用户初始需求收集表", "V0.1_triage", "", authorID, "admin", 0, "", 0, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc.SetAgentMaterializer(&docflow.Commands{Gov: svc.docGov, Rev: svc.docRev, Auth: docflow.DutyAuthorizer()})
|
|
|
|
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
|
TaskKey: "intake", ProjectID: projectID, ProjectName: "P",
|
|
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
|
|
Timeout: 10 * time.Second, DocID: docID, UserID: triggerID,
|
|
})
|
|
if err != nil || res == nil || !res.Success {
|
|
t.Fatalf("duty authorizer must accept author-proxy commit: err=%v reason=%v", err, errorReason(res))
|
|
}
|
|
if strings.Contains(errorReason(res), "permission denied") {
|
|
t.Fatalf("permission denied leaked: %s", res.ErrorReason)
|
|
}
|
|
}
|
|
|
|
func TestExecuteTask_PermissionDeniedNamesNonAuthor(t *testing.T) {
|
|
svc := MustNewService(t, &config.Config{DataDir: t.TempDir(), AgentBackend: "stub"})
|
|
ctx := context.Background()
|
|
const docID = "doc_perm_denied"
|
|
if _, err := svc.docGov.CreateOrGetDocument(ctx, docID, 77, "research", "调研", "V0.5", "", 4, "admin", 0, "", 0, ""); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
svc.SetAgentMaterializer(&recordingMaterializer{err: uow.ErrPermissionDenied})
|
|
|
|
res, err := svc.ExecuteTask(ctx, TaskRunRequest{
|
|
TaskKey: "research", ProjectID: 77, ProjectName: "P",
|
|
CustomerName: "C", DeviceType: "医疗器械", Backend: "stub",
|
|
Timeout: 10 * time.Second, DocID: docID, UserID: 74,
|
|
})
|
|
if err == nil || res == nil || res.Success {
|
|
t.Fatalf("want visible permission failure, err=%v res=%#v", err, res)
|
|
}
|
|
if !strings.Contains(res.ErrorReason, "触发用户非文档编制人") {
|
|
t.Fatalf("reason must name non-author denial, got %q", res.ErrorReason)
|
|
}
|
|
if !errors.Is(err, uow.ErrPermissionDenied) {
|
|
t.Fatalf("sentinel must stay errors.Is compatible, got %v", err)
|
|
}
|
|
}
|
|
|
|
type revMaterializer struct {
|
|
rev *docrevision.Service
|
|
}
|
|
|
|
func (r revMaterializer) MaterializeAgentOutput(ctx context.Context, req docrevision.AgentMaterializeRequest) (*docrevision.AgentMaterializeResult, error) {
|
|
return r.rev.MaterializeAgentOutput(ctx, req)
|
|
}
|
|
|
|
func errorReason(res *TaskRunResult) string {
|
|
if res == nil {
|
|
return ""
|
|
}
|
|
return res.ErrorReason
|
|
}
|