Skip to content

Commit fe04136

Browse files
author
Ajit Kumar
committed
fix: greprile review
1 parent 8782286 commit fe04136

2 files changed

Lines changed: 152 additions & 15 deletions

File tree

src/githubAccount.js

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -168,27 +168,30 @@ export class GitHubAccountController {
168168
for (const listener of this.#listeners) listener(account);
169169
}
170170

171-
async #runLegacyOperation(operation, token) {
171+
async #runLegacyOperation(operation, token, epoch) {
172172
let result;
173173
try {
174174
result = await operation(this.#createGitHub(token));
175175
} catch (error) {
176176
if (isAuthenticationError(error)) {
177-
this.#credentialEpoch += 1;
178-
this.#legacyToken = '';
179-
this.#account = null;
180-
this.#setAccount(null);
177+
if (!this.#invalidateLegacyToken(token, epoch)) {
178+
throw new GitHubAuthError('cancelled');
179+
}
181180
throw new GitHubAuthError('invalid-token');
182181
}
183182
throw error;
184183
}
185-
this.#finalizeLegacyToken(token);
184+
this.#finalizeLegacyToken(token, epoch);
186185
return result;
187186
}
188187

189188
#runOperation(operation, write) {
190189
if (this.#legacyToken) {
191-
return this.#runLegacyOperation(operation, this.#legacyToken);
190+
return this.#runLegacyOperation(
191+
operation,
192+
this.#legacyToken,
193+
this.#credentialEpoch,
194+
);
192195
}
193196
return this.#authManager.runWithToken(
194197
(token) => operation(this.#createGitHub(token)),
@@ -221,9 +224,14 @@ export class GitHubAccountController {
221224
}
222225
}
223226

