package hostclaim import ( "crypto/ed25519" "errors" "reflect" "testing" "github.com/carlosframework/platform/internal/release" ) func TestPlan12ParticipantLossKeepsExecutorAdmission(t *testing.T) { for _, target := range []bool{false, true} { t.Run(map[bool]string{false: "participant", true: "target"}[target], func(t *testing.T) { s := release.NewDirStore(t.TempDir()) pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } o := servingClaim(t, t.Context(), s) o, err = Update(t.Context(), s, o, func(c *Claim) error { c.Op = Op{ID: "move", Kind: KindLanding, Phase: PhaseProbing, Exec: "box-a", Participants: []string{"box-a", "box-b"}, Intent: map[string]string{"target": "box-a"}} if target { c.Op.Intent["target"] = "box-b" } return nil }) if err != nil { t.Fatal(err) } r := lossForObservation(t, o, "participant", 1, "box-b", LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{"kind":"participant-lost"}`)}) if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), r); err != nil { t.Fatal(err) } o, err = DeclareLost(t.Context(), s, pub, o, r.ID, testLossOwner{}) if err != nil { t.Fatal(err) } h := o.Claim.Holder _, err = Update(t.Context(), s, o, AdmitWriter(Admission{ObservedOpID: o.Claim.Op.ID, ObservedPhase: o.Claim.Op.Phase, ObservedSession: h.Session, ObservedGeneration: h.Generation, Box: "box-a", Mode: "cold", NewGeneration: "healthy-next", Region: "ie"})) if target && !errors.Is(err, ErrLossRecord) || !target && err != nil { t.Fatalf("admission: %v", err) } }) } } func TestPlan12DeclareLostRefusesSecondSameOperation(t *testing.T) { for _, which := range []string{"different-box", "new-loss-id", "revoke-execution", "local-only-different-box"} { t.Run(which, func(t *testing.T) { s := release.NewDirStore(t.TempDir()) pub, priv, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } o := servingClaim(t, t.Context(), s) o, err = Update(t.Context(), s, o, func(c *Claim) error { c.Op = Op{ID: "move", Kind: KindLanding, Phase: PhaseProbing, Exec: "box-a", Participants: []string{"box-a", "box-b", "box-c"}} c.Obligations = []Obligation{{ID: "local", Op: "move", Kind: "park-local", Box: "box-c", Retirement: "ret", Owner: "box:box-c"}} return nil }) if err != nil { t.Fatal(err) } first := lossForObservation(t, o, "first", 1, "box-b", LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{}`)}) if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), first); err != nil { t.Fatal(err) } o, err = DeclareLost(t.Context(), s, pub, o, first.ID, testLossOwner{}) if err != nil { t.Fatal(err) } box := "box-c" effects := LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{}`)} switch which { case "new-loss-id": box, effects.RemoveParticipation = "box-b", false case "revoke-execution": box, effects.RevokeExecution = "box-a", true case "local-only-different-box": effects = LossConsequences{ DischargeLocal: []ObligationRef{{ID: "local", Op: "move", Retirement: "ret"}}, DestroyedStorage: []string{"move/local/ret"}, RecoveryIntent: []byte(`{"kind":"destroyed-local-only"}`), } } second := lossForObservation(t, o, "second", 2, box, effects) if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), second); err != nil { t.Fatal(err) } before := make(map[string]ObservedLossRecord) for _, id := range []string{first.ID, second.ID} { r, err := ReadLossRecord(t.Context(), s, pub, id) if err != nil { t.Fatal(err) } before[id] = r } if _, err = DeclareLost(t.Context(), s, pub, o, second.ID, testLossOwner{}); !errors.Is(err, ErrWrongState) { t.Fatalf("second declaration: %v", err) } fresh, err := Read(t.Context(), s, o.Host) if err != nil || fresh.ETag != o.ETag || !reflect.DeepEqual(fresh.Claim, o.Claim) { t.Fatal("refusal mutated claim or obligations", err) } for id, prior := range before { after, err := ReadLossRecord(t.Context(), s, pub, id) if err != nil || !reflect.DeepEqual(prior, after) { t.Fatal("refusal mutated loss record", id, err) } } }) } } func TestPlan12OperationLossGateIgnoresOtherParticipants(t *testing.T) { c := &Claim{Op: Op{ID: "move", Intent: map[string]string{"target": "target"}, Journal: map[string]string{"loss.target": "{}", "loss.participant": "{}", "loss_gate.target": `{"box":"target"}`, "loss_gate.participant": `{"box":"participant"}`}}, LossRefs: []string{"target", "participant"}} for i := 0; i < 100; i++ { id, err := OperationLossID(c) if err != nil || id != "target" { t.Fatalf("loss selection %q %v", id, err) } } } func TestPlan12OperationLossGateIncludesRevokedExecutor(t *testing.T) { c := &Claim{Op: Op{ID: "move", Intent: map[string]string{"target": "target"}, Journal: map[string]string{ "loss.executor": "{}", "loss_gate.executor": `{"box":"source","revoke_execution":true}`, }}, LossRefs: []string{"executor"}} if id, err := OperationLossID(c); err != nil || id != "executor" { t.Fatalf("revoked executor loss selection %q %v", id, err) } }