package hostclaim import ( "context" "crypto/ed25519" "errors" "reflect" "testing" "github.com/carlosframework/platform/internal/release" ) func TestLossIdentityValidation(t *testing.T) { for _, tc := range []struct { name string loss WritesLost valid bool }{ {"legacy", WritesLost{Box: "box"}, true}, {"claimed", WritesLost{Box: "box", Session: 2, Generation: "g2"}, true}, {"missing legacy box", WritesLost{}, false}, {"missing claimed box", WritesLost{Session: 2, Generation: "g2"}, false}, {"zero session with generation", WritesLost{Box: "box", Generation: "g2"}, false}, {"positive session without generation", WritesLost{Box: "box", Session: 2}, false}, } { t.Run(tc.name, func(t *testing.T) { for _, field := range []string{"primary", "additional", "source"} { t.Run(field, func(t *testing.T) { r := validLossRecord(t) a := ArchivedAdmissionLoss{LossID: r.ID, AdmissionClaimGen: 8} switch field { case "primary": a.WritesLost = &tc.loss case "additional": a.AdditionalWritesLost = []WritesLost{tc.loss} case "source": r.WriteSources[0].Box, r.WriteSources[0].Session, r.WriteSources[0].Generation = tc.loss.Box, tc.loss.Session, tc.loss.Generation } if field != "source" { r.ArchivedAdmissionLosses = []ArchivedAdmissionLoss{a} } err := r.Validate() if tc.valid && err != nil || !tc.valid && !errors.Is(err, ErrLossRecord) { t.Fatalf("Validate() = %v, valid = %v", err, tc.valid) } }) } }) } legacy := WritesLost{Box: "box"} if validWriteLosses(&legacy, []WritesLost{legacy}) || validWriteLosses(nil, []WritesLost{legacy, legacy}) { t.Fatal("duplicate legacy identity accepted") } } func TestAdmitWriterWithLossArchivesLegacyIdentity(t *testing.T) { for _, field := range []string{"primary", "additional"} { t.Run(field, func(t *testing.T) { ctx := context.Background() s := release.NewDirStore(t.TempDir()) pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } primary := WritesLost{Box: "target", Session: 4, Generation: "g-target", RemoteMaxTXID: 12} additional := WritesLost{Box: "source", Session: 2, Generation: "g-source", RemoteMaxTXID: 10} if field == "primary" { primary.Session, primary.Generation = 0, "" } else { additional.Session, additional.Generation = 0, "" } obs := servingClaim(t, ctx, s) obs = updateGenerationTest(t, ctx, s, obs, func(c *Claim) error { c.Op = Op{Kind: KindReverting, ID: "return", Phase: "recover", Exec: "recovery", Participants: []string{"recovery"}} c.Holder.WriterBox, c.Holder.Generation, c.Holder.Session = primary.Box, primary.Generation, primary.Session c.LossRefs = []string{"loss_1"} return nil }) r := validLossRecord(t) r.Host = obs.Host r.HolderIncarnation, r.HolderStamp, r.HolderSession = obs.Claim.Holder.Incarnation, obs.Claim.Holder.Stamp, primary.Session r.OpID, r.OpKind, r.OpPhase, r.Box = "return", KindReverting, "recover", primary.Box r.WriteSources = []WriteSource{ {Reason: "target", Box: primary.Box, Generation: primary.Generation, Session: primary.Session, Region: "ie", Baseline: "target-snapshot"}, {Reason: "source-cut", Box: additional.Box, Generation: additional.Generation, Session: additional.Session, Region: "ie", Baseline: "source-seal"}, } created, err := CreateLossRecord(ctx, s, release.RootSigner(priv), r) if err != nil { t.Fatal(err) } r.State, r.ResultClaimGen = "committed", obs.Claim.Gen committed, err := ReplaceLossRecord(ctx, s, release.RootSigner(priv), created, r) if err != nil { t.Fatal(err) } 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}} validated := []ValidatedWriteSource{ {Reason: "target", Box: primary.Box, Generation: primary.Generation, Session: primary.Session, Region: "ie", Baseline: "target-snapshot", RemoteMaxTXID: primary.RemoteMaxTXID}, {Reason: "source-cut", Box: additional.Box, Generation: additional.Generation, Session: additional.Session, Region: "ie", Baseline: "source-seal", RemoteMaxTXID: additional.RemoteMaxTXID}, } admitted, err := AdmitWriterWithLoss(ctx, s, pub, obs, in, r.ID, validated, testLossRecoveryOwner{}) if err != nil { t.Fatal(err) } a := admitted.Claim.Holder.Admissions[0] if a.LossID != r.ID || !reflect.DeepEqual(a.WritesLost, &primary) || !reflect.DeepEqual(a.AdditionalWritesLost, []WritesLost{additional}) { t.Fatalf("admission lost identity: %+v", a) } if _, err := ArchiveAdmissionLosses(ctx, s, pub, admitted, r.ID); !errors.Is(err, ErrLossEvidencePending) { t.Fatalf("unarchived legacy evidence = %v", err) } r.ArchivedAdmissionLosses = []ArchivedAdmissionLoss{{LossID: r.ID, AdmissionClaimGen: a.Session, WritesLost: &primary, AdditionalWritesLost: []WritesLost{additional}}} if _, err := ReplaceLossRecord(ctx, s, release.RootSigner(priv), committed, r); err != nil { t.Fatal(err) } archived, err := ArchiveAdmissionLosses(ctx, s, pub, admitted, r.ID) if err != nil { t.Fatal(err) } if archived.Claim.Holder.Admissions[0].LossArchivedClaimGen != a.Session || RequireLossEvidenceArchived(archived.Claim) != nil { t.Fatalf("legacy evidence not archived: %+v", archived.Claim.Holder.Admissions) } }) } }