224-
#finalizeLegacyToken(token) {
225-
if (this.#legacyMigration || token !== this.#legacyToken) return;
226-
const epoch = this.#credentialEpoch;
227+
#finalizeLegacyToken(token, epoch) {
228+
if (
229+
this.#legacyMigration ||
230+
epoch !== this.#credentialEpoch ||
231+
token !== this.#legacyToken
232+
) {
233+
return;
234+
}
227235
const migration = this.#authManager
228236
.usePersonalAccessToken(token)
229237
.then((account) => {
@@ -248,10 +256,7 @@ export class GitHubAccountController {
248256
) {
249257
return;
250258
}
251-
this.#credentialEpoch += 1;
252-
this.#legacyToken = '';
253-
this.#account = null;
254-
this.#setAccount(null);
259+
this.#invalidateLegacyToken(token, epoch, false);
255260
})
256261
.finally(() => {
257262
if (this.#legacyMigration === migration) {
@@ -261,6 +266,25 @@ export class GitHubAccountController {
261266
this.#legacyMigration = migration;
262267
}
263268

269+
#invalidateLegacyToken(token, epoch, reportStorageError = true) {
270+
if (epoch !== this.#credentialEpoch || token !== this.#legacyToken) {
271+
return false;
272+
}
273+
274+
this.#credentialEpoch += 1;
275+
this.#legacyToken = '';
276+
this.#account = null;
277+
let cleanupError;
278+
try {
279+
this.#removeLegacyToken();
280+
} catch (error) {
281+
cleanupError = error;
282+
}
283+
this.#setAccount(null);
284+
if (cleanupError && reportStorageError) throw cleanupError;
285+
return true;
286+
}
287+
264288
#removeLegacyToken() {
265289
try {
266290
this.#legacyStore?.removeItem?.(LEGACY_TOKEN_KEY);

test/github-account.test.js

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,8 +340,121 @@ test('a confirmed invalid provisional PAT opens account recovery', async () => {
340340
);
341341

342342
assert.equal(await controller.getAccount(), null);
343-
assert.equal(values.get('github-token'), 'legacy-token');
343+
assert.equal(values.has('github-token'), false);
344344
assert.equal(accounts.at(-1), null);
345+
346+
const restarted = new GitHubAccountController({
347+
authManager,
348+
createGitHub: () => ({}),
349+
legacyStore: createStore(values),
350+
});
351+
assert.equal(await restarted.initialize(), null);
352+
},
353+
);
354+
});
355+
356+
test('an invalid background migration removes the legacy PAT permanently', async () => {
357+
const values = new Map([['github-token', 'legacy-token']]);
358+
const authManager = createAuthManager([]);
359+
authManager.usePersonalAccessToken = async () => {
360+
throw Object.assign(new Error('invalid token'), { kind: 'invalid-token' });
361+
};
362+
363+
await withSourceModule(
364+
'githubAccount.js',
365+
{ localStorage: createStore(values) },
366+
async ({ GitHubAccountController }) => {
367+
const controller = new GitHubAccountController({
368+
authManager,
369+
createGitHub: () => ({}),
370+
legacyStore: createStore(values),
371+
});
372+
await controller.initialize();
373+
374+
assert.equal(await controller.run(() => 'loaded'), 'loaded');
375+
await settle();
376+
377+
assert.equal(await controller.getAccount(), null);
378+
assert.equal(values.has('github-token'), false);
379+
380+
const restarted = new GitHubAccountController({
381+
authManager,
382+
createGitHub: () => ({}),
383+
legacyStore: createStore(values),
384+
});
385+
assert.equal(await restarted.initialize(), null);
386+
},
387+
);
388+
});
389+
390+
test('legacy PAT invalidation stays signed out when cleanup fails', async () => {
391+
const values = new Map([['github-token', 'legacy-token']]);
392+
const authManager = createAuthManager([]);
393+
const legacyStore = {
394+
...createStore(values),
395+
removeItem() {
396+
throw new Error('storage unavailable');
397+
},
398+
};
399+
400+
await withSourceModule(
401+
'githubAccount.js',
402+
{ localStorage: legacyStore },
403+
async ({ GitHubAccountController }) => {
404+
const controller = new GitHubAccountController({
405+
authManager,
406+
createGitHub: () => ({}),
407+
legacyStore,
408+
});
409+
await controller.initialize();
410+
411+
await assert.rejects(
412+
controller.run(() => {
413+
throw Object.assign(new Error('unauthorized'), { status: 401 });
414+
}),
415+
hasKind('storage'),
416+
);
417+
418+
assert.equal(await controller.getAccount(), null);
419+
assert.equal(values.get('github-token'), 'legacy-token');
420+
},
421+
);
422+
});
423+
424+
test('a stale legacy rejection cannot clear replacement credentials', async () => {
425+
const values = new Map([['github-token', 'legacy-token']]);
426+
const operationStarted = deferred();
427+
const releaseOperation = deferred();
428+
const authManager = createAuthManager([]);
429+
430+
await withSourceModule(
431+
'githubAccount.js',
432+
{ localStorage: createStore(values) },
433+
async ({ GitHubAccountController }) => {
434+
const controller = new GitHubAccountController({
435+
authManager,
436+
createGitHub: () => ({}),
437+
legacyStore: createStore(values),
438+
});
439+
const accounts = [];
440+
controller.subscribe((account) => accounts.push(account));
441+
await controller.initialize();
442+
443+
const stale = controller.run(async () => {
444+
operationStarted.resolve();
445+
await releaseOperation.promise;
446+
throw Object.assign(new Error('unauthorized'), { status: 401 });
447+
});
448+
const staleRejection = assert.rejects(stale, hasKind('cancelled'));
449+
await operationStarted.promise;
450+
451+
await controller.usePersonalAccessToken('replacement-token');
452+
releaseOperation.resolve();
453+
await staleRejection;
454+
455+
assert.equal((await controller.getAccount()).login, 'octocat');
456+
assert.equal(values.has('github-token'), false);
457+
assert.equal(accounts.at(-1).login, 'octocat');
345458
},
346459
);
347460
});

0 commit comments

Comments
 (0)