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
+5 -11
View File
@@ -10,20 +10,14 @@ import (
"database/sql"
)
const deleteIntegrationsGoogle = `-- name: DeleteIntegrationsGoogle :execrows
const deleteIntegrationsGoogle = `-- name: DeleteIntegrationsGoogle :exec
DELETE FROM integrations_google
WHERE id = 1 AND refresh_token_ct = ?
WHERE id = 1
`
// 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.
func (q *Queries) DeleteIntegrationsGoogle(ctx context.Context, refreshTokenCt []byte) (int64, error) {
result, err := q.db.ExecContext(ctx, deleteIntegrationsGoogle, refreshTokenCt)
if err != nil {
return 0, err
}
return result.RowsAffected()
func (q *Queries) DeleteIntegrationsGoogle(ctx context.Context) error {
_, err := q.db.ExecContext(ctx, deleteIntegrationsGoogle)
return err
}
const getAISettings = `-- name: GetAISettings :one