| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "errors" |
| 5 | "reflect" |
| 6 | "strconv" |
| 7 | "testing" |
| 8 | "time" |
| 9 | ) |
| 10 | |
| 11 | func TestPlan12TargetFenceRelease(t *testing.T) { |
| 12 | for _, kind := range []string{KindLanding, KindRestoring} { |
| 13 | for _, mode := range []string{"ok", "wrong-op", "wrong-target", "wrong-enrollment", "wrong-region", "unverified", "zero-proof", "stale", "committed", "partial-return", "parks", "no-grant"} { |
| 14 | t.Run(kind+"/"+mode, func(t *testing.T) { |
| 15 | s, o, ref, a := c7Fixture(t) |
| 16 | var err error |
| 17 | o, err = Update(t.Context(), s, o, func(c *Claim) error { |
| 18 | c.Obligations = nil |
| 19 | if mode == "parks" { |
| 20 | for _, owner := range []string{"box:adi-knitting", "box:other", "region:ie"} { |
| 21 | id := "existing-" + strconv.Itoa(len(c.Obligations)) |
| 22 | c.Obligations = append(c.Obligations, Obligation{ID: id, Op: c.Op.ID, Kind: "park-generation", Generation: id, Region: "ie", Owner: owner, Retirement: id}) |
| 23 | } |
| 24 | } |
| 25 | c.Op.Kind, c.Op.Phase, c.Op.Exec = kind, PhaseProbing, "adi-knitting" |
| 26 | c.Op.Participants = []string{"bri-knitting", "adi-knitting"} |
| 27 | c.Op.Intent["target"], c.Op.Intent["source"], c.Op.Intent["to"], c.Op.Intent["target_enrollment"] = "adi-knitting", "bri-knitting", "ie", ref.Enrollment |
| 28 | setJournal(c, JournalSourceStamp, "1") |
| 29 | setJournal(c, JournalSrcGen, "src") |
| 30 | if kind == KindRestoring { |
| 31 | c.Op.Phase = PhaseReleasing |
| 32 | c.Op.Intent["rollback"] = lostRB |
| 33 | setJournal(c, JournalTargetStamp, strconv.FormatUint(c.Holder.Stamp, 10)) |
| 34 | } |
| 35 | if mode == "wrong-target" { |
| 36 | c.Op.Intent["target"] = "other" |
| 37 | } |
| 38 | if mode == "wrong-enrollment" { |
| 39 | c.Op.Intent["target_enrollment"] = "bad" |
| 40 | } |
| 41 | if mode == "wrong-region" { |
| 42 | c.Op.Intent["to"] = "uk" |
| 43 | } |
| 44 | if mode == "committed" { |
| 45 | c.Op.Phase = PhaseCommitted |
| 46 | } |
| 47 | return nil |
| 48 | }) |
| 49 | if err != nil { |
| 50 | t.Fatal(err) |
| 51 | } |
| 52 | if mode == "unverified" { |
| 53 | a.err = ErrFence |
| 54 | } |
| 55 | f, err := VerifyMoveTargetFence(t.Context(), s, o, ref, a) |
| 56 | n := 0 |
| 57 | scope := func(loc string) ParkScope { |
| 58 | return ParkScope{Location: loc, Incarnation: o.Claim.Holder.Incarnation, EvidenceAt: f.ObservedAt(), RequestLimit: 2 * time.Minute} |
| 59 | } |
| 60 | r := FencedTargetRelease{OpID: o.Claim.Op.ID, RollbackID: lostRB, Target: "adi-knitting", TargetRegion: "ie", SourceRegion: "uk", Location: scope(o.Claim.Holder.Location), Lane: scope("moves/" + o.Host + "/" + o.Claim.Op.ID), Fence: f, NewID: func() string { n++; return "fence-" + strconv.Itoa(n) }} |
| 61 | if mode == "wrong-op" { |
| 62 | r.OpID = "other" |
| 63 | } |
| 64 | if mode == "zero-proof" { |
| 65 | r.Fence = ReturnFence{} |
| 66 | } |
| 67 | if mode == "partial-return" { |
| 68 | lane := scope("moves/" + o.Host + "/" + lostRB) |
| 69 | r.ReturnLane = &lane |
| 70 | } |
| 71 | if mode == "stale" { |
| 72 | if _, e := Update(t.Context(), s, o, func(*Claim) error { return nil }); e != nil { |
| 73 | t.Fatal(e) |
| 74 | } |
| 75 | } |
| 76 | s.puts = 0 |
| 77 | var got Observed |
| 78 | if err == nil { |
| 79 | transition := releaseFencedTarget(r) |
| 80 | if mode == "no-grant" { |
| 81 | transition = ReleaseFencedTarget(r) |
| 82 | } |
| 83 | got, err = Update(t.Context(), s, o, transition) |
| 84 | } |
| 85 | if mode != "ok" && mode != "partial-return" && mode != "parks" { |
| 86 | if err == nil { |
| 87 | t.Fatal("accepted", mode) |
| 88 | } |
| 89 | if mode == "stale" && !errors.Is(err, ErrConflict) { |
| 90 | t.Fatal(err) |
| 91 | } |
| 92 | return |
| 93 | } |
| 94 | if err != nil { |
| 95 | t.Fatal(err) |
| 96 | } |
| 97 | if s.puts != 1 || got.Claim.Op.Phase != PhaseReleased || got.Claim.Op.Exec != "none" || stringPresent(got.Claim.Op.Participants, r.Target) || len(got.Claim.Obligations) < 2 { |
| 98 | t.Fatal(got.Claim) |
| 99 | } |
| 100 | if mode == "parks" { |
| 101 | gotLoss := got.Claim.EvidenceRefs[len(got.Claim.EvidenceRefs)-1] |
| 102 | want := []ObligationRef{{ID: "existing-0", Op: o.Claim.Op.ID, Retirement: "existing-0"}} |
| 103 | if !reflect.DeepEqual(gotLoss.Reassigned, want) { |
| 104 | t.Fatalf("reassigned %+v", gotLoss.Reassigned) |
| 105 | } |
| 106 | for _, ob := range got.Claim.Obligations { |
| 107 | if ob.ID == "existing-1" && ob.Owner != "box:other" { |
| 108 | t.Fatal("stole another box's park") |
| 109 | } |
| 110 | } |
| 111 | } |
| 112 | clone, _ := cloneClaim(got.Claim) |
| 113 | if err := releaseFencedTarget(r)(clone); err != nil || !reflect.DeepEqual(clone, got.Claim) { |
| 114 | t.Fatal("replay", err) |
| 115 | } |
| 116 | if err := ReturnToSource(r.OpID, "bri-knitting", false, ReturnFence{})(clone); err != nil { |
| 117 | t.Fatal("source needs fence authority", err) |
| 118 | } |
| 119 | }) |
| 120 | } |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | func TestPlan12FenceRefusesCleanCoverage(t *testing.T) { |
| 125 | c := lostTargetClaim(t, KindRestoring, PhaseProbing) |
| 126 | c.LossRefs = nil |
| 127 | delete(c.Op.Journal, "loss."+lostID) |
| 128 | c.Op.Exec = "ie-box" |
| 129 | c.Op.Journal["fence.uk-box"] = "verified-in-release" |
| 130 | h := c.Holder |
| 131 | in := Admission{ObservedOpID: c.Op.ID, ObservedPhase: c.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "ie-box", Mode: "cold", NewGeneration: "return", Region: "ie", PredecessorRegion: "uk", Coverage: "clean-return"} |
| 132 | if err := AdmitWriter(in)(c); !errors.Is(err, ErrFence) { |
| 133 | t.Fatal(err) |
| 134 | } |
| 135 | } |
| 136 | |