package hostclaim import ( "encoding/json" "errors" "testing" "time" ) func TestAcknowledgeExecutionWritesExactJournalValue(t *testing.T) { expires := time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC) ack := ExecutionAcknowledgement{Op: "op-move", Enrollment: "enrollment-1", BootID: "boot-1", ControllerID: "controller-1", LeaseETag: `"lease-etag"`, Expires: expires} c := &Claim{Op: Op{Kind: KindMoving, ID: "op-move", Participants: []string{"box-a"}}} if err := AcknowledgeExecution("box-a", ack)(c); err != nil { t.Fatal(err) } want, _ := json.Marshal(ack) if got := c.Op.Journal["exec_ack.box-a"]; got != string(want) { t.Fatalf("journal = %q, want %q", got, want) } if err := AcknowledgeExecution("box-a", ack)(c); !errors.Is(err, ErrAckAlreadyApplied) { t.Fatalf("idempotent retry = %v", err) } changed := ack changed.LeaseETag = `"other"` if err := AcknowledgeExecution("box-a", changed)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("conflicting ack = %v", err) } } func TestAcknowledgeExecutionRefusesServingAndNonparticipants(t *testing.T) { ack := ExecutionAcknowledgement{Op: "op", Enrollment: "e", BootID: "b", ControllerID: "c", LeaseETag: "tag", Expires: time.Now()} for name, c := range map[string]*Claim{ "serving": {Op: Op{Kind: KindServing, Participants: []string{"box"}}}, "nonparticipant": {Op: Op{Kind: KindMoving, ID: "op", Participants: []string{"other"}}}, } { t.Run(name, func(t *testing.T) { if err := AcknowledgeExecution("box", ack)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("error = %v", err) } }) } } // An acknowledgement may only be extended by the controller that wrote it. // The console's completion check reads the latest expiry, so a participant // keeps it fresh while a create, delete or purge waits for recovery. func TestAcknowledgeExecutionExtendsOnlyTheSameLease(t *testing.T) { expires := time.Date(2026, 10, 20, 12, 0, 0, 0, time.UTC) first := ExecutionAcknowledgement{Op: "op", Enrollment: "enrollment-1", BootID: "boot-1", ControllerID: "controller-1", LeaseETag: `"lease-1"`, Expires: expires} acked := func(t *testing.T) *Claim { t.Helper() c := &Claim{Op: Op{Kind: KindCreating, ID: "op", Participants: []string{"box-a"}}} if err := AcknowledgeExecution("box-a", first)(c); err != nil { t.Fatal(err) } return c } t.Run("extension replaces", func(t *testing.T) { c := acked(t) next := first next.LeaseETag, next.Expires = `"lease-2"`, expires.Add(20*time.Second) if err := AcknowledgeExecution("box-a", next)(c); err != nil { t.Fatal(err) } want, _ := json.Marshal(next) if got := c.Op.Journal["exec_ack.box-a"]; got != string(want) { t.Fatalf("journal = %q, want %q", got, want) } if err := AcknowledgeExecution("box-a", next)(c); !errors.Is(err, ErrAckAlreadyApplied) { t.Fatalf("replayed extension = %v", err) } if err := AcknowledgeExecution("box-a", first)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("older acknowledgement after an extension = %v", err) } }) for name, edit := range map[string]func(*ExecutionAcknowledgement){ "other boot": func(a *ExecutionAcknowledgement) { a.BootID = "boot-2" }, "other controller": func(a *ExecutionAcknowledgement) { a.ControllerID = "controller-2" }, "other enrollment": func(a *ExecutionAcknowledgement) { a.Enrollment = "enrollment-2" }, "equal expiry": func(a *ExecutionAcknowledgement) { a.Expires = expires }, "earlier expiry": func(a *ExecutionAcknowledgement) { a.Expires = expires.Add(-time.Second) }, } { t.Run(name, func(t *testing.T) { c := acked(t) before := c.Op.Journal["exec_ack.box-a"] next := first next.LeaseETag, next.Expires = `"lease-2"`, expires.Add(20*time.Second) edit(&next) if err := AcknowledgeExecution("box-a", next)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("error = %v", err) } if c.Op.Journal["exec_ack.box-a"] != before { t.Fatal("refused acknowledgement replaced the journal") } }) } t.Run("other op", func(t *testing.T) { // The journal entry belongs to the claim's current op; an // acknowledgement naming another op is refused before any comparison. c := acked(t) next := first next.Op, next.Expires = "other-op", expires.Add(20*time.Second) if err := AcknowledgeExecution("box-a", next)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("error = %v", err) } // A journalled acknowledgement for another op (a stale entry) is never extended. stale := first stale.Op = "older-op" raw, _ := json.Marshal(stale) c.Op.Journal["exec_ack.box-a"] = string(raw) next.Op = "op" if err := AcknowledgeExecution("box-a", next)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("stale op entry = %v", err) } }) t.Run("missing lease etag", func(t *testing.T) { c := acked(t) next := first next.LeaseETag, next.Expires = "", expires.Add(20*time.Second) if err := AcknowledgeExecution("box-a", next)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("error = %v", err) } }) for name, previous := range map[string]string{ "undecodable": "{not json", "unknown field": `{"op":"op","enrollment":"enrollment-1","boot_id":"boot-1","controller_id":"controller-1","lease_etag":"x","expires":"2026-10-20T12:00:00Z","extra":1}`, "trailing data": `{"op":"op","enrollment":"enrollment-1","boot_id":"boot-1","controller_id":"controller-1","lease_etag":"x","expires":"2026-10-20T12:00:00Z"} {}`, } { t.Run(name, func(t *testing.T) { c := &Claim{Op: Op{Kind: KindCreating, ID: "op", Participants: []string{"box-a"}, Journal: map[string]string{"exec_ack.box-a": previous}}} next := first next.LeaseETag, next.Expires = `"lease-2"`, expires.Add(20*time.Second) if err := AcknowledgeExecution("box-a", next)(c); !errors.Is(err, ErrWrongState) { t.Fatalf("error = %v", err) } if c.Op.Journal["exec_ack.box-a"] != previous { t.Fatal("refused acknowledgement replaced the journal") } }) } }