package hostops import ( "context" "crypto/ed25519" "errors" "testing" "time" "github.com/carlosframework/platform/internal/hostclaim" "github.com/carlosframework/platform/internal/release" ) // M7: a reset that timed out is uncertain, then expires and is archived. The // archive closes that request's id, but it must not pin the claim ETag for // ever: a later issue gets a fresh request. Absence of a receipt never becomes // proof, and an archived request with a valid root receipt is proof. func TestPlan9M7TimedOutResetDoesNotPinClaimETag(t *testing.T) { for _, proved := range []bool{false, true} { name := "uncertain" if proved { name = "proved-before-archive" } t.Run(name, func(t *testing.T) { tr, r, o, boxkey := resetTransportFixture(t) now := tr.Now() tr.Now = func() time.Time { return now } w, err := NewControlWriter(tr.Store, tr.Directory.PublicKey, tr.Now) if err != nil { t.Fatal(err) } signer := release.RootSigner(ed25519.NewKeyFromSeed(make([]byte, 32))) h := o.Claim.Holder owner := r.Admission.Owner // The fixture's live registration, renewed as its console would. handle := &StartHandle{w: w, account: h.Account, app: h.App, appID: "app", id: "reset", owner: owner, epoch: 1, kind: RegistrationWakeRetryReset, incarnation: h.Incarnation} grant := lifecycleGrant{kind: "app", account: h.Account, app: h.App, appID: "app", operation: "reset", owner: owner, epoch: 1, w: w, registration: RegistrationWakeRetryReset, incarnation: h.Incarnation} ctx := withLifecycleGrant(t.Context(), grant) issue := func() (WakeResetRequest, error) { return w.RequestWakeReset(ctx, tr, signer, r.Console, r.ConsoleEnrollment, r.Namespace, o, r.Attempt) } first, err := issue() if err != nil { t.Fatal(err) } if proved { publishResetProof(t, tr, first, boxkey) } for _, step := range []time.Duration{80 * time.Second, 80 * time.Second, 60 * time.Second, 25 * time.Second} { now = now.Add(step) if err := handle.Renew(t.Context(), now); err != nil { t.Fatal(err) } } if !proved { if err := tr.Wait(t.Context(), first, time.Millisecond); !errors.Is(err, ErrWakeResetUncertain) { t.Fatalf("expired without receipt = %v", err) } } if err := w.SweepWakeResets(t.Context(), tr); err != nil { t.Fatal(err) } if _, _, err := tr.Store.Get(t.Context(), wakeResetArchiveKey(first)); err != nil { t.Fatalf("not archived: %v", err) } again, err := issue() if err != nil { t.Fatalf("archived request pinned the claim ETag: %v", err) } if proved { if err := tr.Wait(t.Context(), again, time.Millisecond); err != nil { t.Fatalf("archived proof lost: %v", err) } return } if again.ID == first.ID || !again.Expires.After(now) || again.ClaimETag != first.ClaimETag { t.Fatalf("reissue reused the archived request: %+v", again) } if err := tr.Authorize(t.Context(), again); err != nil { t.Fatalf("fresh request refused: %v", err) } if err := tr.Authorize(t.Context(), first); err == nil { t.Fatal("archived request resurrected") } short, cancel := context.WithTimeout(t.Context(), 20*time.Millisecond) defer cancel() if err := tr.Wait(short, again, time.Millisecond); !errors.Is(err, ErrWakeResetUncertain) { t.Fatalf("absence became proof: %v", err) } replay, err := issue() if err != nil || replay != again { t.Fatalf("replay=%+v err=%v", replay, err) } calls := 0 stop := func(_ context.Context, _ hostclaim.Observed, a hostclaim.WakeRetryAttempt) error { calls++ publishResetProof(t, tr, again, boxkey) return nil } if err := tr.Pass(t.Context(), "box", stop); err != nil || calls != 1 { t.Fatalf("fresh request not dispatched: calls=%d %v", calls, err) } if err := tr.Wait(t.Context(), again, time.Millisecond); err != nil { t.Fatal(err) } }) } }