Files
DoGaMa-serv/internal/persistence/sqlite/catalog_test.go
T
2026-08-17 21:14:24 +02:00

360 lines
7.8 KiB
Go

package sqlite_test
import (
"context"
"errors"
"path/filepath"
"strings"
"testing"
catalogdata "git.zaynet.fr/DoGaMa/DoGaMa-serv/catalog"
"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"
)
func findSnapshot(t *testing.T, snapshots []catalog.Snapshot, id string) catalog.Snapshot {
t.Helper()
for _, snapshot := range snapshots {
if snapshot.Template.ID == id {
return snapshot
}
}
t.Fatalf("template %q not found in catalog", id)
return catalog.Snapshot{}
}
func TestCatalogSyncIsImmutableAndDraftPinsSnapshot(t *testing.T) {
ctx := context.Background()
db, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "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)
}
palworld := findSnapshot(t, snapshots, "palworld-official")
if err := repository.Sync(ctx, snapshots); err != nil {
t.Fatal(err)
}
summaries, err := repository.List(ctx)
if err != nil {
t.Fatal(err)
}
if len(summaries) != len(snapshots) {
t.Fatalf("summaries = %#v, want %d templates", summaries, len(snapshots))
}
var foundPalworld bool
for _, summary := range summaries {
if summary.TemplateID == palworld.Template.ID {
foundPalworld = true
if summary.Digest != palworld.Digest {
t.Fatalf(
"palworld summary digest = %q, want %q",
summary.Digest,
palworld.Digest,
)
}
}
}
if !foundPalworld {
t.Fatalf("palworld summary missing: %#v", summaries)
}
loaded, err := repository.Get(
ctx,
palworld.Template.ID,
palworld.Template.Version,
)
if err != nil || loaded.Digest != palworld.Digest {
t.Fatalf("loaded snapshot = %#v, error = %v", loaded, err)
}
tampered := palworld
tampered.Digest = "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff"
if err := repository.Sync(ctx, []catalog.Snapshot{tampered}); !errors.Is(err, catalog.ErrImmutableSnapshot) {
t.Fatalf("immutable snapshot error = %v", err)
}
preview, err := instance.BuildPreview(palworld, instance.PreviewRequest{
DisplayName: "Family Palworld",
Slug: "family-palworld",
HostPorts: map[string]int{"game": 8211},
MountPaths: map[string]string{"saved": "/srv/game-servers/family-palworld/saved"},
DataOrigin: "new",
BackupRetention: 7,
})
if err != nil {
t.Fatal(err)
}
if err := repository.CreateDraft(ctx, instance.Draft{
ID: "opaque-instance-id",
Preview: preview,
}); err != nil {
t.Fatal(err)
}
var state, digest string
if err := db.QueryRow(
"SELECT lifecycle_state, template_digest FROM instances WHERE id=?",
"opaque-instance-id",
).Scan(&state, &digest); err != nil {
t.Fatal(err)
}
if state != "draft" || digest != palworld.Digest {
t.Fatalf("draft state=%q digest=%q", state, digest)
}
if _, err := repository.BeginOperation(
ctx,
"operation-one",
"opaque-instance-id",
"install",
"installing",
); err != nil {
t.Fatal(err)
}
if err := repository.SetOperationStep(
ctx,
"operation-one",
"installation",
"running",
); err != nil {
t.Fatal(err)
}
progress, err := repository.GetOperationProgress(ctx, "operation-one")
if err != nil ||
progress.GlobalStatus != "running" ||
progress.CurrentStep != "installation" ||
len(progress.Steps) != 7 {
t.Fatalf("progress=%#v err=%v", progress, err)
}
if _, err := repository.BeginOperation(
ctx,
"operation-two",
"opaque-instance-id",
"start",
"starting",
); !errors.Is(err, instance.ErrOperationConflict) {
t.Fatalf("parallel operation error = %v", err)
}
if err := repository.FailOperation(
ctx,
"operation-one",
"error",
"test_failure",
); err != nil {
t.Fatal(err)
}
progress, err = repository.GetOperationProgress(ctx, "operation-one")
if err != nil ||
progress.GlobalStatus != "failed" ||
progress.ErrorCode != "test_failure" ||
progress.FinishedAt == "" {
t.Fatalf("failed progress=%#v err=%v", progress, err)
}
if _, err := repository.BeginOperation(
ctx,
"operation-three",
"opaque-instance-id",
"start",
"starting",
); err != nil {
t.Fatal(err)
}
if err := repository.RecoverInterruptedOperations(ctx); err != nil {
t.Fatal(err)
}
var operationState string
if err := db.QueryRow(
"SELECT state FROM instance_operations WHERE id='operation-three'",
).Scan(&operationState); err != nil {
t.Fatal(err)
}
if err := db.QueryRow(
"SELECT lifecycle_state FROM instances WHERE id='opaque-instance-id'",
).Scan(&state); err != nil {
t.Fatal(err)
}
if operationState != "intervention_required" ||
state != "intervention_required" {
t.Fatalf(
"recovered operation=%q instance=%q",
operationState,
state,
)
}
}
func TestReplaceMakesLocalCatalogDiskStateAuthoritative(t *testing.T) {
ctx := context.Background()
db, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "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)
}
palworld := findSnapshot(t, snapshots, "palworld-official")
if err := repository.Replace(ctx, snapshots); err != nil {
t.Fatal(err)
}
changed := palworld
changed.Template.Game.Name = "Palworld changed"
changed.CanonicalYAML = strings.Replace(
changed.CanonicalYAML,
"Palworld",
"Palworld changed",
1,
)
changed.Digest = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
if err := repository.Replace(ctx, []catalog.Snapshot{changed}); err != nil {
t.Fatal(err)
}
list, err := repository.List(ctx)
if err != nil ||
len(list) != 1 ||
list[0].GameName != "Palworld changed" {
t.Fatalf("updated list = %#v, %v", list, err)
}
if err := repository.Replace(ctx, nil); err != nil {
t.Fatal(err)
}
list, err = repository.List(ctx)
if err != nil || len(list) != 0 {
t.Fatalf("removed list = %#v, %v", list, err)
}
}
func TestSyncActivatesCurrentBundledSnapshotWithoutDiscardingHistory(t *testing.T) {
ctx := context.Background()
db, err := sqlite.Open(ctx, filepath.Join(t.TempDir(), "dogama.db"))
if err != nil {
t.Fatal(err)
}
defer db.Close()
repository := sqlite.NewRepository(db)
current, err := catalog.LoadFS(catalogdata.Files, ".")
if err != nil {
t.Fatal(err)
}
palworld := findSnapshot(t, current, "palworld-official")
body, err := catalogdata.Files.ReadFile("palworld/template.yaml")
if err != nil {
t.Fatal(err)
}
oldBody := strings.Replace(
string(body),
"version: 1.1.1",
"version: 1.1.0",
1,
)
old, err := catalog.Validate(
[]byte(oldBody),
"palworld",
catalogdata.Files,
)
if err != nil {
t.Fatal(err)
}
if err := repository.Replace(ctx, []catalog.Snapshot{old}); err != nil {
t.Fatal(err)
}
if err := repository.Sync(ctx, current); err != nil {
t.Fatal(err)
}
summaries, err := repository.List(ctx)
if err != nil {
t.Fatal(err)
}
if len(summaries) != len(current) {
t.Fatalf(
"summaries = %#v, want %d current templates",
summaries,
len(current),
)
}
var activePalworldFound bool
for _, summary := range summaries {
if summary.TemplateID != palworld.Template.ID {
continue
}
activePalworldFound = true
if summary.Version != palworld.Template.Version ||
summary.Digest != palworld.Digest {
t.Fatalf(
"active palworld summary = %#v, want %s@%s",
summary,
palworld.Template.ID,
palworld.Template.Version,
)
}
}
if !activePalworldFound {
t.Fatalf("active palworld summary missing: %#v", summaries)
}
preserved, err := repository.Get(
ctx,
old.Template.ID,
old.Template.Version,
)
if err != nil || preserved.Digest != old.Digest {
t.Fatalf(
"historical snapshot = %#v, error = %v",
preserved,
err,
)
}
}