Serialize Google connect and disconnect; consume connect state once
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:
@@ -90,9 +90,6 @@ func deleteGoogle(logger *slog.Logger, svc *gcal.Service) http.HandlerFunc {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
case errors.Is(err, gcal.ErrNotConnected):
|
||||
writeError(w, r, logger, http.StatusConflict, "not_connected", "Google Calendar is not connected")
|
||||
case errors.Is(err, gcal.ErrChanged):
|
||||
writeError(w, r, logger, http.StatusConflict, "conflict",
|
||||
"the Google Calendar connection changed; reload and try again")
|
||||
case errors.As(err, &revoke):
|
||||
logger.WarnContext(r.Context(), "revoke Google access", "error", err)
|
||||
writeError(w, r, logger, http.StatusBadGateway, "revoke_failed",
|
||||
|
||||
Reference in New Issue
Block a user