Serialize Google connect and disconnect; consume connect state once
CI / ci (push) Successful in 3m32s
CI / ci (pull_request) Successful in 4m2s

A disconnect read the stored refresh token, revoked it at Google, then
deleted the row only if it still held that token. Google revokes the whole
grant, so a reconnect whose token was issued before that revoke was kept
as connected with a dead token. Connect's exchange-and-store and
disconnect's read-revoke-delete now hold one mutex across the Google
request and the database write, and the delete is unconditional (the
conditional delete and ErrChanged are gone: the lock is the one guard).

The connect cookie was single-use only in that the browser was asked to
drop it; a saved copy verified again until it expired. The service now
keeps each started connect's state in memory until the cookie expires and
the callback consumes it, so a replayed cookie is refused as bad state.

The fake Google now issues a distinct refresh token per exchange, revokes
the whole grant like Google, and can hold a token or revoke request so
tests can interleave a reconnect and a disconnect.

Maestro task 1e9d7850-3a09-4572-b8a1-cb18cd4774cb (MQ125-01, MQ125-02).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-07 20:46:35 -04:00
co-authored by Claude Opus 5.5
parent 59d0f57a47
commit 2cd2e5af32
9 changed files with 276 additions and 43 deletions
+2 -5
View File
@@ -106,12 +106,9 @@ ON CONFLICT (id) DO UPDATE SET
updated_at = excluded.updated_at
RETURNING *;
-- name: DeleteIntegrationsGoogle :execrows
-- Deletes the row only while it still holds refresh_token_ct, so a
-- connection made after the caller read the row is kept. Returns the number
-- of rows deleted.
-- name: DeleteIntegrationsGoogle :exec
DELETE FROM integrations_google
WHERE id = 1 AND refresh_token_ct = ?;
WHERE id = 1;
-- name: GetChatSettings :one
-- Returns sql.ErrNoRows until the row exists.