Skip to content

Commit 7a19662

Browse files
fix(auth): Recover OAuth device approval retries (#128287)
OAuth device approval errors can ask users to start over while hiding the recovery action. Allow failed decisions, including HTTP 400 session errors, to prepare fresh consent. Keep code entry in its preparing state while retrying a failed context request, whether through resubmission or retry. Adds regression coverage for both recovery paths. Follow-up to #128124 addressing the remaining review findings.
1 parent ecd4450 commit 7a19662

2 files changed

Lines changed: 79 additions & 2 deletions

File tree

‎static/app/views/authV2/oauthDevice/useOAuthDeviceFlow.spec.tsx‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,83 @@ describe('useOAuthDeviceFlow', () => {
330330
);
331331
});
332332

333+
it.each(['enterCode', 'retry'] as const)(
334+
'shows code preparation while %s retries a failed context request',
335+
async action => {
336+
MockApiClient.addMockResponse({
337+
url: '/auth/oauth/device/context/',
338+
statusCode: 400,
339+
body: {detail: 'Invalid or expired code.'},
340+
});
341+
const {result} = renderHookWithProviders(useOAuthDeviceFlow, {
342+
initialRouterConfig: {location},
343+
});
344+
345+
await waitFor(() =>
346+
expect(result.current.state).toMatchObject({message: 'Invalid or expired code.'})
347+
);
348+
349+
const delay = Promise.withResolvers<void>();
350+
MockApiClient.addMockResponse({
351+
url: '/auth/oauth/device/context/',
352+
body: {...OAuthContextFixture(), userCode: 'ABCD-EFGH'},
353+
asyncDelay: delay.promise,
354+
});
355+
356+
act(() => result.current.actions[action]('ABCD-EFGH'));
357+
358+
await waitFor(() =>
359+
expect(result.current.state).toMatchObject({
360+
status: 'code-entry',
361+
isPreparingCode: true,
362+
})
363+
);
364+
365+
await act(() => {
366+
delay.resolve();
367+
return delay.promise;
368+
});
369+
370+
await waitFor(() => expect(result.current.state.status).toBe('consent'));
371+
}
372+
);
373+
374+
it('prepares fresh consent after a rejected decision with a valid code', async () => {
375+
MockApiClient.addMockResponse({
376+
url: '/auth/oauth/device/',
377+
method: 'POST',
378+
statusCode: 400,
379+
body: {detail: 'Please start over.'},
380+
});
381+
const {result} = renderHookWithProviders(useOAuthDeviceFlow, {
382+
initialRouterConfig: {location},
383+
});
384+
385+
await waitFor(() => expect(result.current.state.status).toBe('consent'));
386+
387+
act(() => result.current.actions.decide('approve'));
388+
389+
await waitFor(() =>
390+
expect(result.current.state).toMatchObject({
391+
status: 'error',
392+
canRetry: true,
393+
message: 'Please start over.',
394+
})
395+
);
396+
397+
const prepare = MockApiClient.addMockResponse({
398+
url: '/auth/oauth/device/',
399+
body: consent,
400+
});
401+
402+
act(() => result.current.actions.retry());
403+
404+
await waitFor(() =>
405+
expect(result.current.state).toMatchObject({status: 'consent', consent})
406+
);
407+
expect(prepare).toHaveBeenCalledTimes(1);
408+
});
409+
333410
it('retries a transient consent failure without asking for the code again', async () => {
334411
MockApiClient.addMockResponse({
335412
url: '/auth/oauth/device/',

‎static/app/views/authV2/oauthDevice/useOAuthDeviceFlow.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ export function useOAuthDeviceFlow() {
158158
context: undefined,
159159
status: 'code-entry',
160160
userCode,
161-
isPreparingCode: false,
161+
isPreparingCode: contextQuery.isFetching,
162162
message: getRequestErrorUserMessage(contextQuery.error),
163163
} as const,
164164
actions,
@@ -190,7 +190,7 @@ export function useOAuthDeviceFlow() {
190190
...shared,
191191
status: 'error',
192192
message: getRequestErrorUserMessage(decision.error ?? consentQuery.error),
193-
canRetry: canRetryOAuthRequest(decision.error ?? consentQuery.error),
193+
canRetry: decision.isError || canRetryOAuthRequest(consentQuery.error),
194194
} as const,
195195
actions,
196196
};

0 commit comments

Comments
 (0)