| 1 | //go:build unix |
| 2 | |
| 3 | package hostguard |
| 4 | |
| 5 | import ( |
| 6 | "bytes" |
| 7 | "context" |
| 8 | "errors" |
| 9 | "testing" |
| 10 | "time" |
| 11 | |
| 12 | "github.com/carlosframework/platform/internal/hostclaim" |
| 13 | ) |
| 14 | |
| 15 | // B3 (roster binding): an obsolete cold handover barrier is settled without |
| 16 | // a claim session advance (the live successor admission still holds the |
| 17 | // claim), under the host lock, only for a prior with a completed receipt. |
| 18 | // Settlement undoes BeginColdHandover (the deny slot back to the prior's |
| 19 | // completed fence), retires the successor's stamp so it is never installed, |
| 20 | // runs record after the guard write, and is idempotent. |
| 21 | func TestSettleObsoleteColdHandover(t *testing.T) { |
| 22 | f, prior, captured := fenceFixture(t) |
| 23 | f.obs.Claim.Holder.Account, f.obs.Claim.Holder.App = "bab", "console" |
| 24 | store := &rootTestStore{dir: t.TempDir()} |
| 25 | f.g.Root = NewRootAuthority(store) |
| 26 | in := RootInstallation{Host: prior.Host, Incarnation: captured.Incarnation, Stamp: captured.Stamp, Account: "bab", App: "console", Record: "record", Box: prior.Box, Enrollment: prior.Enrollment, Migration: 1} |
| 27 | if err := f.g.EstablishRoot(t.Context(), in, allow); err != nil { |
| 28 | t.Fatal(err) |
| 29 | } |
| 30 | f.obs.Claim.Op.Kind = hostclaim.KindMoving |
| 31 | if _, err := f.g.BeginFence(t.Context(), prior); err != nil { |
| 32 | t.Fatal(err) |
| 33 | } |
| 34 | if err := f.g.CompleteFence(t.Context(), prior, []byte("receipt"), time.Now(), func(context.Context, FenceTarget, []byte, time.Time) error { return nil }); err != nil { |
| 35 | t.Fatal(err) |
| 36 | } |
| 37 | barrier := prior |
| 38 | barrier.FenceID, barrier.Stamp, barrier.Session, barrier.DirectoryRevision = "barrier", 17, 17, "obsolete-binding" |
| 39 | f.obs.Claim.Holder.Stamp, f.obs.Claim.Holder.Session = 17, 17 |
| 40 | if err := f.g.BeginColdHandover(t.Context(), prior, barrier, allow); err != nil { |
| 41 | t.Fatal(err) |
| 42 | } |
| 43 | if err := f.g.AbandonColdHandover(t.Context(), barrier); !errors.Is(err, ErrFenced) { |
| 44 | t.Fatal("the session-advance abandonment applies here:", err) |
| 45 | } |
| 46 | records := 0 |
| 47 | record := func() error { records++; return nil } |
| 48 | |
| 49 | other := barrier |
| 50 | other.FenceID = "other" |
| 51 | if err := f.g.SettleObsoleteColdHandover(t.Context(), prior, other, record); !errors.Is(err, ErrFenceTarget) { |
| 52 | t.Fatalf("another barrier: %v", err) |
| 53 | } |
| 54 | unfenced := prior |
| 55 | unfenced.FenceID = "never-completed" |
| 56 | if err := f.g.SettleObsoleteColdHandover(t.Context(), unfenced, barrier, record); !errors.Is(err, ErrNoFenceReceipt) { |
| 57 | t.Fatalf("a prior without a completed receipt: %v", err) |
| 58 | } |
| 59 | if records != 0 { |
| 60 | t.Fatal("a refused settlement recorded") |
| 61 | } |
| 62 | |
| 63 | // A failed record leaves the guard settled; the retry only records. |
| 64 | failed := errors.New("journal write failed") |
| 65 | if err := f.g.SettleObsoleteColdHandover(t.Context(), prior, barrier, func() error { return failed }); !errors.Is(err, failed) { |
| 66 | t.Fatal(err) |
| 67 | } |
| 68 | r, err := f.g.Root.read(prior.Host) |
| 69 | if err != nil || r.barrier != nil || r.ActiveFence == nil || *r.ActiveFence != prior || r.ActiveStamp != prior.Stamp || len(r.FenceJournal) != 1 || r.FenceJournal[0].Target != prior { |
| 70 | t.Fatalf("after settlement: %+v %v", r, err) |
| 71 | } |
| 72 | if len(r.rootRetired) != 1 || r.rootRetired[0] != (Captured{Incarnation: barrier.Incarnation, Stamp: barrier.Stamp}) { |
| 73 | t.Fatalf("successor stamp not retired: %v", r.rootRetired) |
| 74 | } |
| 75 | before, _ := store.Read(prior.Host + ".json") |
| 76 | if err := f.g.SettleObsoleteColdHandover(t.Context(), prior, barrier, record); err != nil || records != 1 { |
| 77 | t.Fatalf("idempotent settlement: %v, records %d", err, records) |
| 78 | } |
| 79 | if after, _ := store.Read(prior.Host + ".json"); !bytes.Equal(before, after) { |
| 80 | t.Fatal("a repeated settlement rewrote the record") |
| 81 | } |
| 82 | |
| 83 | // The successor is never installed; the prior stays cleanable. |
| 84 | successor := in |
| 85 | successor.Stamp = barrier.Stamp |
| 86 | if err := f.g.EstablishRoot(t.Context(), successor, allow); !errors.Is(err, ErrRetiredStamp) { |
| 87 | t.Fatalf("settled successor installed: %v", err) |
| 88 | } |
| 89 | if err := f.g.CleanupWith(t.Context(), prior, func(context.Context, FenceReceipt) error { return nil }, func() error { return nil }); err != nil { |
| 90 | t.Fatal("prior cleanup after settlement:", err) |
| 91 | } |
| 92 | } |
| 93 | |