| 1 | package hostops |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "reflect" |
| 6 | "testing" |
| 7 | |
| 8 | "github.com/carlosframework/platform/internal/hostclaim" |
| 9 | "github.com/carlosframework/platform/internal/moves" |
| 10 | "github.com/carlosframework/platform/internal/provision" |
| 11 | ) |
| 12 | |
| 13 | // A source that never had a session (claim spec §6.4a) is the paired legacy |
| 14 | // identity {session 0, generation ""}. The move's cut then names session 0 |
| 15 | // and an empty source generation; a mismatched pair or a missing journal |
| 16 | // key is still refused. |
| 17 | |
| 18 | // legacySource makes the holder the legacy source before the target's first |
| 19 | // admission, with the move's cut at the legacy identity. |
| 20 | func legacySource(covered bool, afterAdmission bool) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { |
| 21 | return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { |
| 22 | h := c.Holder |
| 23 | h.WriterBox, h.Session, h.Generation = "ie-box", 0, "" |
| 24 | h.Admissions = nil |
| 25 | if afterAdmission { |
| 26 | h.WriterBox, h.Session, h.Generation = "uk-box", 1, "mv" |
| 27 | h.Admissions = []hostclaim.AdmissionRecord{{Session: 1, Box: "uk-box", Generation: "mv", Predecessor: "", Region: "uk", PredecessorRegion: "ie", Mode: "cold", Coverage: "sealed"}} |
| 28 | } |
| 29 | c.Op.Journal[hostclaim.JournalCutSession] = "0" |
| 30 | c.Op.Journal[hostclaim.JournalSrcGen] = "" |
| 31 | if covered { |
| 32 | seal, err := moves.DecodeSeal(c.Op.Journal[hostclaim.JournalSeal]) |
| 33 | if err != nil { |
| 34 | panic(err) |
| 35 | } |
| 36 | seal.SrcCovered = true |
| 37 | c.Op.Journal[hostclaim.JournalSeal] = seal.Encode() |
| 38 | } |
| 39 | } |
| 40 | } |
| 41 | |
| 42 | func declareLegacy(t *testing.T, w *lossWorld) []hostclaim.WriteSource { |
| 43 | t.Helper() |
| 44 | in := w.inspect(t, "uk-label") |
| 45 | if _, err := w.declare(w.submission(in)); err != nil { |
| 46 | t.Fatal(err) |
| 47 | } |
| 48 | loss, err := hostclaim.ReadLossRecord(context.Background(), w.raw, w.pub, in.Details.ID) |
| 49 | if err != nil || loss.Record.State != "committed" { |
| 50 | t.Fatalf("loss record = %+v %v", loss.Record, err) |
| 51 | } |
| 52 | return loss.Record.WriteSources |
| 53 | } |
| 54 | |
| 55 | func TestJ4LegacySourceCutDeclares(t *testing.T) { |
| 56 | sourceCut := hostclaim.WriteSource{Reason: "source-cut", Box: "ie-box", Generation: "", Session: 0, Region: "ie", Baseline: "seal:" + lossOp} |
| 57 | cases := map[string]struct { |
| 58 | covered, admitted bool |
| 59 | want []hostclaim.WriteSource |
| 60 | }{ |
| 61 | "before first admission": {false, false, []hostclaim.WriteSource{sourceCut}}, |
| 62 | "after first admission": {false, true, []hostclaim.WriteSource{ |
| 63 | {Reason: "target", Box: "uk-box", Generation: "mv", Session: 1, Region: "uk", Baseline: "replica:uk/mv"}, sourceCut}}, |
| 64 | "covered after first admission": {true, true, []hostclaim.WriteSource{ |
| 65 | {Reason: "target", Box: "uk-box", Generation: "mv", Session: 1, Region: "uk", Baseline: "replica:uk/mv"}}}, |
| 66 | } |
| 67 | for name, tc := range cases { |
| 68 | t.Run(name, func(t *testing.T) { |
| 69 | w := newLossWorld(t, "landing", legacySource(tc.covered, tc.admitted)) |
| 70 | if got := declareLegacy(t, w); !reflect.DeepEqual(got, tc.want) { |
| 71 | t.Fatalf("write sources = %+v, want %+v", got, tc.want) |
| 72 | } |
| 73 | }) |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | func TestJ4LegacyCoveredBeforeAdmissionDeclares(t *testing.T) { |
| 78 | w := newLossWorld(t, "landing", legacySource(true, false)) |
| 79 | if got := declareLegacy(t, w); len(got) != 0 { |
| 80 | t.Fatalf("write sources = %+v", got) |
| 81 | } |
| 82 | } |
| 83 | |
| 84 | func TestJ4LegacyJournalRefusedBeforeAcceptance(t *testing.T) { |
| 85 | set := func(key, v string) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { |
| 86 | return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { c.Op.Journal[key] = v } |
| 87 | } |
| 88 | del := func(key string) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { |
| 89 | return func(c *hostclaim.Claim, _ *provision.Record, _ *AppAdmission) { delete(c.Op.Journal, key) } |
| 90 | } |
| 91 | both := func(fs ...func(*hostclaim.Claim, *provision.Record, *AppAdmission)) func(*hostclaim.Claim, *provision.Record, *AppAdmission) { |
| 92 | return func(c *hostclaim.Claim, r *provision.Record, a *AppAdmission) { |
| 93 | for _, f := range fs { |
| 94 | f(c, r, a) |
| 95 | } |
| 96 | } |
| 97 | } |
| 98 | legacy := legacySource(false, false) |
| 99 | cases := map[string]func(*hostclaim.Claim, *provision.Record, *AppAdmission){ |
| 100 | "missing cut session": both(legacy, del(hostclaim.JournalCutSession)), |
| 101 | "malformed cut session": both(legacy, set(hostclaim.JournalCutSession, "x")), |
| 102 | "negative cut session": both(legacy, set(hostclaim.JournalCutSession, "-1")), |
| 103 | "empty cut session": both(legacy, set(hostclaim.JournalCutSession, "")), |
| 104 | "missing source generation": both(legacy, del(hostclaim.JournalSrcGen)), |
| 105 | "session 0 with generation": both(legacy, set(hostclaim.JournalSrcGen, "src")), |
| 106 | "session with empty generation": both(legacy, set(hostclaim.JournalCutSession, "15")), |
| 107 | "covered missing cut session": both(legacySource(true, false), del(hostclaim.JournalCutSession)), |
| 108 | "covered malformed cut": both(legacySource(true, false), set(hostclaim.JournalCutSession, "x")), |
| 109 | } |
| 110 | for name, mutate := range cases { |
| 111 | t.Run(name, func(t *testing.T) { |
| 112 | w := newLossWorld(t, "landing", mutate) |
| 113 | snap := lossSnapshot(t, w) |
| 114 | if _, err := w.control.InspectMoveLoss(context.Background(), w.signer, lossBucket, "operator", "bab", "app", lossHost, "uk-box"); err == nil { |
| 115 | t.Fatal("inspection accepted") |
| 116 | } |
| 117 | if lossSnapshot(t, w) != snap { |
| 118 | t.Fatal("a refused inspection wrote") |
| 119 | } |
| 120 | if len(w.acceptances(t)) != 0 || w.roster(t).Roster.Boxes["uk-box"].State == "quarantined" { |
| 121 | t.Fatal("accepted or quarantined") |
| 122 | } |
| 123 | }) |
| 124 | } |
| 125 | } |
| 126 | |