diff --git a/.gitattributes b/.gitattributes index 526c8a3..a11336d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ -*.sh text eol=lf \ No newline at end of file +*.sh text eol=lf +*.go text eol=lf \ No newline at end of file diff --git a/internal/agent/agent_plan_test.go b/internal/agent/agent_plan_test.go index 5a3028f..12ccb9c 100644 --- a/internal/agent/agent_plan_test.go +++ b/internal/agent/agent_plan_test.go @@ -11,35 +11,71 @@ import ( func TestEmbeddedPalworldSnapshotMatchesApplicationDeploymentPlan(t *testing.T) { snapshots, err := catalog.LoadFS(catalogdata.Files, ".") - if err != nil || len(snapshots) != 1 { - t.Fatalf("embedded snapshots = %#v, error = %v", snapshots, err) + if err != nil { + t.Fatal(err) } - snapshot := snapshots[0] - if snapshot.Template.ID != "palworld-official" || snapshot.Template.Version != "1.1.1" { - t.Fatalf("embedded Palworld snapshot = %s@%s", snapshot.Template.ID, snapshot.Template.Version) + + var snapshot *catalog.Snapshot + for i := range snapshots { + if snapshots[i].Template.ID == "palworld-official" { + snapshot = &snapshots[i] + break + } } - preview, err := instance.BuildPreview(snapshot, instance.PreviewRequest{ - DisplayName: "Snapshot consistency", Slug: "snapshot-consistency", - HostPorts: map[string]int{"game": 38211}, - MountPaths: map[string]string{"saved": filepath.Join(t.TempDir(), "saved")}, - DataOrigin: "new", BackupRetention: 7, + + if snapshot == nil { + t.Fatal(`embedded template "palworld-official" not found`) + } + + if snapshot.Template.Version != "1.1.1" { + t.Fatalf( + "embedded Palworld snapshot = %s@%s", + snapshot.Template.ID, + snapshot.Template.Version, + ) + } + + preview, err := instance.BuildPreview(*snapshot, instance.PreviewRequest{ + DisplayName: "Snapshot consistency", + Slug: "snapshot-consistency", + HostPorts: map[string]int{ + "game": 38211, + }, + MountPaths: map[string]string{ + "saved": filepath.Join(t.TempDir(), "saved"), + }, + DataOrigin: "new", + BackupRetention: 7, }) if err != nil { t.Fatal(err) } + plan, err := preview.DeploymentPlan("abcdefghijklmnopqrstuvwx") if err != nil { t.Fatal(err) } + policy, err := NewPlanPolicy(snapshots, catalogdata.Files) if err != nil { t.Fatal(err) } + known, ok := policy.snapshots[plan.TemplateID+"@"+plan.TemplateVersion] if !ok || known.Digest != plan.TemplateDigest { - t.Fatalf("agent snapshot=%#v plan=%s@%s digest=%s", known, plan.TemplateID, plan.TemplateVersion, plan.TemplateDigest) + t.Fatalf( + "agent snapshot=%#v plan=%s@%s digest=%s", + known, + plan.TemplateID, + plan.TemplateVersion, + plan.TemplateDigest, + ) } + if err := policy.Validate(plan); err != nil { - t.Fatalf("application plan rejected by matching embedded snapshot: %v", err) + t.Fatalf( + "application plan rejected by matching embedded snapshot: %v", + err, + ) } } diff --git a/internal/persistence/sqlite/catalog_test.go b/internal/persistence/sqlite/catalog_test.go index e9764b8..e4274ec 100644 --- a/internal/persistence/sqlite/catalog_test.go +++ b/internal/persistence/sqlite/catalog_test.go @@ -13,6 +13,19 @@ import ( "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")) @@ -20,81 +33,180 @@ func TestCatalogSyncIsImmutableAndDraftPinsSnapshot(t *testing.T) { 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 || len(summaries) != 1 || summaries[0].Digest != snapshots[0].Digest { - t.Fatalf("summaries = %#v, error = %v", summaries, err) + if err != nil { + t.Fatal(err) } - loaded, err := repository.Get(ctx, snapshots[0].Template.ID, snapshots[0].Template.Version) - if err != nil || loaded.Digest != snapshots[0].Digest { + 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 := snapshots[0] + + 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(snapshots[0], 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, + + 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 { + + 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 { + 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 != snapshots[0].Digest { + + 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 { + + 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 { + + 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 { + 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) { + + 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 { + + 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 == "" { + 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 { + + 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 { + 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 { + + 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) + + if operationState != "intervention_required" || + state != "intervention_required" { + t.Fatalf( + "recovered operation=%q instance=%q", + operationState, + state, + ) } } @@ -105,28 +217,45 @@ func TestReplaceMakesLocalCatalogDiskStateAuthoritative(t *testing.T) { 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 := snapshots[0] + + changed := palworld changed.Template.Game.Name = "Palworld changed" - changed.CanonicalYAML = strings.Replace(changed.CanonicalYAML, "Palworld", "Palworld changed", 1) + 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" { + 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) @@ -140,34 +269,91 @@ func TestSyncActivatesCurrentBundledSnapshotWithoutDiscardingHistory(t *testing. t.Fatal(err) } defer db.Close() + repository := sqlite.NewRepository(db) + current, err := catalog.LoadFS(catalogdata.Files, ".") - if err != nil || len(current) != 1 { - t.Fatalf("current catalog = %#v, %v", current, err) + 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) + + 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 || len(summaries) != 1 { - t.Fatalf("summaries = %#v, error = %v", summaries, err) + if err != nil { + t.Fatal(err) } - if summaries[0].Version != current[0].Template.Version || summaries[0].Digest != current[0].Digest { - t.Fatalf("active summary = %#v, want %s@%s", summaries[0], current[0].Template.ID, current[0].Template.Version) + + if len(summaries) != len(current) { + t.Fatalf( + "summaries = %#v, want %d current templates", + summaries, + len(current), + ) } - if preserved, err := repository.Get(ctx, old.Template.ID, old.Template.Version); err != nil || preserved.Digest != old.Digest { - t.Fatalf("historical snapshot = %#v, error = %v", preserved, err) + + 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, + ) } } diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 4b3cc14..d437434 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -1070,7 +1070,7 @@ func TestCatalogPagesAndAdminScan(t *testing.T) { adminCookies := []*http.Cookie{{Name: sessionCookie, Value: admin.Token}, {Name: csrfCookie, Value: admin.CSRFToken}} page := request(t, handler, http.MethodGet, "/catalog", adminCookies) assertStatus(t, page, http.StatusOK) - for _, expected := range []string{"Palworld", snapshots[0].Template.Game.Image, "/catalog/palworld-official", "Scan"} { + for _, expected := range []string{"Palworld", snapshots[0].Template.Game.Artwork.Image, "/catalog/palworld-official", "Scan"} { if !strings.Contains(page.Body.String(), expected) { t.Fatalf("catalog missing %q", expected) }