Scert/backend/internal/repository/history_scope_test.go
xuans 0ccdb14b26 完善任务审计范围快照与身份关联并同步整改进度
新增三库第13号迁移和历史范围回填,保留已删除资源的任务与审计。补充仓储、调用链、身份来源及迁移失败重试回归,同步任务清单、支持矩阵和运维边界。项目成员授权与生产验收仍待完成。
2026-09-28 11:48:27 +08:00

247 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package repository
import (
"context"
"path/filepath"
"testing"
"time"
"git.pcux.cn/xuans/Scert/backend/internal/config"
"git.pcux.cn/xuans/Scert/backend/internal/database"
"git.pcux.cn/xuans/Scert/backend/internal/domain"
"git.pcux.cn/xuans/Scert/backend/internal/testsupport/mysqltest"
)
func TestHistoryScopeSQLite(t *testing.T) {
cfg := config.Default(t.TempDir()).Database
cfg.DSN = filepath.Join(t.TempDir(), "history.db")
checkHistoryScope(t, cfg)
}
func TestHistoryScopeMySQLReal(t *testing.T) {
server := mysqltest.New(t)
checkHistoryScope(t, server.Database(t))
}
func checkHistoryScope(t *testing.T, cfg config.DatabaseConfig) {
t.Helper()
ctx := t.Context()
db, err := database.Open(ctx, cfg)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
if err := database.Migrate(ctx, db.DB, cfg.Driver); err != nil {
t.Fatal(err)
}
projects := NewSQLProjectRepository(db.DB, cfg.Driver)
domains := NewSQLDomainRepository(db.DB, cfg.Driver)
tasks := NewSQLTaskRepository(db.DB, cfg.Driver)
audit := NewSQLAuditRepository(db.DB, cfg.Driver)
for _, id := range []string{"history-a", "history-b"} {
if err := projects.CreateProject(ctx, domain.Project{ID: id, Name: id}); err != nil {
t.Fatal(err)
}
}
resource, err := domain.NewDomain("history.example.test", domain.CertificateSingle, nil)
if err != nil {
t.Fatal(err)
}
resource.ProjectID = "history-a"
if err := domains.Create(ctx, resource); err != nil {
t.Fatal(err)
}
expected := HistoryScope{ScopeType: ScopeProject, ProjectID: "history-a"}
task := Task{ID: "history-task", DomainID: resource.ID, TaskType: "certificate_issue", Status: "running", Stage: "issue", OperatorID: "actor", IdempotencyKey: "history-key"}
if err := tasks.CreateTask(ctx, task); err != nil {
t.Fatal(err)
}
for name, read := range map[string]func() (Task, error){
"get": func() (Task, error) { return tasks.GetTask(ctx, task.ID) },
"active": func() (Task, error) { return tasks.FindActiveTaskByDomain(ctx, resource.ID, task.TaskType) },
"idempotency": func() (Task, error) { return tasks.FindTaskByIdempotencyKey(ctx, resource.ID, task.IdempotencyKey) },
} {
t.Run(name, func(t *testing.T) {
stored, err := read()
if err != nil || stored.HistoryScope != expected || stored.OperatorID != "actor" {
t.Fatalf("任务范围读取错误:%+v,%v", stored, err)
}
})
}
for _, invalid := range []Task{
{ID: "missing", DomainID: "missing", TaskType: "certificate_issue"},
{ID: "forged-project", DomainID: resource.ID, TaskType: "certificate_issue", HistoryScope: HistoryScope{ProjectID: "history-b"}},
{ID: "forged-system", DomainID: resource.ID, TaskType: "certificate_issue", HistoryScope: HistoryScope{ScopeType: ScopeSystem}},
{ID: "forged-unknown", DomainID: resource.ID, TaskType: "certificate_issue", HistoryScope: HistoryScope{ScopeType: ScopeUnknown}},
{ID: "false-management", DomainID: resource.ID, TaskType: "management_renewal"},
{ID: "false-resource", DomainID: ManagementResourceID, TaskType: "certificate_issue"},
{ID: "system-project", DomainID: ManagementResourceID, TaskType: "management_renewal", HistoryScope: expected},
} {
t.Run(invalid.ID, func(t *testing.T) {
invalid.Status, invalid.Stage = "queued", "queued"
if err := tasks.CreateTask(ctx, invalid); err == nil {
t.Fatal("无效任务范围未拒绝")
}
})
}
attempt := TaskAttempt{ID: "history-attempt", TaskID: task.ID, AttemptNumber: 1, Stage: "issue", Status: "running"}
if err := tasks.CreateAttempt(ctx, attempt); err != nil {
t.Fatal(err)
}
for _, invalid := range []TaskAttempt{
{ID: "missing-parent", TaskID: "missing"},
{ID: "wrong-project", TaskID: task.ID, HistoryScope: HistoryScope{ProjectID: "history-b"}},
{ID: "wrong-system", TaskID: task.ID, HistoryScope: HistoryScope{ScopeType: ScopeSystem}},
} {
t.Run(invalid.ID, func(t *testing.T) {
invalid.AttemptNumber, invalid.Stage, invalid.Status = 2, "issue", "running"
if err := tasks.CreateAttempt(ctx, invalid); err == nil {
t.Fatal("无效任务尝试关联未拒绝")
}
})
}
management := Task{ID: "management", DomainID: ManagementResourceID, TaskType: "management_renewal", Status: "running", Stage: "issue"}
if err := tasks.CreateTask(ctx, management); err != nil {
t.Fatal(err)
}
if stored, err := tasks.GetTask(ctx, management.ID); err != nil || stored.HistoryScope != (HistoryScope{ScopeType: ScopeSystem}) {
t.Fatalf("系统任务范围错误:%+v,%v", stored, err)
}
if err := tasks.CreateAttempt(ctx, TaskAttempt{ID: "management-attempt", TaskID: management.ID, AttemptNumber: 1, Stage: "issue", Status: "running"}); err != nil {
t.Fatal(err)
}
if attempts, err := tasks.ListAttempts(ctx, management.ID); err != nil || len(attempts) != 1 || attempts[0].HistoryScope != (HistoryScope{ScopeType: ScopeSystem}) {
t.Fatalf("系统尝试范围错误:%+v,%v", attempts, err)
}
for _, entry := range []AuditLog{
{Action: "direct", ResourceType: "domain", ResourceID: resource.ID},
{Action: "linked", ResourceType: "domain", ResourceID: resource.ID, TaskID: task.ID, OperatorID: "actor"},
{Action: "task", ResourceType: "task", ResourceID: task.ID},
{Action: "system", ResourceType: "management_certificate", ResourceID: ManagementResourceID, TaskID: management.ID},
{Action: "auth", ResourceType: "auth", ResourceID: "login"},
{Action: "missing", ResourceType: "domain", ResourceID: "deleted"},
} {
entry.Status = "success"
if err := audit.CreateAuditLog(ctx, entry); err != nil {
t.Fatal(entry.Action, err)
}
}
for _, entry := range []AuditLog{
{Action: "forged-project", ResourceType: "domain", ResourceID: resource.ID, HistoryScope: HistoryScope{ProjectID: "history-b"}},
{Action: "wrong-resource", ResourceType: "domain", ResourceID: "different", TaskID: task.ID},
{Action: "missing-task", ResourceType: "domain", ResourceID: resource.ID, TaskID: "missing"},
{Action: "wrong-system", ResourceType: "domain", ResourceID: resource.ID, TaskID: management.ID},
{Action: "wrong-task", ResourceType: "task", ResourceID: task.ID, TaskID: management.ID},
{Action: "false-system", ResourceType: "management_certificate", ResourceID: ManagementResourceID, TaskID: task.ID},
} {
t.Run(entry.Action, func(t *testing.T) {
entry.Status = "success"
if err := audit.CreateAuditLog(ctx, entry); err == nil {
t.Fatal("无效审计关联未拒绝")
}
})
}
finished := time.Now().UTC()
if err := tasks.CancelTask(ctx, task.ID, "测试取消", &finished); err != nil {
t.Fatal(err)
}
if err := domains.Delete(ctx, resource.ID); err != nil {
t.Fatal(err)
}
if err := projects.DeleteProject(ctx, "history-a"); err != nil {
t.Fatal(err)
}
// 资源和项目删除不删除任务,也不能让收尾审计丢失原项目。
if err := audit.CreateAuditLog(ctx, AuditLog{Action: "after-delete", ResourceType: "domain", ResourceID: resource.ID, TaskID: task.ID, Status: "cancelled"}); err != nil {
t.Fatal(err)
}
items, err := tasks.ListTasks(ctx, resource.ID, 10)
if err != nil || len(items) != 1 || items[0].HistoryScope != expected || items[0].Status != "cancelled" {
t.Fatalf("历史任务丢失:%+v,%v", items, err)
}
attempts, err := tasks.ListAttempts(ctx, task.ID)
if err != nil || len(attempts) != 1 || attempts[0].HistoryScope != expected || attempts[0].Status != "cancelled" {
t.Fatalf("尝试未继承历史范围:%+v,%v", attempts, err)
}
logs, err := audit.ListAuditLogs(ctx, 50)
if err != nil || len(logs) != 7 {
t.Fatalf("审计数量错误:%d,%v", len(logs), err)
}
for _, entry := range logs {
want := expected
if entry.Action == "system" || entry.Action == "auth" {
want = HistoryScope{ScopeType: ScopeSystem}
}
if entry.Action == "missing" {
want = HistoryScope{ScopeType: ScopeUnknown}
}
if entry.HistoryScope != want {
t.Fatalf("审计快照错误:%+v", entry)
}
if entry.Action == "linked" && (entry.TaskID != task.ID || entry.OperatorID != "actor") {
t.Fatalf("审计任务或操作者丢失:%+v", entry)
}
}
cancelled, cancel := context.WithCancel(ctx)
cancel()
if err := tasks.CreateTask(cancelled, management); err == nil {
t.Fatal("已取消上下文未生效")
}
checkHistoryAttemptBoundaries(t, domains, tasks)
}
func checkHistoryAttemptBoundaries(t *testing.T, domains *SQLDomainRepository, tasks *SQLTaskRepository) {
t.Helper()
for _, name := range []string{"deleted-resource", "changed-project", "unknown-parent", "orphan-attempt"} {
t.Run(name, func(t *testing.T) {
ctx := t.Context()
resource, err := domain.NewDomain(name+".example.test", domain.CertificateSingle, nil)
if err != nil {
t.Fatal(err)
}
resource.ProjectID = "history-b"
if err := domains.Create(ctx, resource); err != nil {
t.Fatal(err)
}
task := Task{ID: name, DomainID: resource.ID, TaskType: "certificate_issue", Status: "running", Stage: "issue"}
if err := tasks.CreateTask(ctx, task); err != nil {
t.Fatal(err)
}
attempt := TaskAttempt{ID: name, TaskID: task.ID, AttemptNumber: 1, Status: "running", Stage: "issue"}
if err := tasks.CreateAttempt(ctx, attempt); err != nil {
t.Fatal(err)
}
switch name {
case "deleted-resource":
err = domains.Delete(ctx, resource.ID)
case "changed-project":
_, err = tasks.db.ExecContext(ctx, tasks.query("UPDATE domains SET project_id = ? WHERE id = ?"), "default", resource.ID)
case "unknown-parent":
// 模拟保留下来的未知历史任务,不通过新建接口伪造范围。
_, err = tasks.db.ExecContext(ctx, tasks.query("UPDATE tasks SET scope_type = 'unknown', project_id = '' WHERE id = ?"), task.ID)
case "orphan-attempt":
_, err = tasks.db.ExecContext(ctx, tasks.query("DELETE FROM tasks WHERE id = ?"), task.ID)
}
if err != nil {
t.Fatal(err)
}
attempt.ID += "-retry"
attempt.AttemptNumber = 2
if err := tasks.CreateAttempt(ctx, attempt); err == nil {
t.Fatal("资源或历史范围失效后仍允许新增尝试")
}
stored, err := tasks.ListAttempts(ctx, task.ID)
if err != nil || len(stored) != 1 || stored[0].ID != name {
t.Fatalf("历史尝试丢失或新增拒绝后残留:%+v,%v", stored, err)
}
want := HistoryScope{ScopeType: ScopeProject, ProjectID: "history-b"}
if name == "unknown-parent" || name == "orphan-attempt" {
want = HistoryScope{ScopeType: ScopeUnknown}
}
if stored[0].HistoryScope != want {
t.Fatalf("历史尝试范围被当前资源覆盖:%+v", stored[0])
}
})
}
}