182 lines
7.1 KiB
Go
182 lines
7.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"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 TestReconcileStateDryRunReportsMissingManagedOutputsWithoutRewrite(t *testing.T) {
|
|
backend := fake.New()
|
|
cfg := reconcileStateS3Config(t)
|
|
catalog := pruneCatalogState(time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC))
|
|
writeFakeCatalogState(t, backend, catalog)
|
|
if err := backend.DeleteManagedOutputs(context.Background(), "", []string{"fresh.txt"}, storage.DeleteOptions{}); err != nil {
|
|
t.Fatalf("delete managed output: %v", err)
|
|
}
|
|
testutil.WriteFakeFile(t, backend, "extra.txt", "unmanaged")
|
|
|
|
report, err := reconcileStateConfigWithBackendFactory(context.Background(), cfg, ReconcileStateOptions{
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
DryRun: true,
|
|
}, fakeBackendFactoryProvider(t, map[string]storage.Backend{"s3:reports": backend}))
|
|
if err != nil {
|
|
t.Fatalf("reconcileStateConfigWithBackendFactory() error = %v", err)
|
|
}
|
|
if !report.WouldChange || report.Changed {
|
|
t.Fatalf("report changed=%t would_change=%t, want dry-run pending change", report.Changed, report.WouldChange)
|
|
}
|
|
if got := reportPathList(report.MissingManagedOutputs); got != "fresh.txt" {
|
|
t.Fatalf("missing outputs = %q, want fresh.txt", got)
|
|
}
|
|
if got := entryPathList(report.UnmanagedEntries); got != "extra.txt" {
|
|
t.Fatalf("unmanaged entries = %q, want extra.txt", got)
|
|
}
|
|
repaired := readFakeCatalogState(t, backend)
|
|
if got := strings.Join(state.CatalogManagedOutputPaths(repaired), ","); got != "old.txt,fresh.txt,html.txt" {
|
|
t.Fatalf("state outputs = %q, want original outputs", got)
|
|
}
|
|
}
|
|
|
|
func TestReconcileStateApplyRemovesMissingRecordsAndPreservesUnmanagedFiles(t *testing.T) {
|
|
backend := fake.New()
|
|
cfg := reconcileStateS3Config(t)
|
|
writeFakeCatalogState(t, backend, pruneCatalogState(time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)))
|
|
if err := backend.DeleteManagedOutputs(context.Background(), "", []string{"fresh.txt"}, storage.DeleteOptions{}); err != nil {
|
|
t.Fatalf("delete managed output: %v", err)
|
|
}
|
|
testutil.WriteFakeFile(t, backend, "extra.txt", "unmanaged")
|
|
|
|
report, err := reconcileStateConfigWithBackendFactory(context.Background(), cfg, ReconcileStateOptions{
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
}, fakeBackendFactoryProvider(t, map[string]storage.Backend{"s3:reports": backend}))
|
|
if err != nil {
|
|
t.Fatalf("reconcileStateConfigWithBackendFactory() error = %v", err)
|
|
}
|
|
if !report.Changed || report.WouldChange {
|
|
t.Fatalf("report changed=%t would_change=%t, want applied change", report.Changed, report.WouldChange)
|
|
}
|
|
repaired := readFakeCatalogState(t, backend)
|
|
if err := state.ValidateCatalog(repaired); err != nil {
|
|
t.Fatalf("ValidateCatalog() repaired state error = %v", err)
|
|
}
|
|
if got := strings.Join(state.CatalogManagedOutputPaths(repaired), ","); got != "old.txt,html.txt" {
|
|
t.Fatalf("state outputs = %q, want old.txt,html.txt", got)
|
|
}
|
|
if repaired.SchemaVersion != state.CatalogSchemaVersion {
|
|
t.Fatalf("state schema_version = %d, want %d", repaired.SchemaVersion, state.CatalogSchemaVersion)
|
|
}
|
|
testutil.AssertFakeFile(t, backend, "extra.txt", "unmanaged")
|
|
}
|
|
|
|
func TestReconcileStateInvalidStateFailsWithoutRewrite(t *testing.T) {
|
|
backend := fake.New()
|
|
cfg := reconcileStateS3Config(t)
|
|
invalid := `{"schema_version":2,"pipeline_id":"reports"}`
|
|
testutil.WriteFakeFile(t, backend, storage.StateFileName, invalid)
|
|
|
|
_, err := reconcileStateConfigWithBackendFactory(context.Background(), cfg, ReconcileStateOptions{
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
}, fakeBackendFactoryProvider(t, map[string]storage.Backend{"s3:reports": backend}))
|
|
if err == nil {
|
|
t.Fatal("reconcileStateConfigWithBackendFactory() error = nil, want invalid state error")
|
|
}
|
|
data, readErr := backend.ReadFile(context.Background(), storage.StateFileName)
|
|
if readErr != nil {
|
|
t.Fatalf("read invalid state: %v", readErr)
|
|
}
|
|
if string(data) != invalid {
|
|
t.Fatalf("state data = %q, want original invalid data", data)
|
|
}
|
|
}
|
|
|
|
func TestReconcileStateOwnerScopeRepairsCurrentOwnerOnly(t *testing.T) {
|
|
backend := fake.New()
|
|
cfg := reconcileStateS3Config(t)
|
|
writeFakeCatalogState(t, backend, pruneCatalogState(time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)))
|
|
if err := backend.DeleteManagedOutputs(context.Background(), "", []string{"old.txt", "html.txt"}, storage.DeleteOptions{}); err != nil {
|
|
t.Fatalf("delete managed outputs: %v", err)
|
|
}
|
|
|
|
report, err := reconcileStateConfigWithBackendFactory(context.Background(), cfg, ReconcileStateOptions{
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
}, fakeBackendFactoryProvider(t, map[string]storage.Backend{"s3:reports": backend}))
|
|
if err != nil {
|
|
t.Fatalf("reconcileStateConfigWithBackendFactory() error = %v", err)
|
|
}
|
|
if !report.Changed {
|
|
t.Fatal("report changed = false, want true")
|
|
}
|
|
repaired := readFakeCatalogState(t, backend)
|
|
if got := strings.Join(state.CatalogManagedOutputPaths(repaired), ","); got != "fresh.txt,html.txt" {
|
|
t.Fatalf("catalog outputs = %q, want other owner output preserved", got)
|
|
}
|
|
}
|
|
|
|
func TestReconcileStateAllOwnersRepairsEveryOwner(t *testing.T) {
|
|
backend := fake.New()
|
|
cfg := reconcileStateS3Config(t)
|
|
writeFakeCatalogState(t, backend, pruneCatalogState(time.Date(2026, 6, 8, 12, 0, 0, 0, time.UTC)))
|
|
if err := backend.DeleteManagedOutputs(context.Background(), "", []string{"old.txt", "html.txt"}, storage.DeleteOptions{}); err != nil {
|
|
t.Fatalf("delete managed outputs: %v", err)
|
|
}
|
|
|
|
report, err := reconcileStateConfigWithBackendFactory(context.Background(), cfg, ReconcileStateOptions{
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
AllOwners: true,
|
|
}, fakeBackendFactoryProvider(t, map[string]storage.Backend{"s3:reports": backend}))
|
|
if err != nil {
|
|
t.Fatalf("reconcileStateConfigWithBackendFactory() error = %v", err)
|
|
}
|
|
if !report.Changed || report.CheckedCount != 3 {
|
|
t.Fatalf("report changed=%t checked=%d, want all-owner repair", report.Changed, report.CheckedCount)
|
|
}
|
|
repaired := readFakeCatalogState(t, backend)
|
|
if got := strings.Join(state.CatalogManagedOutputPaths(repaired), ","); got != "fresh.txt" {
|
|
t.Fatalf("catalog outputs = %q, want fresh.txt", got)
|
|
}
|
|
}
|
|
|
|
func reconcileStateS3Config(t *testing.T) config.Config {
|
|
t.Helper()
|
|
cfg := config.Config{Pipelines: []config.Pipeline{{
|
|
ID: "reports",
|
|
Source: config.Backend{Backend: config.BackendLocal, Path: t.TempDir()},
|
|
Destinations: []config.Destination{{
|
|
ID: "archive",
|
|
Backend: config.BackendS3,
|
|
Bucket: "reports",
|
|
}},
|
|
}}}
|
|
config.ApplyDefaults(&cfg)
|
|
return cfg
|
|
}
|
|
|
|
func reportPathList(paths []ReconcileStatePath) string {
|
|
values := make([]string, 0, len(paths))
|
|
for _, path := range paths {
|
|
values = append(values, path.Path)
|
|
}
|
|
return strings.Join(values, ",")
|
|
}
|
|
|
|
func entryPathList(entries []ReconcileStateEntry) string {
|
|
values := make([]string, 0, len(entries))
|
|
for _, entry := range entries {
|
|
values = append(values, entry.Path)
|
|
}
|
|
return strings.Join(values, ",")
|
|
}
|