carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package hostclaim
2
3import (
4 "crypto/ed25519"
5 "errors"
6 "reflect"
7 "testing"
8
9 "github.com/carlosframework/platform/internal/release"
10)
11
12func TestPlan12ParticipantLossKeepsExecutorAdmission(t *testing.T) {
13 for _, target := range []bool{false, true} {
14 t.Run(map[bool]string{false: "participant", true: "target"}[target], func(t *testing.T) {
15 s := release.NewDirStore(t.TempDir())
16 pub, priv, err := ed25519.GenerateKey(nil)
17 if err != nil {
18 t.Fatal(err)
19 }
20 o := servingClaim(t, t.Context(), s)
21 o, err = Update(t.Context(), s, o, func(c *Claim) error {
22 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"}}
23 if target {
24 c.Op.Intent["target"] = "box-b"
25 }
26 return nil
27 })
28 if err != nil {
29 t.Fatal(err)
30 }
31 r := lossForObservation(t, o, "participant", 1, "box-b", LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{"kind":"participant-lost"}`)})
32 if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), r); err != nil {
33 t.Fatal(err)
34 }
35 o, err = DeclareLost(t.Context(), s, pub, o, r.ID, testLossOwner{})
36 if err != nil {
37 t.Fatal(err)
38 }
39 h := o.Claim.Holder
40 _, 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"}))
41 if target && !errors.Is(err, ErrLossRecord) || !target && err != nil {
42 t.Fatalf("admission: %v", err)
43 }
44 })
45 }
46}
47
48func TestPlan12DeclareLostRefusesSecondSameOperation(t *testing.T) {
49 for _, which := range []string{"different-box", "new-loss-id", "revoke-execution", "local-only-different-box"} {
50 t.Run(which, func(t *testing.T) {
51 s := release.NewDirStore(t.TempDir())
52 pub, priv, err := ed25519.GenerateKey(nil)
53 if err != nil {
54 t.Fatal(err)
55 }
56 o := servingClaim(t, t.Context(), s)
57 o, err = Update(t.Context(), s, o, func(c *Claim) error {
58 c.Op = Op{ID: "move", Kind: KindLanding, Phase: PhaseProbing, Exec: "box-a", Participants: []string{"box-a", "box-b", "box-c"}}
59 c.Obligations = []Obligation{{ID: "local", Op: "move", Kind: "park-local", Box: "box-c", Retirement: "ret", Owner: "box:box-c"}}
60 return nil
61 })
62 if err != nil {
63 t.Fatal(err)
64 }
65 first := lossForObservation(t, o, "first", 1, "box-b", LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{}`)})
66 if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), first); err != nil {
67 t.Fatal(err)
68 }
69 o, err = DeclareLost(t.Context(), s, pub, o, first.ID, testLossOwner{})
70 if err != nil {
71 t.Fatal(err)
72 }
73 box := "box-c"
74 effects := LossConsequences{RemoveParticipation: true, RecoveryIntent: []byte(`{}`)}
75 switch which {
76 case "new-loss-id":
77 box, effects.RemoveParticipation = "box-b", false
78 case "revoke-execution":
79 box, effects.RevokeExecution = "box-a", true
80 case "local-only-different-box":
81 effects = LossConsequences{
82 DischargeLocal: []ObligationRef{{ID: "local", Op: "move", Retirement: "ret"}},
83 DestroyedStorage: []string{"move/local/ret"}, RecoveryIntent: []byte(`{"kind":"destroyed-local-only"}`),
84 }
85 }
86 second := lossForObservation(t, o, "second", 2, box, effects)
87 if _, err = CreateLossRecord(t.Context(), s, release.RootSigner(priv), second); err != nil {
88 t.Fatal(err)
89 }
90 before := make(map[string]ObservedLossRecord)
91 for _, id := range []string{first.ID, second.ID} {
92 r, err := ReadLossRecord(t.Context(), s, pub, id)
93 if err != nil {
94 t.Fatal(err)
95 }
96 before[id] = r
97 }
98 if _, err = DeclareLost(t.Context(), s, pub, o, second.ID, testLossOwner{}); !errors.Is(err, ErrWrongState) {
99 t.Fatalf("second declaration: %v", err)
100 }
101 fresh, err := Read(t.Context(), s, o.Host)
102 if err != nil || fresh.ETag != o.ETag || !reflect.DeepEqual(fresh.Claim, o.Claim) {
103 t.Fatal("refusal mutated claim or obligations", err)
104 }
105 for id, prior := range before {
106 after, err := ReadLossRecord(t.Context(), s, pub, id)
107 if err != nil || !reflect.DeepEqual(prior, after) {
108 t.Fatal("refusal mutated loss record", id, err)
109 }
110 }
111 })
112 }
113}
114
115func TestPlan12OperationLossGateIgnoresOtherParticipants(t *testing.T) {
116 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"}}
117 for i := 0; i < 100; i++ {
118 id, err := OperationLossID(c)
119 if err != nil || id != "target" {
120 t.Fatalf("loss selection %q %v", id, err)
121 }
122 }
123}
124
125func TestPlan12OperationLossGateIncludesRevokedExecutor(t *testing.T) {
126 c := &Claim{Op: Op{ID: "move", Intent: map[string]string{"target": "target"}, Journal: map[string]string{
127 "loss.executor": "{}", "loss_gate.executor": `{"box":"source","revoke_execution":true}`,
128 }}, LossRefs: []string{"executor"}}
129 if id, err := OperationLossID(c); err != nil || id != "executor" {
130 t.Fatalf("revoked executor loss selection %q %v", id, err)
131 }
132}
133