622 lines
25 KiB
Go
622 lines
25 KiB
Go
package publish
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"gitea.maximumdirect.net/eric/distributor/internal/bundle"
|
|
"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 TestBuildSharedRootTreatsAbsentOwnerAsPublishable(t *testing.T) {
|
|
sourceBackend := fake.New()
|
|
sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{
|
|
Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}},
|
|
})
|
|
destinationBackend := fake.New()
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, false))
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
plan, err := Build(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
if plan.Action != ActionPublishNew {
|
|
t.Fatalf("plan action = %s, want publish_new", plan.Action)
|
|
}
|
|
if plan.OwnerScope != state.CurrentOwnerScope("reports", "archive") {
|
|
t.Fatalf("owner scope = %#v, want reports/archive", plan.OwnerScope)
|
|
}
|
|
if got, want := len(plan.OtherOwnerOutputs), 1; got != want {
|
|
t.Fatalf("other owner output count = %d, want %d", got, want)
|
|
}
|
|
if got, want := len(plan.OwnerOutputsToWrite), 1; got != want {
|
|
t.Fatalf("owner output write count = %d, want %d", got, want)
|
|
}
|
|
if len(plan.OwnerOutputsToDelete) != 0 || len(plan.RetainedOwnerOutputs) != 0 {
|
|
t.Fatalf("delete=%#v retained=%#v, want none", plan.OwnerOutputsToDelete, plan.RetainedOwnerOutputs)
|
|
}
|
|
}
|
|
|
|
func TestBuildSharedRootReplaceDeletesOnlyCurrentOwnerOmittedOutputs(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()
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, true))
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
plan, err := Build(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
if plan.Action != ActionReplaceOlder {
|
|
t.Fatalf("plan action = %s, want replace_older", plan.Action)
|
|
}
|
|
if got, want := sharedRootOutputPathList(plan.OwnerOutputsToDelete), "old.md"; got != want {
|
|
t.Fatalf("owner outputs to delete = %q, want %q", got, want)
|
|
}
|
|
if got, want := sharedRootOutputPathList(plan.OtherOwnerOutputs), "other/report.md"; got != want {
|
|
t.Fatalf("other owner outputs = %q, want %q", got, want)
|
|
}
|
|
if len(plan.RetainedOwnerOutputs) != 0 {
|
|
t.Fatalf("retained owner outputs = %#v, want none", plan.RetainedOwnerOutputs)
|
|
}
|
|
}
|
|
|
|
func TestBuildSharedRootMergeRetainsCurrentOwnerOmittedOutputs(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()
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, true))
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeMerge)
|
|
plan, err := Build(context.Background(), req)
|
|
if err != nil {
|
|
t.Fatalf("Build() error = %v", err)
|
|
}
|
|
if got, want := sharedRootOutputPathList(plan.RetainedOwnerOutputs), "old.md"; got != want {
|
|
t.Fatalf("retained owner outputs = %q, want %q", got, want)
|
|
}
|
|
if len(plan.OwnerOutputsToDelete) != 0 {
|
|
t.Fatalf("owner outputs to delete = %#v, want none", plan.OwnerOutputsToDelete)
|
|
}
|
|
}
|
|
|
|
func TestBuildSharedRootRejectsOtherOwnerPathConflict(t *testing.T) {
|
|
sourceBackend := fake.New()
|
|
sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{
|
|
Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}},
|
|
})
|
|
destinationBackend := fake.New()
|
|
sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false)
|
|
sharedRoot.Outputs[0].Path = "report.md"
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot)
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
plan, err := Build(context.Background(), req)
|
|
if err == nil || !strings.Contains(err.Error(), "fail_conflict") {
|
|
t.Fatalf("Build() error = %v, want fail_conflict", err)
|
|
}
|
|
if plan.Action != ActionFailConflict {
|
|
t.Fatalf("plan action = %s, want fail_conflict", plan.Action)
|
|
}
|
|
}
|
|
|
|
func TestBuildSharedRootPlansOutputTakeoverByPolicy(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
takeover config.TakeoverPolicy
|
|
ownerScope state.OwnerScope
|
|
sameSource bool
|
|
wantAction Action
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "default same pipeline allows different destination",
|
|
takeover: config.TakeoverPolicy{},
|
|
ownerScope: state.CurrentOwnerScope("reports", "web"),
|
|
wantAction: ActionReplaceTakeover,
|
|
},
|
|
{
|
|
name: "same pipeline refuses different pipeline",
|
|
takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSamePipeline},
|
|
ownerScope: state.CurrentOwnerScope("other", "archive"),
|
|
wantErr: "fail_conflict",
|
|
},
|
|
{
|
|
name: "same source allows different pipeline",
|
|
takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSameSource},
|
|
ownerScope: state.CurrentOwnerScope("other", "archive"),
|
|
sameSource: true,
|
|
wantAction: ActionReplaceTakeover,
|
|
},
|
|
{
|
|
name: "same source refuses different source",
|
|
takeover: config.TakeoverPolicy{Mode: config.TakeoverModeSameSource},
|
|
ownerScope: state.CurrentOwnerScope("reports", "web"),
|
|
wantErr: "fail_conflict",
|
|
},
|
|
{
|
|
name: "any managed allows different pipeline",
|
|
takeover: config.TakeoverPolicy{Mode: config.TakeoverModeAnyManaged},
|
|
ownerScope: state.CurrentOwnerScope("other", "archive"),
|
|
wantAction: ActionReplaceTakeover,
|
|
},
|
|
{
|
|
name: "never refuses same pipeline",
|
|
takeover: config.TakeoverPolicy{Mode: config.TakeoverModeNever},
|
|
ownerScope: state.CurrentOwnerScope("reports", "web"),
|
|
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{
|
|
Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}},
|
|
})
|
|
destinationBackend := fake.New()
|
|
sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false)
|
|
ownerManifest := sharedRoot.Owners[0].Source.Manifest
|
|
if tt.sameSource {
|
|
ownerManifest = sourceBundle.Manifest
|
|
}
|
|
setSharedRootOwnerOutput(t, &sharedRoot, 0, tt.ownerScope, ownerManifest, "report.md")
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot)
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
req.Takeover = tt.takeover
|
|
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)
|
|
}
|
|
if plan.Action != ActionFailConflict {
|
|
t.Fatalf("plan action = %s, want fail_conflict", plan.Action)
|
|
}
|
|
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)
|
|
}
|
|
if got, want := sharedRootOutputPathList(plan.TakenOverOwnerOutputs), "report.md"; got != want {
|
|
t.Fatalf("taken over outputs = %q, want %q", got, want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBuildSharedRootRejectsUnmanagedPathCollision(t *testing.T) {
|
|
sourceBackend := fake.New()
|
|
sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{
|
|
Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}},
|
|
})
|
|
destinationBackend := fake.New()
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, false))
|
|
testutil.WriteFakeFile(t, destinationBackend, "bundle/report.md", "unmanaged")
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
plan, err := Build(context.Background(), req)
|
|
if err == nil || !strings.Contains(err.Error(), "fail_unmanaged") {
|
|
t.Fatalf("Build() error = %v, want fail_unmanaged", err)
|
|
}
|
|
if plan.Action != ActionFailUnmanaged {
|
|
t.Fatalf("plan action = %s, want fail_unmanaged", plan.Action)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSharedRootPublishesOwnerAndPreservesOtherOwners(t *testing.T) {
|
|
sourceBackend := fake.New()
|
|
sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{
|
|
Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}},
|
|
})
|
|
destinationBackend := fake.New()
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, false))
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
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, "bundle/report.md", "# Report\n")
|
|
testutil.AssertFakeFile(t, destinationBackend, "bundle/other/report.md", "old")
|
|
destinationState := readFakeSharedRootState(t, destinationBackend, "bundle")
|
|
if got, want := len(destinationState.Owners), 2; got != want {
|
|
t.Fatalf("owner count = %d, want %d", got, want)
|
|
}
|
|
if got, want := destinationState.State.Mode, state.StateModeSharedRoot; got != want {
|
|
t.Fatalf("state mode = %q, want %q", got, want)
|
|
}
|
|
if _, ok := destinationState.Owner(state.CurrentOwnerScope("other", "archive")); !ok {
|
|
t.Fatal("other owner missing from shared-root state")
|
|
}
|
|
if _, ok := destinationState.Owner(state.CurrentOwnerScope("reports", "archive")); !ok {
|
|
t.Fatal("current owner missing from shared-root state")
|
|
}
|
|
if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "other/report.md,report.md"; got != want {
|
|
t.Fatalf("managed paths = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSharedRootReplaceDeletesOnlyCurrentOwnerOmittedOutputs(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()
|
|
existing := sharedRootStateWithOwners(t, sourceBundle.Manifest, true)
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", existing)
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
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, "bundle/report.md", "# Report\nNew.\n")
|
|
testutil.AssertFakeMissing(t, destinationBackend, "bundle/old.md")
|
|
testutil.AssertFakeFile(t, destinationBackend, "bundle/other/report.md", "old")
|
|
destinationState := readFakeSharedRootState(t, destinationBackend, "bundle")
|
|
if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "other/report.md,report.md"; got != want {
|
|
t.Fatalf("managed paths = %q, want %q", got, want)
|
|
}
|
|
if !destinationState.CreatedAt.Equal(existing.CreatedAt) {
|
|
t.Fatalf("created_at = %s, want %s", destinationState.CreatedAt, existing.CreatedAt)
|
|
}
|
|
output, ok := findSharedRootOutputForTest(destinationState.Outputs, "report.md")
|
|
if !ok {
|
|
t.Fatal("report.md missing from shared-root outputs")
|
|
}
|
|
if !output.CreatedAt.Equal(existing.Outputs[1].CreatedAt) || !output.UpdatedAt.After(existing.Outputs[1].UpdatedAt) {
|
|
t.Fatalf("report.md timestamps = created:%s updated:%s, want preserved created and newer updated", output.CreatedAt, output.UpdatedAt)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSharedRootTakeoverReassignsPathAndPreservesUnrelatedOutputs(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()
|
|
sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false)
|
|
previousScope := state.CurrentOwnerScope("reports", "web")
|
|
setSharedRootOwnerOutput(t, &sharedRoot, 0, previousScope, sharedRoot.Owners[0].Source.Manifest, "report.md")
|
|
keepOutput := sharedRoot.Outputs[0]
|
|
keepOutput.Path = "web/keep.md"
|
|
keepOutput.SourcePath = "web/keep.md"
|
|
sharedRoot.Outputs = append(sharedRoot.Outputs, keepOutput)
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot)
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
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.AssertFakeFile(t, destinationBackend, "bundle/web/keep.md", "old")
|
|
destinationState := readFakeSharedRootState(t, destinationBackend, "bundle")
|
|
reportOutput, ok := findSharedRootOutputForTest(destinationState.Outputs, "report.md")
|
|
if !ok {
|
|
t.Fatal("report.md missing from shared-root outputs")
|
|
}
|
|
if reportOutput.Owner != state.CurrentOwnerScope("reports", "archive") {
|
|
t.Fatalf("report.md owner = %#v, want reports/archive", reportOutput.Owner)
|
|
}
|
|
keep, ok := findSharedRootOutputForTest(destinationState.Outputs, "web/keep.md")
|
|
if !ok {
|
|
t.Fatal("web/keep.md missing from shared-root outputs")
|
|
}
|
|
if keep.Owner != previousScope {
|
|
t.Fatalf("web/keep.md owner = %#v, want reports/web", keep.Owner)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSharedRootTakeoverMergeDoesNotRetainOldSourceOutputs(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()
|
|
sharedRoot := sharedRootStateWithOwners(t, sourceBundle.Manifest, false)
|
|
previousScope := state.CurrentOwnerScope("reports", "web")
|
|
setSharedRootOwnerOutput(t, &sharedRoot, 0, previousScope, sharedRoot.Owners[0].Source.Manifest, "report.md")
|
|
|
|
createdAt := sharedRoot.CreatedAt
|
|
oldManifest := sourceBundle.Manifest
|
|
oldManifest.ID = "old.source"
|
|
oldManifest.Created = oldManifest.Created.Add(-time.Hour)
|
|
oldManifest.Files = []bundle.ManifestFile{{
|
|
Path: "old.md",
|
|
SHA256: bundle.FileDigest([]byte("old\n")),
|
|
Size: int64(len("old\n")),
|
|
}}
|
|
oldManifest.Digest = bundle.BundleDigest(oldManifest.Files)
|
|
currentScope := state.CurrentOwnerScope("reports", "archive")
|
|
sharedRoot.Owners = append(sharedRoot.Owners, state.OwnerRecord{
|
|
Scope: currentScope,
|
|
Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeMerge},
|
|
Source: state.SourceState{Manifest: oldManifest},
|
|
})
|
|
sharedRoot.Outputs = append(sharedRoot.Outputs, state.SharedRootOutputFile{
|
|
Path: "old.md",
|
|
Kind: state.OutputKindSource,
|
|
SourcePath: "old.md",
|
|
SHA256: oldManifest.Files[0].SHA256,
|
|
Size: oldManifest.Files[0].Size,
|
|
Owner: currentScope,
|
|
SourceID: oldManifest.ID,
|
|
SourceDigest: oldManifest.Digest,
|
|
SourceCreated: oldManifest.Created,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: createdAt,
|
|
})
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRoot)
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeMerge)
|
|
req.PathMapping = config.PathMappingFixed
|
|
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 got, want := sharedRootOutputPathList(plan.OwnerOutputsToDelete), "old.md"; got != want {
|
|
t.Fatalf("owner outputs to delete = %q, want %q", got, want)
|
|
}
|
|
if len(plan.RetainedOwnerOutputs) != 0 {
|
|
t.Fatalf("retained owner outputs = %#v, want none", plan.RetainedOwnerOutputs)
|
|
}
|
|
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/old.md")
|
|
destinationState := readFakeSharedRootState(t, destinationBackend, "bundle")
|
|
if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "report.md"; got != want {
|
|
t.Fatalf("managed paths = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSharedRootMergeRetainsCurrentOwnerOmittedOutputs(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()
|
|
writeFakeSharedRootState(t, destinationBackend, "bundle", sharedRootStateWithOwners(t, sourceBundle.Manifest, true))
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeMerge)
|
|
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, "bundle/report.md", "# Report\nNew.\n")
|
|
testutil.AssertFakeFile(t, destinationBackend, "bundle/old.md", "old")
|
|
testutil.AssertFakeFile(t, destinationBackend, "bundle/other/report.md", "old")
|
|
destinationState := readFakeSharedRootState(t, destinationBackend, "bundle")
|
|
if got, want := strings.Join(destinationState.AllManagedOutputPaths(), ","), "other/report.md,report.md,old.md"; got != want {
|
|
t.Fatalf("managed paths = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestExecuteSharedRootForceReplaceDeletesOnlyBundlePath(t *testing.T) {
|
|
sourceBackend := fake.New()
|
|
sourceBundle := testutil.WriteFakeSourceBundle(t, sourceBackend, "bundle", testutil.BundleOptions{
|
|
Files: []testutil.SourceFile{{Path: "report.md", Data: "# Report\n"}},
|
|
})
|
|
destinationBackend := fake.New()
|
|
testutil.WriteFakeFile(t, destinationBackend, "bundle/unmanaged.txt", "unmanaged")
|
|
testutil.WriteFakeFile(t, destinationBackend, "bundle/nested/old.txt", "old")
|
|
testutil.WriteFakeFile(t, destinationBackend, "bundle-sibling/keep.txt", "keep")
|
|
|
|
req := sharedRootRequest(sourceBackend, destinationBackend, sourceBundle, config.ReconciliationModeReplace)
|
|
req.Force = true
|
|
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 force_replace", plan.Action)
|
|
}
|
|
if err := Execute(context.Background(), req, plan); err != nil {
|
|
t.Fatalf("Execute() error = %v", err)
|
|
}
|
|
testutil.AssertFakeFile(t, destinationBackend, "bundle/report.md", "# Report\n")
|
|
testutil.AssertFakeMissing(t, destinationBackend, "bundle/unmanaged.txt")
|
|
testutil.AssertFakeMissing(t, destinationBackend, "bundle/nested/old.txt")
|
|
testutil.AssertFakeFile(t, destinationBackend, "bundle-sibling/keep.txt", "keep")
|
|
destinationState := readFakeSharedRootState(t, destinationBackend, "bundle")
|
|
if got, want := len(destinationState.Owners), 1; got != want {
|
|
t.Fatalf("owner count = %d, want %d", got, want)
|
|
}
|
|
}
|
|
|
|
func sharedRootRequest(sourceBackend, destinationBackend *fake.Backend, sourceBundle bundle.Bundle, reconciliationMode string) Request {
|
|
return Request{
|
|
PipelineID: "reports",
|
|
DestinationID: "archive",
|
|
SourceBundle: sourceBundle,
|
|
SourceBackend: sourceBackend,
|
|
DestinationBackend: destinationBackend,
|
|
DestinationBundlePath: sourceBundle.RootRelativePath,
|
|
Publish: config.PublishPolicy{Source: true},
|
|
State: config.StatePolicy{Mode: config.StateModeSharedRoot},
|
|
Reconciliation: config.ReconciliationPolicy{Mode: reconciliationMode},
|
|
Transfer: defaultTransfer(),
|
|
DistributorVersion: "test",
|
|
}
|
|
}
|
|
|
|
func writeFakeSharedRootState(t *testing.T, backend *fake.Backend, relative string, sharedRoot state.SharedRootState) {
|
|
t.Helper()
|
|
data, err := json.MarshalIndent(sharedRoot, "", " ")
|
|
if err != nil {
|
|
t.Fatalf("marshal shared-root state: %v", err)
|
|
}
|
|
statePath, err := storage.StatePath(relative)
|
|
if err != nil {
|
|
t.Fatalf("state path: %v", err)
|
|
}
|
|
testutil.WriteFakeFile(t, backend, statePath, string(append(data, '\n')))
|
|
for _, output := range sharedRoot.Outputs {
|
|
path, err := storage.Join(relative, output.Path)
|
|
if err != nil {
|
|
t.Fatalf("join output path: %v", err)
|
|
}
|
|
testutil.WriteFakeFile(t, backend, path, "old")
|
|
}
|
|
}
|
|
|
|
func readFakeSharedRootState(t *testing.T, backend *fake.Backend, relative string) state.SharedRootState {
|
|
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 shared-root state: %v", err)
|
|
}
|
|
destinationState, err := state.ParseSharedRoot(data)
|
|
if err != nil {
|
|
t.Fatalf("parse shared-root state: %v", err)
|
|
}
|
|
return destinationState
|
|
}
|
|
|
|
func findSharedRootOutputForTest(outputs []state.SharedRootOutputFile, path string) (state.SharedRootOutputFile, bool) {
|
|
for _, output := range outputs {
|
|
if output.Path == path {
|
|
return output, true
|
|
}
|
|
}
|
|
return state.SharedRootOutputFile{}, false
|
|
}
|
|
|
|
func setSharedRootOwnerOutput(t *testing.T, sharedRoot *state.SharedRootState, index int, scope state.OwnerScope, manifest bundle.Manifest, path string) {
|
|
t.Helper()
|
|
sharedRoot.Owners[index].Scope = scope
|
|
sharedRoot.Owners[index].Source = state.SourceState{Manifest: manifest}
|
|
sourcePath := path
|
|
if len(manifest.Files) > 0 {
|
|
sourcePath = manifest.Files[0].Path
|
|
}
|
|
sharedRoot.Outputs[index].Path = path
|
|
sharedRoot.Outputs[index].SourcePath = sourcePath
|
|
sharedRoot.Outputs[index].Owner = scope
|
|
sharedRoot.Outputs[index].SourceID = manifest.ID
|
|
sharedRoot.Outputs[index].SourceDigest = manifest.Digest
|
|
sharedRoot.Outputs[index].SourceCreated = manifest.Created
|
|
if len(manifest.Files) > 0 {
|
|
sharedRoot.Outputs[index].SHA256 = manifest.Files[0].SHA256
|
|
sharedRoot.Outputs[index].Size = manifest.Files[0].Size
|
|
}
|
|
}
|
|
|
|
func sharedRootStateWithOwners(t *testing.T, current bundle.Manifest, includeCurrent bool) state.SharedRootState {
|
|
t.Helper()
|
|
createdAt := time.Date(2026, 5, 30, 11, 12, 0, 0, time.UTC)
|
|
otherManifest := testutil.ValidManifest(testutil.BundleOptions{
|
|
ID: "other.source",
|
|
Files: []testutil.SourceFile{{Path: "other/report.md", Data: "# Other\n"}},
|
|
})
|
|
sharedRoot := state.SharedRootState{
|
|
SchemaVersion: state.SharedRootSchemaVersion,
|
|
DistributorVersion: "test",
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: createdAt,
|
|
State: state.StatePolicy{Mode: state.StateModeSharedRoot},
|
|
Owners: []state.OwnerRecord{{
|
|
Scope: state.CurrentOwnerScope("other", "archive"),
|
|
Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace},
|
|
Source: state.SourceState{Manifest: otherManifest},
|
|
}},
|
|
Outputs: []state.SharedRootOutputFile{{
|
|
Path: "other/report.md",
|
|
Kind: state.OutputKindSource,
|
|
SourcePath: "other/report.md",
|
|
SHA256: otherManifest.Files[0].SHA256,
|
|
Size: otherManifest.Files[0].Size,
|
|
Owner: state.CurrentOwnerScope("other", "archive"),
|
|
SourceID: otherManifest.ID,
|
|
SourceDigest: otherManifest.Digest,
|
|
SourceCreated: otherManifest.Created,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: createdAt,
|
|
}},
|
|
}
|
|
if !includeCurrent {
|
|
return sharedRoot
|
|
}
|
|
older := current
|
|
older.Created = older.Created.Add(-time.Hour)
|
|
older.Files = append([]bundle.ManifestFile(nil), current.Files...)
|
|
older.Files = append(older.Files, bundle.ManifestFile{
|
|
Path: "old.md",
|
|
SHA256: bundle.FileDigest([]byte("old\n")),
|
|
Size: int64(len("old\n")),
|
|
})
|
|
older.Digest = bundle.BundleDigest(older.Files)
|
|
scope := state.CurrentOwnerScope("reports", "archive")
|
|
sharedRoot.Owners = append(sharedRoot.Owners, state.OwnerRecord{
|
|
Scope: scope,
|
|
Reconciliation: state.ReconciliationPolicy{Mode: config.ReconciliationModeReplace},
|
|
Source: state.SourceState{Manifest: older},
|
|
})
|
|
for _, file := range older.Files {
|
|
sharedRoot.Outputs = append(sharedRoot.Outputs, state.SharedRootOutputFile{
|
|
Path: file.Path,
|
|
Kind: state.OutputKindSource,
|
|
SourcePath: file.Path,
|
|
SHA256: file.SHA256,
|
|
Size: file.Size,
|
|
Owner: scope,
|
|
SourceID: older.ID,
|
|
SourceDigest: older.Digest,
|
|
SourceCreated: older.Created,
|
|
CreatedAt: createdAt,
|
|
UpdatedAt: createdAt,
|
|
})
|
|
}
|
|
return sharedRoot
|
|
}
|
|
|
|
func sharedRootOutputPathList(outputs []state.SharedRootOutputFile) string {
|
|
paths := make([]string, 0, len(outputs))
|
|
for _, output := range outputs {
|
|
paths = append(paths, output.Path)
|
|
}
|
|
return strings.Join(paths, ",")
|
|
}
|