206 lines
6.7 KiB
Go
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
|
|
}
|