package hostops import ( "context" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/lease" "github.com/carlosframework/platform/internal/lease/leasetest" "github.com/carlosframework/platform/internal/release" ) func TestPlan9B8VersionedRecordWritesCannotReopenSequence(t *testing.T) { r := newResolveFixture(t, false) ctx := context.Background() if got, err := ResolveMoveStart(ctx, r.h, r.moving, moveStartID); err != nil || got != OutcomeNotApplied { t.Fatalf("resolve: %q %v", got, err) } versions := leasetest.NewVersionedBucket() versions.AddBucket("control") old := versions.Put("control", r.raw.recordKey, r.before) guarded, err := NewVersionedBoundary(true, versions, r.raw, r.w, "control", nil, nil) if err != nil { t.Fatal(err) } issued, settle, err := r.h.Runner(r.w.now).Issue(ctx, Effect{ID: "versions", Resource: r.raw.recordKey, Action: "test-versions"}) if err != nil { t.Fatal(err) } defer func() { _ = settle("", ErrNotApplied) }() if _, err = guarded.PutIfAbsent(issued, "control", r.raw.recordKey, r.before); !errors.Is(err, ErrContract) { t.Fatalf("PutIfAbsent: %v", err) } if _, _, err = guarded.CopyVersion(issued, "control", r.raw.recordKey, old, "control", r.raw.recordKey, false); !errors.Is(err, ErrContract) { t.Fatalf("CopyVersion: %v", err) } if err = guarded.DeleteVersion(issued, "control", r.raw.recordKey, old); !errors.Is(err, ErrContract) { t.Fatalf("DeleteVersion: %v", err) } retained, err := versions.GetVersion(ctx, "control", r.raw.recordKey, old) if err != nil || string(retained) != string(r.before) { t.Fatalf("old version changed: %v", err) } // Even the private sweep scope cannot delete a live record's version. sweep := context.WithValue(issued, retiredRecordSweepKey{}, r.raw.recordKey) if err = guarded.DeleteVersion(sweep, "control", r.raw.recordKey, old); !errors.Is(err, ErrPending) { t.Fatalf("live sweep: %v", err) } } func TestPlan9B8PurgeSweepRechecksRetiredRecord(t *testing.T) { ctx := context.Background() control := release.NewDirStore(t.TempDir()) versions := leasetest.NewVersionedBucket() versions.AddBucket("control") key := "apps/bab/demo/instances/shop.example.com.json" old := versions.Put("control", key, []byte(`{"record_seq":1}`)) journal := PurgeJournal{V: 1, Purge: Purge{ID: "p", Phase: "retired"}, AppID: "old", Account: "bab", App: "demo", Hosts: map[string]PurgeHost{}} w := seedPurge(t, ctx, control, time.Now(), journal) if err := PreparePurgeSweep(ctx, control, w, versions, "control", "bab", "demo", "p", "old"); err != nil { t.Fatal(err) } tag, err := control.Put(ctx, key, []byte(`{"record_seq":2}`), "") if err != nil { t.Fatal(err) } if err := DeletePurgeSweep(ctx, control, w, versions, "control", "bab", "demo", "p", "old"); !errors.Is(err, ErrPending) { t.Fatalf("late live record: %v", err) } if _, err := versions.GetVersion(ctx, "control", key, old); err != nil { t.Fatalf("live version removed: %v", err) } if err := release.DeleteIfMatch(ctx, control, key, tag); err != nil { t.Fatal(err) } if err := DeletePurgeSweep(ctx, control, w, versions, "control", "bab", "demo", "p", "old"); err != nil { t.Fatal(err) } if _, err := versions.GetVersion(ctx, "control", key, old); !errors.Is(err, lease.ErrMirrorNotFound) { t.Fatalf("retired version not swept: %v", err) } } func TestPlan9B8FenceRefusesChangedClaim(t *testing.T) { r := newResolveFixture(t, false) ctx := context.Background() if _, err := hostclaim.Update(r.h.Lifecycle(ctx), r.data, r.moving, hostclaim.ReleaseMove(moveStartID, "ie-box")); err != nil { t.Fatal(err) } if got, err := ResolveMoveStart(ctx, r.h, r.moving, moveStartID); got != OutcomeUncertain || !errors.Is(err, hostclaim.ErrConflict) { t.Fatalf("stale fence: %q %v", got, err) } if r.control.fences != 0 || r.effect(t).State != "uncertain" { t.Fatal("stale observation fenced or settled") } } type sweepWithoutResurrection struct { *leasetest.VersionedBucket t *testing.T key string } func (v sweepWithoutResurrection) DeleteVersion(ctx context.Context, bucket, key, id string) error { if err := v.VersionedBucket.DeleteVersion(ctx, bucket, key, id); err != nil { return err } if _, live := v.Current(bucket, v.key); live { v.t.Fatal("sweep resurrected a record body") } return nil } func TestPlan9B8PurgeRemovesRecordMarkersLast(t *testing.T) { ctx := context.Background() control := release.NewDirStore(t.TempDir()) versions := leasetest.NewVersionedBucket() versions.AddBucket("control") key := "apps/bab/demo/instances/shop.example.com.json" versions.Put("control", key, []byte(`{"record_seq":1}`)) versions.BareDelete("control", key) versions.Put("control", key, []byte(`{"record_seq":2}`)) marker := versions.BareDelete("control", key) if err := retiredRecordVersion(ctx, control, versions, "control", key, marker); !errors.Is(err, ErrPending) { t.Fatalf("marker could expose old body: %v", err) } journal := PurgeJournal{V: 1, Purge: Purge{ID: "p", Phase: "retired"}, AppID: "old", Account: "bab", App: "demo", Hosts: map[string]PurgeHost{}} w := seedPurge(t, ctx, control, time.Now(), journal) guarded := sweepWithoutResurrection{versions, t, key} if err := PreparePurgeSweep(ctx, control, w, guarded, "control", "bab", "demo", "p", "old"); err != nil { t.Fatal(err) } if err := DeletePurgeSweep(ctx, control, w, guarded, "control", "bab", "demo", "p", "old"); err != nil { t.Fatal(err) } all, err := versions.ListVersions(ctx, "control", key) if err != nil || len(all) != 0 { t.Fatalf("retained versions: %+v %v", all, err) } }