Files
BackupX/server/internal/service/backup_task_service_test.go
2026-05-13 14:24:45 +08:00

197 lines
6.5 KiB
Go

package service
import (
"context"
"path/filepath"
"strings"
"testing"
"backupx/server/internal/config"
"backupx/server/internal/database"
"backupx/server/internal/logger"
"backupx/server/internal/model"
"backupx/server/internal/repository"
"backupx/server/internal/storage/codec"
)
func newBackupTaskServiceForTest(t *testing.T) (*BackupTaskService, repository.StorageTargetRepository, repository.BackupTaskRepository) {
t.Helper()
log, err := logger.New(config.LogConfig{Level: "error"})
if err != nil {
t.Fatalf("logger.New returned error: %v", err)
}
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "backupx.db")}, log)
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
targets := repository.NewStorageTargetRepository(db)
tasks := repository.NewBackupTaskRepository(db)
service := NewBackupTaskService(tasks, targets, codec.NewConfigCipher("task-service-secret"))
return service, targets, tasks
}
func TestBackupTaskServiceRejectsEncryptedRemoteTasks(t *testing.T) {
ctx := context.Background()
service, targets, _ := newBackupTaskServiceForTest(t)
service.SetNodeRepository(&nodeRepoStub{nodes: []model.Node{
{ID: 41, Name: "master", Token: "master-token", Status: model.NodeStatusOnline, IsLocal: true},
{ID: 42, Name: "edge", Token: "edge-token", Status: model.NodeStatusOnline, IsLocal: false},
}})
if err := targets.Create(ctx, &model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "ciphertext", ConfigVersion: 1, LastTestStatus: "unknown"}); err != nil {
t.Fatalf("seed storage target error: %v", err)
}
_, err := service.Create(ctx, BackupTaskUpsertInput{
Name: "encrypted-node-pool",
Type: "file",
Enabled: true,
SourcePath: "/srv/site",
StorageTargetID: 1,
NodePoolTag: "db",
RetentionDays: 30,
Compression: "gzip",
MaxBackups: 10,
Encrypt: true,
})
if err == nil || !strings.Contains(err.Error(), "远程节点暂不支持加密备份") {
t.Fatalf("expected encrypted node-pool task to be rejected, got %v", err)
}
created, err := service.Create(ctx, BackupTaskUpsertInput{
Name: "local-encrypted",
Type: "file",
Enabled: true,
SourcePath: "/srv/site",
StorageTargetID: 1,
RetentionDays: 30,
Compression: "gzip",
MaxBackups: 10,
Encrypt: true,
})
if err != nil {
t.Fatalf("Create local encrypted task returned error: %v", err)
}
localNodeTask, err := service.Create(ctx, BackupTaskUpsertInput{
Name: "local-node-encrypted",
Type: "file",
Enabled: true,
SourcePath: "/srv/site",
StorageTargetID: 1,
NodeID: 41,
RetentionDays: 30,
Compression: "gzip",
MaxBackups: 10,
Encrypt: true,
})
if err != nil {
t.Fatalf("Create encrypted task pinned to local node returned error: %v", err)
}
if localNodeTask.NodeID != 41 || !localNodeTask.Encrypt {
t.Fatalf("expected encrypted task to keep local node, got %#v", localNodeTask)
}
_, err = service.Update(ctx, created.ID, BackupTaskUpsertInput{
Name: created.Name,
Type: created.Type,
Enabled: true,
SourcePath: "/srv/site",
StorageTargetID: 1,
NodeID: 42,
RetentionDays: 30,
Compression: "gzip",
MaxBackups: 10,
Encrypt: true,
})
if err == nil || !strings.Contains(err.Error(), "远程节点暂不支持加密备份") {
t.Fatalf("expected encrypted fixed-node update to be rejected, got %v", err)
}
}
func TestBackupTaskServiceCreateAndGet(t *testing.T) {
ctx := context.Background()
service, targets, _ := newBackupTaskServiceForTest(t)
if err := targets.Create(ctx, &model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "ciphertext", ConfigVersion: 1, LastTestStatus: "unknown"}); err != nil {
t.Fatalf("seed storage target error: %v", err)
}
created, err := service.Create(ctx, BackupTaskUpsertInput{
Name: "site-files",
Type: "file",
Enabled: true,
SourcePath: "/srv/site",
ExcludePatterns: []string{"*.log", "node_modules"},
StorageTargetID: 1,
RetentionDays: 30,
Compression: "gzip",
MaxBackups: 10,
})
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
if created.Name != "site-files" || len(created.ExcludePatterns) != 2 {
t.Fatalf("unexpected created task: %#v", created)
}
loaded, err := service.Get(ctx, created.ID)
if err != nil {
t.Fatalf("Get returned error: %v", err)
}
if loaded.StorageTargetName != "local" {
t.Fatalf("expected storage target name local, got %s", loaded.StorageTargetName)
}
}
func TestBackupTaskServiceKeepsMaskedPasswordOnUpdate(t *testing.T) {
ctx := context.Background()
service, targets, tasks := newBackupTaskServiceForTest(t)
if err := targets.Create(ctx, &model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "ciphertext", ConfigVersion: 1, LastTestStatus: "unknown"}); err != nil {
t.Fatalf("seed storage target error: %v", err)
}
created, err := service.Create(ctx, BackupTaskUpsertInput{
Name: "mysql-prod",
Type: "mysql",
Enabled: true,
DBHost: "127.0.0.1",
DBPort: 3306,
DBUser: "root",
DBPassword: "secret",
DBName: "app",
StorageTargetID: 1,
RetentionDays: 7,
Compression: "gzip",
MaxBackups: 5,
})
if err != nil {
t.Fatalf("Create returned error: %v", err)
}
stored, err := tasks.FindByID(ctx, created.ID)
if err != nil {
t.Fatalf("FindByID returned error: %v", err)
}
originalCiphertext := stored.DBPasswordCiphertext
updated, err := service.Update(ctx, created.ID, BackupTaskUpsertInput{
Name: created.Name,
Type: created.Type,
Enabled: true,
DBHost: "127.0.0.1",
DBPort: 3306,
DBUser: "root",
DBPassword: "",
DBName: "app_updated",
StorageTargetID: 1,
RetentionDays: 7,
Compression: "gzip",
MaxBackups: 5,
})
if err != nil {
t.Fatalf("Update returned error: %v", err)
}
if len(updated.MaskedFields) != 1 || updated.MaskedFields[0] != "dbPassword" {
t.Fatalf("expected masked dbPassword field, got %#v", updated.MaskedFields)
}
reloaded, err := tasks.FindByID(ctx, created.ID)
if err != nil {
t.Fatalf("FindByID returned error: %v", err)
}
if reloaded.DBPasswordCiphertext != originalCiphertext {
t.Fatalf("expected ciphertext unchanged")
}
}