package publish import ( "context" "testing" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/state" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) func TestExecuteAdditiveWritesOutputsAndCatalog(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive) existing := baseCatalog(req) existing.Outputs = []state.CatalogOutputFile{ catalogOutput(req, "reports", "archive", "report.md", state.OutputKindSource, planCreatedAt), catalogOutput(req, "reports", "web", "old.txt", state.OutputKindSource, planCreatedAt), } writeCatalogState(t, destinationBackend, "", existing) testutil.WriteFakeFile(t, destinationBackend, "report.md", "old report") testutil.WriteFakeFile(t, destinationBackend, "old.txt", "retained") plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "report.md", "# Report\nSunny.\n") testutil.AssertFakeFile(t, destinationBackend, "summary.txt", "Summary\n") testutil.AssertFakeFile(t, destinationBackend, "old.txt", "retained") catalog := readCatalogState(t, destinationBackend, "") if catalog.SchemaVersion != state.CatalogSchemaVersion || catalog.State.Mode != state.StateModeCatalog { t.Fatalf("catalog identity = schema %d mode %s", catalog.SchemaVersion, catalog.State.Mode) } if len(catalog.Outputs) != 3 { t.Fatalf("catalog outputs = %#v, want three outputs", catalog.Outputs) } report, ok := state.FindCatalogOutputByPath(catalog.Outputs, "report.md") if !ok { t.Fatalf("catalog outputs = %#v, want report.md", catalog.Outputs) } if !report.CreatedAt.Equal(planCreatedAt) || !report.UpdatedAt.Equal(planUpdatedAt) { t.Fatalf("report times = %s/%s, want created preserved and updated now", report.CreatedAt, report.UpdatedAt) } if report.SourcePath != "" { t.Fatalf("source catalog output source_path = %q, want empty", report.SourcePath) } } func TestExecuteReplacementDeletesCurrentOwnerAndPreservesOtherOwners(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement) existing := baseCatalog(req) existing.Outputs = []state.CatalogOutputFile{ catalogOutput(req, "reports", "archive", "report.md", state.OutputKindSource, planCreatedAt), catalogOutput(req, "reports", "archive", "stale.txt", state.OutputKindSource, planCreatedAt), catalogOutput(req, "reports", "web", "shared.txt", state.OutputKindSource, planCreatedAt), } writeCatalogState(t, destinationBackend, "", existing) testutil.WriteFakeFile(t, destinationBackend, "report.md", "old report") testutil.WriteFakeFile(t, destinationBackend, "stale.txt", "delete me") testutil.WriteFakeFile(t, destinationBackend, "shared.txt", "keep me") plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "report.md", "# Report\nSunny.\n") testutil.AssertFakeFile(t, destinationBackend, "summary.txt", "Summary\n") testutil.AssertFakeMissing(t, destinationBackend, "stale.txt") testutil.AssertFakeFile(t, destinationBackend, "shared.txt", "keep me") catalog := readCatalogState(t, destinationBackend, "") if _, ok := state.FindCatalogOutputByPath(catalog.Outputs, "stale.txt"); ok { t.Fatalf("catalog outputs = %#v, want stale.txt removed", catalog.Outputs) } if _, ok := state.FindCatalogOutputByPath(catalog.Outputs, "shared.txt"); !ok { t.Fatalf("catalog outputs = %#v, want shared.txt retained", catalog.Outputs) } } func TestExecuteSupersededReplacementClearsDestinationRootOnly(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowReplacement) req.DestinationBundlePath = "bundle" legacyState := legacyStateDocument(2) writeJSONState(t, destinationBackend, req.DestinationBundlePath, legacyState) testutil.WriteFakeFile(t, destinationBackend, "bundle/unplanned.txt", "remove") testutil.WriteFakeFile(t, destinationBackend, "outside.txt", "keep") plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeMissing(t, destinationBackend, "bundle/unplanned.txt") testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nSunny.\n") testutil.AssertFakeFile(t, destinationBackend, "outside.txt", "keep") readCatalogState(t, destinationBackend, "bundle") } func TestExecuteSupersededAdditiveLeavesUnplannedFilesUnmanaged(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive) legacyState := legacyStateDocument(2) writeJSONState(t, destinationBackend, "", legacyState) testutil.WriteFakeFile(t, destinationBackend, "report.md", "legacy report") testutil.WriteFakeFile(t, destinationBackend, "unplanned.txt", "leave me") plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "report.md", "# Report\nSunny.\n") testutil.AssertFakeFile(t, destinationBackend, "unplanned.txt", "leave me") catalog := readCatalogState(t, destinationBackend, "") if _, ok := state.FindCatalogOutputByPath(catalog.Outputs, "unplanned.txt"); ok { t.Fatalf("catalog outputs = %#v, want unplanned file omitted", catalog.Outputs) } } func TestExecuteForceReplaceClearsDestinationBundlePathOnly(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive) req.DestinationBundlePath = "bundle" req.Force = true testutil.WriteFakeFile(t, destinationBackend, "bundle/report.md", "old report") testutil.WriteFakeFile(t, destinationBackend, "bundle/unplanned.txt", "remove") testutil.WriteFakeFile(t, destinationBackend, "bundle-child/keep.txt", "keep") testutil.WriteFakeFile(t, destinationBackend, "outside.txt", "keep") plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionForceReplace { t.Fatalf("plan action = %s, want %s", plan.Action, ActionForceReplace) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nSunny.\n") testutil.AssertFakeFile(t, destinationBackend, "bundle/summary.txt", "Summary\n") testutil.AssertFakeMissing(t, destinationBackend, "bundle/unplanned.txt") testutil.AssertFakeFile(t, destinationBackend, "bundle-child/keep.txt", "keep") testutil.AssertFakeFile(t, destinationBackend, "outside.txt", "keep") catalog := readCatalogState(t, destinationBackend, "bundle") if catalog.SchemaVersion != state.CatalogSchemaVersion || catalog.State.Mode != state.StateModeCatalog { t.Fatalf("catalog identity = schema %d mode %s", catalog.SchemaVersion, catalog.State.Mode) } if len(catalog.Outputs) != 2 { t.Fatalf("catalog outputs = %#v, want planned outputs only", catalog.Outputs) } } func TestExecuteForceReplaceClearsFixedDestinationRoot(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive) req.DestinationBundlePath = "" req.PathMapping = config.PathMappingFixed req.Force = true testutil.WriteFakeFile(t, destinationBackend, "report.md", "old report") testutil.WriteFakeFile(t, destinationBackend, "unplanned.txt", "remove") plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionForceReplace || storage.DisplayPath(plan.DestinationBundlePath) != "." { t.Fatalf("plan action=%s destination=%s, want force_replace at root", plan.Action, storage.DisplayPath(plan.DestinationBundlePath)) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "report.md", "# Report\nSunny.\n") testutil.AssertFakeFile(t, destinationBackend, "summary.txt", "Summary\n") testutil.AssertFakeMissing(t, destinationBackend, "unplanned.txt") readCatalogState(t, destinationBackend, "") } func TestExecuteFailedWriteDoesNotWriteCatalogState(t *testing.T) { _, destinationBackend, req := catalogPlanRequest(t, config.WorkflowAdditive) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if err := destinationBackend.AddDirectory("report.md"); err != nil { t.Fatalf("add conflicting directory: %v", err) } err = Execute(context.Background(), req, plan) if err == nil { t.Fatal("Execute() error = nil, want write failure") } if _, statErr := destinationBackend.Stat(context.Background(), storage.StateFileName); !storage.IsNotFound(statErr) { t.Fatalf("state stat error = %v, want missing state", statErr) } } func readCatalogState(t *testing.T, backend *fake.Backend, relative string) state.CatalogState { t.Helper() statePath, err := storage.StatePath(relative) if err != nil { t.Fatalf("state path: %v", err) } data, err := backend.ReadFile(context.Background(), statePath) if err != nil { t.Fatalf("read catalog state: %v", err) } catalog, err := state.ParseCatalog(data) if err != nil { t.Fatalf("parse catalog state: %v", err) } return catalog }