Files
DoGaMa-serv/internal/backup/service_test.go
T

206 lines
6.7 KiB
Go

package backup_test
import (
"context"
"errors"
"os"
"path/filepath"
"testing"
catalogdata "git.zaynet.fr/DoGaMa/DoGaMa-serv/catalog"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/agentwire"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/auth"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/backup"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/instance"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/persistence/sqlite"
)
type backupAgent struct{ running bool }
func (a *backupAgent) StartInstance(_ context.Context, id string) (agentwire.InstanceState, error) {
a.running = true
return agentwire.InstanceState{InstanceID: id, ContainerID: "container", Running: true, Ready: true, Health: "healthy"}, nil
}
func (a *backupAgent) StopInstance(_ context.Context, id string, _ int) (agentwire.InstanceState, error) {
a.running = false
return agentwire.InstanceState{InstanceID: id, ContainerID: "container", Health: "stopped"}, nil
}
func TestCreateRestoreExportAndRetention(t *testing.T) {
ctx := context.Background()
root := t.TempDir()
serversRoot, backupsRoot := filepath.Join(root, "servers"), filepath.Join(root, "backups")
mount := filepath.Join(serversRoot, "instance", "saved")
if err := os.MkdirAll(mount, 0o750); err != nil {
t.Fatal(err)
}
world := filepath.Join(mount, "Level.sav")
if err := os.WriteFile(world, []byte("world-v1"), 0o640); err != nil {
t.Fatal(err)
}
db, err := sqlite.Open(ctx, filepath.Join(root, "dogama.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
repository := sqlite.NewRepository(db)
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
}
if err := repository.Sync(ctx, snapshots); err != nil {
t.Fatal(err)
}
authService := auth.New(db)
if err := authService.BootstrapAdmin(ctx, "admin", "correct horse battery staple"); err != nil {
t.Fatal(err)
}
adminSession, err := authService.Login(ctx, "admin", "correct horse battery staple", "192.0.2.1:1234")
if err != nil {
t.Fatal(err)
}
admin, err := authService.Authenticate(ctx, adminSession.Token)
if err != nil {
t.Fatal(err)
}
preview, err := instance.BuildPreview(snapshots[0], instance.PreviewRequest{
DisplayName: "Backup Test", Slug: "backup-test", HostPorts: map[string]int{"game": 38211},
MountPaths: map[string]string{"saved": mount}, DataOrigin: "new", BackupRetention: 2,
})
if err != nil {
t.Fatal(err)
}
const instanceID = "backup-instance"
if err := repository.CreateDraft(ctx, instance.Draft{ID: instanceID, Preview: preview}); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`UPDATE instances SET lifecycle_state='online', observed_state='ready', container_id='container', desired_running=1 WHERE id=?`, instanceID); err != nil {
t.Fatal(err)
}
agent := &backupAgent{running: true}
service, err := backup.New(repository, agent, serversRoot, backupsRoot)
if err != nil {
t.Fatal(err)
}
manual, err := service.Create(ctx, admin.ID, instanceID, "manual")
if err != nil {
t.Fatal(err)
}
if manual.Status != "available" || manual.SHA256 == "" || manual.SizeBytes == 0 || !agent.running {
t.Fatalf("backup = %#v, running=%v", manual, agent.running)
}
exported, metadata, err := service.Export(ctx, instanceID, manual.ID)
if err != nil {
t.Fatal(err)
}
if metadata.ID != manual.ID {
t.Fatalf("export metadata = %#v", metadata)
}
if err := exported.Close(); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(world, []byte("world-v2"), 0o640); err != nil {
t.Fatal(err)
}
if err := service.Restore(ctx, admin.ID, instanceID, manual.ID); err != nil {
t.Fatal(err)
}
restored, err := os.ReadFile(world)
if err != nil || string(restored) != "world-v1" {
t.Fatalf("restored world = %q, error = %v", restored, err)
}
for index := 0; index < 3; index++ {
if _, err := service.Create(ctx, admin.ID, instanceID, "scheduled"); err != nil {
t.Fatal(err)
}
}
backups, err := service.List(ctx, instanceID)
if err != nil {
t.Fatal(err)
}
var manualCount, scheduledCount, safetyCount int
for _, item := range backups {
switch item.Origin {
case "manual":
manualCount++
case "scheduled":
scheduledCount++
case "pre_restore":
safetyCount++
}
}
if manualCount != 1 || scheduledCount != 2 || safetyCount != 1 {
t.Fatalf("retained backups: manual=%d scheduled=%d safety=%d", manualCount, scheduledCount, safetyCount)
}
}
func TestBackupRejectsSymlinks(t *testing.T) {
ctx, service, actorID, instanceID, mount := backupFixture(t)
outside := filepath.Join(t.TempDir(), "outside")
if err := os.WriteFile(outside, []byte("secret"), 0o600); err != nil {
t.Fatal(err)
}
if err := os.Symlink(outside, filepath.Join(mount, "escape")); err != nil {
t.Fatal(err)
}
_, err := service.Create(ctx, actorID, instanceID, "manual")
if !errors.Is(err, backup.ErrUnsafePath) {
t.Fatalf("symlink backup error = %v", err)
}
}
func backupFixture(t *testing.T) (context.Context, *backup.Service, string, string, string) {
t.Helper()
ctx := context.Background()
root := t.TempDir()
serversRoot := filepath.Join(root, "servers")
backupsRoot := filepath.Join(root, "backups")
mount := filepath.Join(serversRoot, "instance", "saved")
if err := os.MkdirAll(mount, 0o750); err != nil {
t.Fatal(err)
}
db, err := sqlite.Open(ctx, filepath.Join(root, "dogama.db"))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = db.Close() })
repository := sqlite.NewRepository(db)
snapshots, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
}
if err := repository.Sync(ctx, snapshots); err != nil {
t.Fatal(err)
}
authService := auth.New(db)
if err := authService.BootstrapAdmin(ctx, "admin", "correct horse battery staple"); err != nil {
t.Fatal(err)
}
session, err := authService.Login(ctx, "admin", "correct horse battery staple", "192.0.2.1:1234")
if err != nil {
t.Fatal(err)
}
actor, err := authService.Authenticate(ctx, session.Token)
if err != nil {
t.Fatal(err)
}
preview, err := instance.BuildPreview(snapshots[0], instance.PreviewRequest{DisplayName: "Backup Test", Slug: "backup-test", HostPorts: map[string]int{"game": 38211}, MountPaths: map[string]string{"saved": mount}, DataOrigin: "new", BackupRetention: 2})
if err != nil {
t.Fatal(err)
}
instanceID := "backup-instance"
if err := repository.CreateDraft(ctx, instance.Draft{ID: instanceID, Preview: preview}); err != nil {
t.Fatal(err)
}
if _, err := db.Exec(`UPDATE instances SET lifecycle_state='stopped', observed_state='stopped', container_id='container' WHERE id=?`, instanceID); err != nil {
t.Fatal(err)
}
service, err := backup.New(repository, &backupAgent{}, serversRoot, backupsRoot)
if err != nil {
t.Fatal(err)
}
return ctx, service, actor.ID, instanceID, mount
}