Loading internal/api/api_test.go +5 −5 Changes for internal/api/api_test.go: 5 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -1734,13 +1734,13 @@ func (f *fakeRepo) OpLoginRequestByID(_ context.Context, id string) (*OpLoginReq }, nil } // ConsumeOpLoginRequest stamps consumed on an unconsumed, unexpired request (the // finish path's single-use guard), mirroring the PG zero-rows-else UPDATE. The // approval gate is read by the handler BEFORE this call, so consume only checks // consumed_at and expiry (exactly as PG does). // ConsumeOpLoginRequest stamps consumed on an approved, unconsumed, unexpired // request (the finish path's single-use guard), mirroring the PG zero-rows-else // UPDATE. The handler reads the approval first too; the store refuses a pending // request on its own so the single-use guard never depends on that read. func (f *fakeRepo) ConsumeOpLoginRequest(_ context.Context, id string, now time.Time) error { r, ok := f.opLogins[id] if !ok || r.consumed || !r.expiresAt.After(now) { if !ok || r.status != "approved" || r.consumed || !r.expiresAt.After(now) { return ErrNotFound } r.consumed = true Loading internal/api/pgrepo.go +1 −1 Changes for internal/api/pgrepo.go: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -2948,7 +2948,7 @@ func (p *PGRepo) ApproveOpLogin(ctx context.Context, id, approverUserID string, func (p *PGRepo) ConsumeOpLoginRequest(ctx context.Context, id string, now time.Time) error { res, err := p.db.ExecContext(ctx, `UPDATE op_login_requests SET consumed_at = $2 WHERE id = $1 AND consumed_at IS NULL AND expires_at > $2`, WHERE id = $1 AND approved_at IS NOT NULL AND consumed_at IS NULL AND expires_at > $2`, id, now) if err != nil { return err Loading internal/pgint/pgint_test.go +9 −0 Changes for internal/pgint/pgint_test.go: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1260,6 +1260,15 @@ func TestOpLoginStateMachine(t *testing.T) { t.Fatal("fresh pending request missing from the list") } // A pending request is not a ticket: the store refuses to consume it on its own, // whatever the caller checked, and the refusal leaves it pending and unconsumed. if err := repo.ConsumeOpLoginRequest(ctx, id, now); !errors.Is(err, api.ErrNotFound) { t.Fatalf("consume pending = %v, want ErrNotFound", err) } if req, err := repo.OpLoginRequestByID(ctx, id); err != nil || req.Status != "pending" || req.Consumed { t.Fatalf("after refused consume = %+v, %v; want pending+unconsumed", req, err) } // Approve -> consume -> single use; second approve/consume are ErrNotFound. if err := repo.ApproveOpLogin(ctx, id, approver.ID, now); err != nil { t.Fatalf("ApproveOpLogin: %v", err) Loading Loading
internal/api/api_test.go +5 −5 Changes for internal/api/api_test.go: 5 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -1734,13 +1734,13 @@ func (f *fakeRepo) OpLoginRequestByID(_ context.Context, id string) (*OpLoginReq }, nil } // ConsumeOpLoginRequest stamps consumed on an unconsumed, unexpired request (the // finish path's single-use guard), mirroring the PG zero-rows-else UPDATE. The // approval gate is read by the handler BEFORE this call, so consume only checks // consumed_at and expiry (exactly as PG does). // ConsumeOpLoginRequest stamps consumed on an approved, unconsumed, unexpired // request (the finish path's single-use guard), mirroring the PG zero-rows-else // UPDATE. The handler reads the approval first too; the store refuses a pending // request on its own so the single-use guard never depends on that read. func (f *fakeRepo) ConsumeOpLoginRequest(_ context.Context, id string, now time.Time) error { r, ok := f.opLogins[id] if !ok || r.consumed || !r.expiresAt.After(now) { if !ok || r.status != "approved" || r.consumed || !r.expiresAt.After(now) { return ErrNotFound } r.consumed = true Loading
internal/api/pgrepo.go +1 −1 Changes for internal/api/pgrepo.go: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -2948,7 +2948,7 @@ func (p *PGRepo) ApproveOpLogin(ctx context.Context, id, approverUserID string, func (p *PGRepo) ConsumeOpLoginRequest(ctx context.Context, id string, now time.Time) error { res, err := p.db.ExecContext(ctx, `UPDATE op_login_requests SET consumed_at = $2 WHERE id = $1 AND consumed_at IS NULL AND expires_at > $2`, WHERE id = $1 AND approved_at IS NOT NULL AND consumed_at IS NULL AND expires_at > $2`, id, now) if err != nil { return err Loading
internal/pgint/pgint_test.go +9 −0 Changes for internal/pgint/pgint_test.go: 9 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -1260,6 +1260,15 @@ func TestOpLoginStateMachine(t *testing.T) { t.Fatal("fresh pending request missing from the list") } // A pending request is not a ticket: the store refuses to consume it on its own, // whatever the caller checked, and the refusal leaves it pending and unconsumed. if err := repo.ConsumeOpLoginRequest(ctx, id, now); !errors.Is(err, api.ErrNotFound) { t.Fatalf("consume pending = %v, want ErrNotFound", err) } if req, err := repo.OpLoginRequestByID(ctx, id); err != nil || req.Status != "pending" || req.Consumed { t.Fatalf("after refused consume = %+v, %v; want pending+unconsumed", req, err) } // Approve -> consume -> single use; second approve/consume are ErrNotFound. if err := repo.ApproveOpLogin(ctx, id, approver.ID, now); err != nil { t.Fatalf("ApproveOpLogin: %v", err) Loading