124 lines
5.9 KiB
Go
124 lines
5.9 KiB
Go
package repository
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.pcux.cn/xuans/Scert/backend/internal/deployment"
|
|
)
|
|
|
|
func completionTestReport() *deployment.Report {
|
|
return &deployment.Report{
|
|
Total: 2, Success: 1, Failed: 1, Status: "failed", VerificationStatus: "failed", RecoveryStatus: "unconfirmed",
|
|
Results: []deployment.Result{
|
|
{ServerID: "server-1", Server: "第一台", Status: "success", BackupID: "backup-1"},
|
|
{ServerID: "server-2", Server: "第二台", Status: "failed", FailureCode: deployment.FailureRecoveryUnknown, Message: "secret-output-marker"},
|
|
},
|
|
}
|
|
}
|
|
|
|
func TestTaskCompletionReportPersistsAcrossRepositoryRestart(t *testing.T) {
|
|
repo, db := newTaskRepositoryTest(t)
|
|
defer db.Close()
|
|
completion := seedTaskCompletion(t, repo)
|
|
completion.Status = "failed"
|
|
completion.Report = completionTestReport()
|
|
completion.AuditAction = "certificate_deploy"
|
|
if err := repo.FinishTaskAttempt(t.Context(), completion); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reader := NewSQLTaskRepository(db)
|
|
var auditStatus, scope, projectID, resourceID string
|
|
if err := db.QueryRowContext(t.Context(), `SELECT status, scope_type, project_id, resource_id FROM audit_logs WHERE task_id = ?`,
|
|
completion.TaskID).Scan(&auditStatus, &scope, &projectID, &resourceID); err != nil || auditStatus != "error" || scope != "project" || projectID != "default" || resourceID != "domain-1" {
|
|
t.Fatalf("完成审计与任务状态或归属不一致:%s %s %s %s %v", auditStatus, scope, projectID, resourceID, err)
|
|
}
|
|
reports, err := reader.ListTaskDeploymentReports(t.Context(), completion.TaskID)
|
|
if err != nil || len(reports) != 1 {
|
|
t.Fatalf("完整报告未保存:%+v %v", reports, err)
|
|
}
|
|
stored := reports[0]
|
|
if stored.AttemptNumber != 1 || stored.AttemptID != completion.AttemptID || stored.Report.VerificationStatus != "failed" || stored.Report.RecoveryStatus != "unconfirmed" || len(stored.Report.Results) != 2 {
|
|
t.Fatalf("重读后验证或恢复证据丢失:%+v", stored)
|
|
}
|
|
if stored.Report.Results[1].FailureCode != deployment.FailureRecoveryUnknown || strings.Contains(stored.Report.Results[1].Message, "secret-output-marker") {
|
|
t.Fatal("报告未保留固定诊断或泄露远端输出")
|
|
}
|
|
var raw string
|
|
if err := db.QueryRowContext(t.Context(), `SELECT report_json FROM task_deployment_reports WHERE task_id = ?`, completion.TaskID).Scan(&raw); err != nil || strings.Contains(raw, "secret-output-marker") {
|
|
t.Fatalf("持久化前未脱敏:%v", err)
|
|
}
|
|
results, err := reader.ListTaskDeploymentResults(t.Context(), completion.TaskID)
|
|
if err != nil || len(results) != 2 || results[1].Message != stored.Report.Results[1].Message {
|
|
t.Fatalf("逐目标结果与报告不一致:%+v %v", results, err)
|
|
}
|
|
if completion.Report.Results[1].Message != "secret-output-marker" {
|
|
t.Fatal("保存报告不能原地修改调用者结果")
|
|
}
|
|
if _, err := db.ExecContext(t.Context(), `INSERT INTO users (id, username, password_hash, role, status)
|
|
VALUES ('report-admin', 'report-admin', 'test', 'admin', 'active')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := db.ExecContext(t.Context(), `INSERT INTO project_members (project_id, user_id, role)
|
|
VALUES ('default', 'report-admin', 'project_admin')`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, retry, err := repo.RetryVisibleTask(t.Context(), "report-admin", completion.TaskID, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
completion.AttemptID = retry.ID
|
|
completion.Status = "success"
|
|
completion.FinishedAt = time.Now().UTC()
|
|
completion.Report = &deployment.Report{Status: "success", VerificationStatus: "success"}
|
|
if err := repo.FinishTaskAttempt(t.Context(), completion); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reports, err = reader.ListTaskDeploymentReports(t.Context(), completion.TaskID)
|
|
if err != nil || len(reports) != 2 || reports[0].Report.RecoveryStatus != "unconfirmed" || reports[1].AttemptNumber != 2 || reports[1].Report.VerificationStatus != "success" {
|
|
t.Fatalf("重试覆盖或混用了旧报告:%+v %v", reports, err)
|
|
}
|
|
if reports[1].Report.Results == nil {
|
|
t.Fatal("零目标报告必须返回空数组而非空值,避免前端忽略验证状态")
|
|
}
|
|
}
|
|
|
|
func TestTaskCompletionReportFailureRollsBackAllRecords(t *testing.T) {
|
|
for _, table := range []string{"task_deployment_reports", "task_deployment_results", "audit_logs"} {
|
|
t.Run(table, func(t *testing.T) {
|
|
repo, db := newTaskRepositoryTest(t)
|
|
defer db.Close()
|
|
completion := seedTaskCompletion(t, repo)
|
|
completion.Report = completionTestReport()
|
|
completion.AuditAction = "certificate_deploy"
|
|
if _, err := db.ExecContext(t.Context(), `CREATE TRIGGER reject_report BEFORE INSERT ON `+table+` BEGIN SELECT RAISE(ABORT, 'injected report failure'); END`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := repo.FinishTaskAttempt(t.Context(), completion); err == nil {
|
|
t.Fatal("报告保存失败必须拒绝提交终态")
|
|
}
|
|
task, err := repo.GetTask(t.Context(), completion.TaskID)
|
|
if err != nil || task.Status != "running" || task.FinishedAt != nil {
|
|
t.Fatalf("报告失败但任务被提交:%+v %v", task, err)
|
|
}
|
|
attempts, err := repo.ListAttempts(t.Context(), completion.TaskID)
|
|
if err != nil || len(attempts) != 1 || attempts[0].Status != "running" || attempts[0].FinishedAt != nil {
|
|
t.Fatalf("报告失败但尝试被提交:%+v %v", attempts, err)
|
|
}
|
|
reports, err := repo.ListTaskDeploymentReports(t.Context(), completion.TaskID)
|
|
if err != nil || len(reports) != 0 {
|
|
t.Fatalf("出现部分提交的报告:%+v %v", reports, err)
|
|
}
|
|
results, err := repo.ListTaskDeploymentResults(t.Context(), completion.TaskID)
|
|
if err != nil || len(results) != 0 {
|
|
t.Fatalf("出现部分提交的目标结果:%+v %v", results, err)
|
|
}
|
|
var auditCount int
|
|
if err := db.QueryRowContext(t.Context(), `SELECT COUNT(*) FROM audit_logs WHERE task_id = ?`, completion.TaskID).Scan(&auditCount); err != nil || auditCount != 0 {
|
|
t.Fatalf("出现部分提交的完成审计:%d %v", auditCount, err)
|
|
}
|
|
})
|
|
}
|
|
}
|