mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-08-06 21:13:31 +08:00
新增合规报表:ReportService 逐任务聚合备份合规证据(成功率/最近成功/SLA 判定/加密/受保护量)+ JSON/CSV API;前端新增 /reports 页面(汇总卡片+明细表+CSV 导出)。后端 go test、前端 tsc+vite、端到端路由验证均通过。
137 lines
4.8 KiB
Go
137 lines
4.8 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"backupx/server/internal/backup"
|
|
"backupx/server/internal/config"
|
|
"backupx/server/internal/database"
|
|
"backupx/server/internal/logger"
|
|
"backupx/server/internal/model"
|
|
"backupx/server/internal/repository"
|
|
"backupx/server/internal/storage"
|
|
"backupx/server/internal/storage/codec"
|
|
storageRclone "backupx/server/internal/storage/rclone"
|
|
)
|
|
|
|
func newReportTestHarness(t *testing.T) (*ReportService, *BackupExecutionService) {
|
|
t.Helper()
|
|
baseDir := t.TempDir()
|
|
sourceDir := filepath.Join(baseDir, "data")
|
|
storeDir := filepath.Join(baseDir, "store")
|
|
if err := os.MkdirAll(sourceDir, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(sourceDir, "f.txt"), []byte("report-data"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
log, err := logger.New(config.LogConfig{Level: "error"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(baseDir, "backupx.db")}, log)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cipher := codec.NewConfigCipher("report-secret")
|
|
targets := repository.NewStorageTargetRepository(db)
|
|
tasks := repository.NewBackupTaskRepository(db)
|
|
records := repository.NewBackupRecordRepository(db)
|
|
cfg, err := cipher.EncryptJSON(map[string]any{"basePath": storeDir})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := targets.Create(context.Background(), &model.StorageTarget{Name: "s", Type: string(storage.ProviderTypeLocalDisk), Enabled: true, ConfigCiphertext: cfg, ConfigVersion: 1, LastTestStatus: "unknown"}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
task := &model.BackupTask{Name: "rep-task", Type: "file", Enabled: true, SourcePath: sourceDir, StorageTargetID: 1, NodeID: 0, RetentionDays: 30, Compression: "gzip", MaxBackups: 10, LastStatus: "idle"}
|
|
if err := tasks.Create(context.Background(), task); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
runnerRegistry := backup.NewRegistry(backup.NewFileRunner(), backup.NewSQLiteRunner(), backup.NewMySQLRunner(nil), backup.NewPostgreSQLRunner(nil))
|
|
storageRegistry := storage.NewRegistry(storageRclone.NewLocalDiskFactory())
|
|
execution := NewBackupExecutionService(tasks, records, targets, storageRegistry, runnerRegistry, backup.NewLogHub(), nil, cipher, nil, baseDir, 2, 10, "")
|
|
return NewReportService(tasks, records), execution
|
|
}
|
|
|
|
func findRow(rows []ComplianceTaskRow, taskID uint) *ComplianceTaskRow {
|
|
for i := range rows {
|
|
if rows[i].TaskID == taskID {
|
|
return &rows[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestComplianceReport_ReflectsBackupOutcome(t *testing.T) {
|
|
report, execution := newReportTestHarness(t)
|
|
ctx := context.Background()
|
|
|
|
// 备份前:任务启用但从未成功 → at_risk。
|
|
before, err := report.ComplianceReport(ctx, 30)
|
|
if err != nil {
|
|
t.Fatalf("ComplianceReport: %v", err)
|
|
}
|
|
row := findRow(before.Tasks, 1)
|
|
if row == nil {
|
|
t.Fatal("task row missing before backup")
|
|
}
|
|
if row.Risk != "at_risk" || row.Compliant {
|
|
t.Fatalf("expected at_risk before any success, got risk=%s compliant=%v", row.Risk, row.Compliant)
|
|
}
|
|
if before.Summary.AtRiskTasks != 1 || before.Summary.CompliantTasks != 0 {
|
|
t.Fatalf("unexpected summary before: %+v", before.Summary)
|
|
}
|
|
|
|
// 跑一次成功备份。
|
|
bd, err := execution.RunTaskByIDSync(ctx, 1)
|
|
if err != nil {
|
|
t.Fatalf("RunTaskByIDSync: %v", err)
|
|
}
|
|
if bd.Status != "success" {
|
|
t.Fatalf("backup not success: %s", bd.Status)
|
|
}
|
|
|
|
// 备份后:合规、成功率 1.0、保护字节数 > 0。
|
|
after, err := report.ComplianceReport(ctx, 30)
|
|
if err != nil {
|
|
t.Fatalf("ComplianceReport after: %v", err)
|
|
}
|
|
row = findRow(after.Tasks, 1)
|
|
if row == nil {
|
|
t.Fatal("task row missing after backup")
|
|
}
|
|
if !row.Compliant || row.Risk != "ok" {
|
|
t.Fatalf("expected ok/compliant after success, got risk=%s compliant=%v", row.Risk, row.Compliant)
|
|
}
|
|
if row.TotalRuns != 1 || row.Successes != 1 || row.Failures != 0 {
|
|
t.Fatalf("unexpected counts: runs=%d ok=%d fail=%d", row.TotalRuns, row.Successes, row.Failures)
|
|
}
|
|
if row.SuccessRate != 1 {
|
|
t.Fatalf("expected success rate 1.0, got %v", row.SuccessRate)
|
|
}
|
|
if row.LastStatus != "success" || row.LastSuccessAt == nil || row.ProtectedBytes <= 0 {
|
|
t.Fatalf("unexpected last/protected: status=%s lastSuccess=%v bytes=%d", row.LastStatus, row.LastSuccessAt, row.ProtectedBytes)
|
|
}
|
|
if after.Summary.CompliantTasks != 1 || after.Summary.AtRiskTasks != 0 || after.Summary.OverallSuccessRate != 1 {
|
|
t.Fatalf("unexpected summary after: %+v", after.Summary)
|
|
}
|
|
if after.Summary.TotalProtectedB <= 0 {
|
|
t.Fatalf("expected protected bytes > 0, got %d", after.Summary.TotalProtectedB)
|
|
}
|
|
}
|
|
|
|
func TestComplianceReport_RejectsInvalidRange(t *testing.T) {
|
|
report, _ := newReportTestHarness(t)
|
|
ctx := context.Background()
|
|
if _, err := report.ComplianceReport(ctx, 0); err == nil {
|
|
t.Fatal("expected error for days=0")
|
|
}
|
|
if _, err := report.ComplianceReport(ctx, 9999); err == nil {
|
|
t.Fatal("expected error for days>365")
|
|
}
|
|
}
|