| 1 | package hostclaim |
| 2 | |
| 3 | import ( |
| 4 | "context" |
| 5 | "crypto/ed25519" |
| 6 | "errors" |
| 7 | "reflect" |
| 8 | "testing" |
| 9 | |
| 10 | "github.com/carlosframework/platform/internal/release" |
| 11 | ) |
| 12 | |
| 13 | func TestLossIdentityValidation(t *testing.T) { |
| 14 | for _, tc := range []struct { |
| 15 | name string |
| 16 | loss WritesLost |
| 17 | valid bool |
| 18 | }{ |
| 19 | {"legacy", WritesLost{Box: "box"}, true}, |
| 20 | {"claimed", WritesLost{Box: "box", Session: 2, Generation: "g2"}, true}, |
| 21 | {"missing legacy box", WritesLost{}, false}, |
| 22 | {"missing claimed box", WritesLost{Session: 2, Generation: "g2"}, false}, |
| 23 | {"zero session with generation", WritesLost{Box: "box", Generation: "g2"}, false}, |
| 24 | {"positive session without generation", WritesLost{Box: "box", Session: 2}, false}, |
| 25 | } { |
| 26 | t.Run(tc.name, func(t *testing.T) { |
| 27 | for _, field := range []string{"primary", "additional", "source"} { |
| 28 | t.Run(field, func(t *testing.T) { |
| 29 | r := validLossRecord(t) |
| 30 | a := ArchivedAdmissionLoss{LossID: r.ID, AdmissionClaimGen: 8} |
| 31 | switch field { |
| 32 | case "primary": |
| 33 | a.WritesLost = &tc.loss |
| 34 | case "additional": |
| 35 | a.AdditionalWritesLost = []WritesLost{tc.loss} |
| 36 | case "source": |
| 37 | r.WriteSources[0].Box, r.WriteSources[0].Session, r.WriteSources[0].Generation = tc.loss.Box, tc.loss.Session, tc.loss.Generation |
| 38 | } |
| 39 | if field != "source" { |
| 40 | r.ArchivedAdmissionLosses = []ArchivedAdmissionLoss{a} |
| 41 | } |
| 42 | err := r.Validate() |
| 43 | if tc.valid && err != nil || !tc.valid && !errors.Is(err, ErrLossRecord) { |
| 44 | t.Fatalf("Validate() = %v, valid = %v", err, tc.valid) |
| 45 | } |
| 46 | }) |
| 47 | } |
| 48 | }) |
| 49 | } |
| 50 | legacy := WritesLost{Box: "box"} |
| 51 | if validWriteLosses(&legacy, []WritesLost{legacy}) || validWriteLosses(nil, []WritesLost{legacy, legacy}) { |
| 52 | t.Fatal("duplicate legacy identity accepted") |
| 53 | } |
| 54 | } |
| 55 | |
| 56 | func TestAdmitWriterWithLossArchivesLegacyIdentity(t *testing.T) { |
| 57 | for _, field := range []string{"primary", "additional"} { |
| 58 | t.Run(field, func(t *testing.T) { |
| 59 | ctx := context.Background() |
| 60 | s := release.NewDirStore(t.TempDir()) |
| 61 | pub, priv, err := ed25519.GenerateKey(nil) |
| 62 | if err != nil { |
| 63 | t.Fatal(err) |
| 64 | } |
| 65 | primary := WritesLost{Box: "target", Session: 4, Generation: "g-target", RemoteMaxTXID: 12} |
| 66 | additional := WritesLost{Box: "source", Session: 2, Generation: "g-source", RemoteMaxTXID: 10} |
| 67 | if field == "primary" { |
| 68 | primary.Session, primary.Generation = 0, "" |
| 69 | } else { |
| 70 | additional.Session, additional.Generation = 0, "" |
| 71 | } |
| 72 | obs := servingClaim(t, ctx, s) |
| 73 | obs = updateGenerationTest(t, ctx, s, obs, func(c *Claim) error { |
| 74 | c.Op = Op{Kind: KindReverting, ID: "return", Phase: "recover", Exec: "recovery", Participants: []string{"recovery"}} |
| 75 | c.Holder.WriterBox, c.Holder.Generation, c.Holder.Session = primary.Box, primary.Generation, primary.Session |
| 76 | c.LossRefs = []string{"loss_1"} |
| 77 | return nil |
| 78 | }) |
| 79 | r := validLossRecord(t) |
| 80 | r.Host = obs.Host |
| 81 | r.HolderIncarnation, r.HolderStamp, r.HolderSession = obs.Claim.Holder.Incarnation, obs.Claim.Holder.Stamp, primary.Session |
| 82 | r.OpID, r.OpKind, r.OpPhase, r.Box = "return", KindReverting, "recover", primary.Box |
| 83 | r.WriteSources = []WriteSource{ |
| 84 | {Reason: "target", Box: primary.Box, Generation: primary.Generation, Session: primary.Session, Region: "ie", Baseline: "target-snapshot"}, |
| 85 | {Reason: "source-cut", Box: additional.Box, Generation: additional.Generation, Session: additional.Session, Region: "ie", Baseline: "source-seal"}, |
| 86 | } |
| 87 | created, err := CreateLossRecord(ctx, s, release.RootSigner(priv), r) |
| 88 | if err != nil { |
| 89 | t.Fatal(err) |
| 90 | } |
| 91 | r.State, r.ResultClaimGen = "committed", obs.Claim.Gen |
| 92 | committed, err := ReplaceLossRecord(ctx, s, release.RootSigner(priv), created, r) |
| 93 | if err != nil { |
| 94 | t.Fatal(err) |
| 95 | } |
| 96 | in := Admission{ObservedOpID: "return", ObservedPhase: "recover", ObservedSession: primary.Session, ObservedGeneration: primary.Generation, Box: "recovery", Mode: "cold", NewGeneration: "g-recovery", Region: "ie", PredecessorRegion: "ie", RemoteMaxTXID: primary.RemoteMaxTXID, AdditionalWritesLost: []WritesLost{additional}} |
| 97 | validated := []ValidatedWriteSource{ |
| 98 | {Reason: "target", Box: primary.Box, Generation: primary.Generation, Session: primary.Session, Region: "ie", Baseline: "target-snapshot", RemoteMaxTXID: primary.RemoteMaxTXID}, |
| 99 | {Reason: "source-cut", Box: additional.Box, Generation: additional.Generation, Session: additional.Session, Region: "ie", Baseline: "source-seal", RemoteMaxTXID: additional.RemoteMaxTXID}, |
| 100 | } |
| 101 | admitted, err := AdmitWriterWithLoss(ctx, s, pub, obs, in, r.ID, validated, testLossRecoveryOwner{}) |
| 102 | if err != nil { |
| 103 | t.Fatal(err) |
| 104 | } |
| 105 | a := admitted.Claim.Holder.Admissions[0] |
| 106 | if a.LossID != r.ID || !reflect.DeepEqual(a.WritesLost, &primary) || !reflect.DeepEqual(a.AdditionalWritesLost, []WritesLost{additional}) { |
| 107 | t.Fatalf("admission lost identity: %+v", a) |
| 108 | } |
| 109 | if _, err := ArchiveAdmissionLosses(ctx, s, pub, admitted, r.ID); !errors.Is(err, ErrLossEvidencePending) { |
| 110 | t.Fatalf("unarchived legacy evidence = %v", err) |
| 111 | } |
| 112 | r.ArchivedAdmissionLosses = []ArchivedAdmissionLoss{{LossID: r.ID, AdmissionClaimGen: a.Session, WritesLost: &primary, AdditionalWritesLost: []WritesLost{additional}}} |
| 113 | if _, err := ReplaceLossRecord(ctx, s, release.RootSigner(priv), committed, r); err != nil { |
| 114 | t.Fatal(err) |
| 115 | } |
| 116 | archived, err := ArchiveAdmissionLosses(ctx, s, pub, admitted, r.ID) |
| 117 | if err != nil { |
| 118 | t.Fatal(err) |
| 119 | } |
| 120 | if archived.Claim.Holder.Admissions[0].LossArchivedClaimGen != a.Session || RequireLossEvidenceArchived(archived.Claim) != nil { |
| 121 | t.Fatalf("legacy evidence not archived: %+v", archived.Claim.Holder.Admissions) |
| 122 | } |
| 123 | }) |
| 124 | } |
| 125 | } |
| 126 | |