Skip to content

Commit 2faf340

Browse files
authored
test: E2E ログインでトークン実効性を検証し logout 無効化窓の flake を解消 (closes #142) (#143)
1 parent e4492ec commit 2faf340

2 files changed

Lines changed: 22 additions & 3 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@
77

88
## [Unreleased]
99

10+
### 2026-07-14
11+
- **Test**: E2E の auth 系シナリオ (whoami / auto-refresh) が間欠的に 401 で落ちる flake を修正 (#142, #143)
12+
- 真因はサーバーの logout が「現在秒 +1 まで」の全トークンを無効化するため、直後 (同一壁時計秒) に正規ログインで発行された新トークンまで無効化窓に入ること。ローカルモードでは無効化レコードがインメモリ保持のため、E2E の DB クリーンアップでは除去できない
13+
- `loginAs` で取得したトークンを `/me` で実効性検証し、無効化窓を跨ぐバックオフ (計 2 秒) 付きでリトライするように変更
14+
1015
## [0.17.0] - 2026-06-12
1116

1217
### 2026-06-10

tests/e2e/support/world.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,17 @@ setWorldConstructor(GdbWorld);
114114

115115
/**
116116
* Internal helper: login with given credentials and write the token to CLI config.
117-
* Retries up to 3 times to handle race conditions where the DB was just
118-
* cleared and the server hasn't yet recreated users.
117+
* Retries to handle two failure windows:
118+
* - the DB was just cleared and the server hasn't yet recreated users
119+
* - the server's logout invalidates ALL of the user's tokens issued in the
120+
* same wall-clock second (`invalidatedBefore = now + 1s`), so a token minted
121+
* right after a logout in a preceding scenario is dead on arrival even
122+
* though the login itself succeeded (#142). The token is therefore verified
123+
* against /me before being handed to the scenario; the backoff spans more
124+
* than one second so at least one attempt lands past the invalidation window.
119125
*/
120126
async function loginAs(world: GdbWorld, email: string, password: string): Promise<Record<string, unknown>> {
121-
const maxRetries = 3;
127+
const maxRetries = 5;
122128
let lastError: Error | undefined;
123129

124130
for (let attempt = 0; attempt < maxRetries; attempt++) {
@@ -142,6 +148,14 @@ async function loginAs(world: GdbWorld, email: string, password: string): Promis
142148
continue;
143149
}
144150

151+
const meRes = await fetch(new URL("/me", world.serverUrl).toString(), {
152+
headers: { Authorization: `Bearer ${token}` },
153+
});
154+
if (!meRes.ok) {
155+
lastError = new Error(`Fresh token rejected by /me: HTTP ${meRes.status} (token invalidation window, see #142)`);
156+
continue;
157+
}
158+
145159
const profile: Record<string, unknown> = { url: world.serverUrl, token };
146160
if (data.refreshToken) profile.refreshToken = data.refreshToken;
147161
world.writeConfig({

0 commit comments

Comments
 (0)