package provision import ( "errors" "strings" "testing" "time" ) func c7RelocationFixture() (*Record, MoveGuard, string, string) { g := MoveGuard{ID: "mv_abcdefghijklmnopqrstuvwxyz", Incarnation: strings.Repeat("a", 64)} en := strings.Repeat("b", 32) fence := `{"enrollment":"` + en + `","key":"control/box-fences/` + en + `.json","sha256":"` + strings.Repeat("c", 64) + `"}` rec := &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", State: MoveCutting, CutPhase: CutArchived}} return rec, g, en, fence } func c7Decide(g MoveGuard, en, region, fence string, now time.Time) func(*Record) error { return MoveRelocatingDecision(g, "source", en, region, fence, 7, "claim-etag", strings.Repeat("d", 64), strings.Repeat("e", 64), now) } // The decision and the flip exclude each other under one record CAS, in // both orders. MoveComplete is the flip that exists on main today; Moves // Plan 2's MoveFlip must add the same negative case. func TestC7RecordDecisionAndFlipExcludeEachOther(t *testing.T) { now := time.Now().UTC() rec, g, en, fence := c7RelocationFixture() if err := c7Decide(g, en, "ie", fence, now)(rec); err != nil { t.Fatal(err) } if rec.Move.State != MoveRelocating || rec.Move.Relocation.PreviousState != MoveCutting || rec.Move.Relocation.PreviousCutPhase != CutArchived || rec.Move.To != "uk" || rec.Region != "ie" { t.Fatalf("lost source or move history: %+v", rec.Move) } for name, tr := range map[string]func(*Record) error{ "complete": MoveComplete(), "guarded-cut": MoveGuardedCut(g), "guarded-abort": MoveGuardedAbort(g), "abort": MoveAbort(), "abort-complete": MoveGuardedAbortComplete(g, now), "promote": MoveGuardedPromoteReady(g, now, MoveInputs{}), "mark-cutting": MoveMarkCutting(), "start": MoveStartTransition(MoveStart{ID: g.ID, Incarnation: g.Incarnation, From: "ie", To: "uk", Cut: "auto", Now: now}), } { probe := *rec m := *rec.Move probe.Move = &m if err := tr(&probe); !errors.Is(err, ErrMoveConflict) || probe.Move.State != MoveRelocating || probe.Region != "ie" { t.Fatalf("%s crossed a durable relocation decision: %v", name, err) } } if err := c7Decide(g, en, "ie", fence, now.Add(time.Hour))(rec); err != nil || !rec.Move.Relocation.DecidedAt.Equal(now) { t.Fatal("replay refreshed decision", err) } other := MoveRelocatingDecision(g, "source", en, "ie", fence, 7, "claim-etag", strings.Repeat("f", 64), strings.Repeat("e", 64), now) if err := other(rec); !errors.Is(err, ErrMoveConflict) { t.Fatal("a different moving operation replayed the decision", err) } rec, g, en, fence = c7RelocationFixture() if err := MoveComplete()(rec); err != nil { t.Fatal(err) } if err := c7Decide(g, en, "ie", fence, now)(rec); !errors.Is(err, ErrMoveConflict) { t.Fatal("conversion crossed flip", err) } } func TestC7RecordDecisionRefusals(t *testing.T) { now := time.Now().UTC() for _, tc := range []string{"wrong-region", "record-region", "flipped", "rollback", "aborting", "landing", "wrong-move", "fence-enrollment", "fence-key", "zero-time"} { t.Run(tc, func(t *testing.T) { rec, g, en, fence := c7RelocationFixture() region, now := "ie", now switch tc { case "wrong-region": region = "uk" case "record-region": rec.Region = "uk" case "flipped": at := now rec.Move.FlippedAt = &at case "rollback": rec.Move.Rollback = &MoveRollback{ID: "rb"} case "aborting": rec.Move.State = MoveAborting case "landing": rec.Move.State = MoveLanding case "wrong-move": g.ID = "mv_bcdefghijklmnopqrstuvwxyza" case "fence-enrollment": fence = strings.Replace(fence, `"enrollment":"`+en, `"enrollment":"`+strings.Repeat("9", 32), 1) case "fence-key": fence = strings.Replace(fence, "control/box-fences/", "control/other/", 1) case "zero-time": now = time.Time{} } before := *rec.Move if err := c7Decide(g, en, region, fence, now)(rec); !errors.Is(err, ErrMoveConflict) { t.Fatalf("accepted: %v", err) } if rec.Move.State != before.State || rec.Move.Relocation != nil { t.Fatal("a refused decision changed the record") } }) } } func TestC7LostTargetParkStampIsOriginalAndConditional(t *testing.T) { now := time.Now().UTC() g := MoveGuard{ID: "mv_abcdefghijklmnopqrstuvwxyz", Incarnation: strings.Repeat("a", 64)} rec := &Record{Region: "ie", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, State: MoveRollingBack, Rollback: &MoveRollback{ID: "rb-1", TargetPhase: RbTargetReleased}}} if err := MoveLostTargetParked(g, "other", now)(rec); err == nil { t.Fatal("other rollback stamped") } rec.Move.Rollback.Dirty = true if err := MoveLostTargetParked(g, "rb-1", now)(rec); err == nil { t.Fatal("uncommitted dirty loss stamped") } rec.Move.Rollback.Dirty = false if err := MoveLostTargetParked(g, "rb-1", now)(rec); err != nil { t.Fatal(err) } if err := MoveLostTargetParked(g, "rb-1", now.Add(time.Hour))(rec); err != nil || !rec.Move.Rollback.TargetParkedAt.Equal(now) || rec.Move.Rollback.TargetPhase != RbTargetCleaned { t.Fatal("replay changed stamp", err) } }