package hostops import ( "context" "reflect" "testing" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/moves" "github.com/carlosframework/platform/internal/provision" ) // A source that never had a session (claim spec ยง6.4a) is the paired legacy // identity {session 0, generation ""}. The move's cut then names session 0 // and an empty source generation; a mismatched pair or a missing journal // key is still refused. // legacySource makes the holder the legacy source before the target's first // admission, with the move's cut at the legacy identity. func legacySource(covered bool, afterAdmission bool) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { h := c.Holder h.WriterBox, h.Session, h.Generation = "ie-box", 0, "" h.Admissions = nil if afterAdmission { h.WriterBox, h.Session, h.Generation = "uk-box", 1, "mv" h.Admissions = []hostclaim.AdmissionRecord{{Session: 1, Box: "uk-box", Generation: "mv", Predecessor: "", Region: "uk", PredecessorRegion: "ie", Mode: "cold", Coverage: "sealed"}} } c.Op.Journal[hostclaim.JournalCutSession] = "0" c.Op.Journal[hostclaim.JournalSrcGen] = "" if covered { seal, err := moves.DecodeSeal(c.Op.Journal[hostclaim.JournalSeal]) if err != nil { panic(err) } seal.SrcCovered = true c.Op.Journal[hostclaim.JournalSeal] = seal.Encode() } } } func declareLegacy(t *testing.T, w *lossWorld) []hostclaim.WriteSource { t.Helper() in := w.inspect(t, "uk-label") if _, err := w.declare(w.submission(in)); err != nil { t.Fatal(err) } loss, err := hostclaim.ReadLossRecord(context.Background(), w.raw, w.pub, in.Details.ID) if err != nil || loss.Record.State != "committed" { t.Fatalf("loss record = %+v %v", loss.Record, err) } return loss.Record.WriteSources } func TestJ4LegacySourceCutDeclares(t *testing.T) { sourceCut := hostclaim.WriteSource{Reason: "source-cut", Box: "ie-box", Generation: "", Session: 0, Region: "ie", Baseline: "seal:" + lossOp} cases := map[string]struct { covered, admitted bool want []hostclaim.WriteSource }{ "before first admission": {false, false, []hostclaim.WriteSource{sourceCut}}, "after first admission": {false, true, []hostclaim.WriteSource{ {Reason: "target", Box: "uk-box", Generation: "mv", Session: 1, Region: "uk", Baseline: "replica:uk/mv"}, sourceCut}}, "covered after first admission": {true, true, []hostclaim.WriteSource{ {Reason: "target", Box: "uk-box", Generation: "mv", Session: 1, Region: "uk", Baseline: "replica:uk/mv"}}}, } for name, tc := range cases { t.Run(name, func(t *testing.T) { w := newLossWorld(t, "landing", legacySource(tc.covered, tc.admitted)) if got := declareLegacy(t, w); !reflect.DeepEqual(got, tc.want) { t.Fatalf("write sources = %+v, want %+v", got, tc.want) } }) } } func TestJ4LegacyCoveredBeforeAdmissionDeclares(t *testing.T) { w := newLossWorld(t, "landing", legacySource(true, false)) if got := declareLegacy(t, w); len(got) != 0 { t.Fatalf("write sources = %+v", got) } } func TestJ4LegacyJournalRefusedBeforeAcceptance(t *testing.T) { set := func(key, v string) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { c.Op.Journal[key] = v } } del := func(key string) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { delete(c.Op.Journal, key) } } both := func(fs ...func(*hostclaim.Claim, *provision.Record, *AppAdmission)) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { return func(c *hostclaim.Claim, r *provision.Record, a *AppAdmission) { for _, f := range fs { f(c, r, a) } } } legacy := legacySource(false, false) cases := map[string]func(*hostclaim.Claim, *provision.Record, *AppAdmission){ "missing cut session": both(legacy, del(hostclaim.JournalCutSession)), "malformed cut session": both(legacy, set(hostclaim.JournalCutSession, "x")), "negative cut session": both(legacy, set(hostclaim.JournalCutSession, "-1")), "empty cut session": both(legacy, set(hostclaim.JournalCutSession, "")), "missing source generation": both(legacy, del(hostclaim.JournalSrcGen)), "session 0 with generation": both(legacy, set(hostclaim.JournalSrcGen, "src")), "session with empty generation": both(legacy, set(hostclaim.JournalCutSession, "15")), "covered missing cut session": both(legacySource(true, false), del(hostclaim.JournalCutSession)), "covered malformed cut": both(legacySource(true, false), set(hostclaim.JournalCutSession, "x")), } for name, mutate := range cases { t.Run(name, func(t *testing.T) { w := newLossWorld(t, "landing", mutate) snap := lossSnapshot(t, w) if _, err := w.control.InspectMoveLoss(context.Background(), w.signer, lossBucket, "operator", "bab", "app", lossHost, "uk-box"); err == nil { t.Fatal("inspection accepted") } if lossSnapshot(t, w) != snap { t.Fatal("a refused inspection wrote") } if len(w.acceptances(t)) != 0 || w.roster(t).Roster.Boxes["uk-box"].State == "quarantined" { t.Fatal("accepted or quarantined") } }) } }