package publish import ( "context" "strings" "testing" "gitea.maximumdirect.net/eric/distributor/internal/bundle" "gitea.maximumdirect.net/eric/distributor/internal/config" "gitea.maximumdirect.net/eric/distributor/internal/storage" "gitea.maximumdirect.net/eric/distributor/internal/storage/fake" "gitea.maximumdirect.net/eric/distributor/internal/testutil" ) func TestBuildPlansSingleOwnerTakeoverByPolicy(t *testing.T) { tests := []struct { name string takeover config.TakeoverPolicy mutateState func(*bundle.Manifest, *testutil.DestinationStateOptions) wantAction Action wantErr string }{ { name: "default same pipeline different source", takeover: config.TakeoverPolicy{}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { manifest.ID = "other.source" }, wantAction: ActionReplaceTakeover, }, { name: "same pipeline different newer source", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSamePipeline}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { manifest.ID = "other.source" manifest.Created = manifest.Created.AddDate(0, 0, 1) }, wantAction: ActionReplaceTakeover, }, { name: "default same pipeline different destination", takeover: config.TakeoverPolicy{}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { opts.DestinationID = "web" }, wantAction: ActionReplaceTakeover, }, { name: "same pipeline refuses different pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSamePipeline}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { opts.PipelineID = "other" }, wantErr: "fail_conflict", }, { name: "same source allows different pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSameSource}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { opts.PipelineID = "other" }, wantAction: ActionReplaceTakeover, }, { name: "same source refuses different source", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSameSource}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { manifest.ID = "other.source" }, wantErr: "fail_conflict", }, { name: "any managed allows different pipeline", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeAnyManaged}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { manifest.ID = "other.source" opts.PipelineID = "other" }, wantAction: ActionReplaceTakeover, }, { name: "never refuses different source", takeover: config.TakeoverPolicy{Mode: config.TakeoverModeNever}, mutateState: func(manifest *bundle.Manifest, opts *testutil.DestinationStateOptions) { manifest.ID = "other.source" }, wantErr: "fail_conflict", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{}) destinationBackend := fake.New() destinationManifest := sourceBundle.Manifest destinationManifest.Files = append([]bundle.ManifestFile(nil), sourceBundle.Manifest.Files...) opts := testutil.DestinationStateOptions{} tt.mutateState(&destinationManifest, &opts) testutil.WriteFakeDestinationState(t, destinationBackend, "bundle", destinationManifest, opts) req := takeoverRequest(sourceBackend, destinationBackend, sourceBundle, tt.takeover, config.ReconciliationModeReplace) plan, err := Build(context.Background(), req) if tt.wantErr != "" { if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("Build() error = %v, want %q", err, tt.wantErr) } return } if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != tt.wantAction { t.Fatalf("plan action = %s, want %s", plan.Action, tt.wantAction) } }) } } func TestBuildDoesNotTakeOverInvalidOrUnmanagedDestination(t *testing.T) { tests := []struct { name string prepare func(t *testing.T, backend *fake.Backend) wantErr string }{ { name: "invalid state", prepare: func(t *testing.T, backend *fake.Backend) { t.Helper() statePath, err := storage.StatePath("bundle") if err != nil { t.Fatalf("state path: %v", err) } testutil.WriteFakeFile(t, backend, statePath, "{invalid") }, wantErr: "fail_conflict", }, { name: "unmanaged content", prepare: func(t *testing.T, backend *fake.Backend) { t.Helper() testutil.WriteFakeFile(t, backend, "bundle/old.txt", "old") }, wantErr: "fail_unmanaged", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{}) destinationBackend := fake.New() tt.prepare(t, destinationBackend) req := takeoverRequest(sourceBackend, destinationBackend, sourceBundle, config.TakeoverPolicy{Mode: config.TakeoverModeAnyManaged}, config.ReconciliationModeReplace) _, err := Build(context.Background(), req) if err == nil || !strings.Contains(err.Error(), tt.wantErr) { t.Fatalf("Build() error = %v, want %q", err, tt.wantErr) } }) } } func TestExecuteTakeoverMergeDoesNotRetainOldSourceOutputs(t *testing.T) { sourceBackend := fake.New() sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{ Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\nNew.\n"}}, }) destinationBackend := fake.New() oldManifest := sourceBundle.Manifest oldManifest.ID = "old.source" oldManifest.Files = []bundle.ManifestFile{ {Path: "report.md", SHA256: bundle.FileDigest([]byte("old\n")), Size: int64(len("old\n"))}, {Path: "summary.txt", SHA256: bundle.FileDigest([]byte("old summary\n")), Size: int64(len("old summary\n"))}, } oldManifest.Digest = bundle.BundleDigest(oldManifest.Files) testutil.WriteFakeDestinationState(t, destinationBackend, "bundle", oldManifest, testutil.DestinationStateOptions{}) req := takeoverRequest(sourceBackend, destinationBackend, sourceBundle, config.TakeoverPolicy{Mode: config.TakeoverModeSamePipeline}, config.ReconciliationModeMerge) plan, err := Build(context.Background(), req) if err != nil { t.Fatalf("Build() error = %v", err) } if plan.Action != ActionReplaceTakeover { t.Fatalf("plan action = %s, want %s", plan.Action, ActionReplaceTakeover) } if err := Execute(context.Background(), req, plan); err != nil { t.Fatalf("Execute() error = %v", err) } testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\nNew.\n") testutil.AssertFakeMissing(t, destinationBackend, "bundle/summary.txt") destinationState := readFakeState(t, destinationBackend, "bundle") if got, want := len(destinationState.Outputs), 1; got != want { t.Fatalf("state output count = %d, want %d", got, want) } if got, want := destinationState.Source.Manifest.ID, sourceBundle.Manifest.ID; got != want { t.Fatalf("state source id = %q, want %q", got, want) } } func takeoverRequest(sourceBackend, destinationBackend *fake.Backend, sourceBundle bundle.Bundle, takeover config.TakeoverPolicy, reconciliationMode string) Request { return Request{ PipelineID: "reports", DestinationID: "archive", SourceBundle: sourceBundle, SourceBackend: sourceBackend, DestinationBackend: destinationBackend, DestinationBundlePath: sourceBundle.RootRelativePath, Publish: config.PublishPolicy{Source: true}, Reconciliation: config.ReconciliationPolicy{Mode: reconciliationMode}, Takeover: takeover, Transfer: defaultTransfer(), DistributorVersion: "test", } }