Skip to content

Commit c75820d

Browse files
committed
add better comments to tests
1 parent 810f89d commit c75820d

File tree

2 files changed

+59
-16
lines changed

2 files changed

+59
-16
lines changed

Diff for: src/browser/sync/authentication_manager.ts

-1
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,6 @@ export class AuthenticationManager {
223223
// don't retry with bad auth.
224224
private async tryToReauthenticate(serverMessage: AuthError) {
225225
this._logVerbose(`attempting to reauthenticate: ${serverMessage.error}`);
226-
// We only retry once, to avoid infinite retries
227226
if (
228227
// No way to fetch another token, kaboom
229228
this.authState.state === "noAuth" ||

Diff for: src/react/auth_websocket.test.tsx

+59-15
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ const testReactClient = (address: string) =>
2222
// https://linear.app/convex/issue/ENG-7052/re-enable-auth-websocket-client-tests
2323

2424
// On Linux these can retry forever due to EADDRINUSE so run then sequentially.
25-
describe.sequential.skip("auth websocket tests", () => {
25+
describe.sequential("auth websocket tests", () => {
2626
// This is the path usually taken on page load after a user logged in,
2727
// with a constant token provider.
2828
test("Authenticate via valid static token", async () => {
@@ -357,6 +357,8 @@ describe.sequential.skip("auth websocket tests", () => {
357357
});
358358
});
359359

360+
// This is a race condition where a delayed auth error from a non-auth message
361+
// comes back while the client is waiting for server validation of the new token.
360362
test("Client ignores non-auth responses for token validation", async () => {
361363
await withInMemoryWebSocket(async ({ address, receive, send }) => {
362364
const client = testReactClient(address);
@@ -390,9 +392,8 @@ describe.sequential.skip("auth websocket tests", () => {
390392

391393
expect((await receive()).type).toEqual("Authenticate");
392394

393-
// In this race condition, a delayed auth error from a non-auth message
394-
// comes back while the client is waiting for server validation of
395-
// the new token.
395+
// This auth error text is specific to query/mutation/action related auth
396+
// errors, which should be ignored for token validation.
396397
send({
397398
type: "AuthError",
398399
error: "Convex token identity expired",
@@ -429,20 +430,27 @@ describe.sequential.skip("auth websocket tests", () => {
429430
});
430431
});
431432

433+
// This is a race condition where a connection stopped by reauthentication
434+
// never restarts due to reauthentication exiting early. This happens when
435+
// an additional refetch begins while reauthentication is still running,
436+
// such as with a scheduled refetch.
432437
test("Client maintains connection when refetch occurs during reauth attempt", async () => {
433438
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
434439
vi.useFakeTimers();
435440
const client = testReactClient(address);
436-
const ts = Math.ceil(Date.now() / 1000);
441+
// Tokens have a 3 second expiration, scheduled refetch occurs 2 seconds
442+
// prior to expiration (so 1 second after token validation completes).
437443
const tokens = [
438-
jwtEncode({ iat: ts, exp: ts + 3 }, "token1"),
439-
jwtEncode({ iat: ts, exp: ts + 3 }, "token2"),
440-
jwtEncode({ iat: ts, exp: ts + 3 }, "token3"),
441-
jwtEncode({ iat: ts, exp: ts + 3 }, "token4"),
444+
(ts: number) => jwtEncode({ iat: ts, exp: ts + 3 }, "token1"),
445+
(ts: number) => jwtEncode({ iat: ts, exp: ts + 3 }, "token2"),
446+
(ts: number) => jwtEncode({ iat: ts, exp: ts + 3 }, "token3"),
447+
(ts: number) => jwtEncode({ iat: ts, exp: ts + 3 }, "token4"),
442448
];
443449
const tokenFetcher = vi.fn(async (_opts) => {
450+
// Simulate a one second delay in token fetching - long enough to
451+
// cause a scheduled refetch to occur while reauth is still running.
444452
vi.advanceTimersByTime(1000);
445-
return tokens.shift()!;
453+
return tokens.shift()!(Math.ceil(Date.now() / 1000));
446454
});
447455
const onChange = vi.fn();
448456
client.setAuth(tokenFetcher, onChange);
@@ -482,18 +490,29 @@ describe.sequential.skip("auth websocket tests", () => {
482490
});
483491

484492
// A race condition where a user triggered query with a newly stale
485-
// token gets an auth error while reauth is refetching a fresh token.
493+
// token triggers an auth error.
486494
send({
487495
type: "AuthError",
488496
error: "Convex token identity expired",
489497
baseVersion: 2,
490498
});
491499
close();
492500

501+
// The error has now caused reauthentication to begin. Reauth stops the
502+
// connection. The scheduled refetch will have started during the
503+
// 1000ms token fetcher delay, causing the reauth attempt to exit early
504+
// and never restart the connection.
505+
//
506+
// This is the race condition this test covers. The scheduled refetch
507+
// previously would fetch a new token but never restart the connection
508+
// stopped by reauth.
493509
expect((await receive()).type).toEqual("Connect");
494510
expect((await receive()).type).toEqual("Authenticate");
495511
expect((await receive()).type).toEqual("ModifyQuerySet");
496512

513+
// If the connection is successfully restarted, the client will receive
514+
// the following Transition message and call onChange a second time with
515+
// true for `isAuthenticated`.
497516
send({
498517
type: "Transition",
499518
startVersion: {
@@ -510,33 +529,50 @@ describe.sequential.skip("auth websocket tests", () => {
510529
await client.close();
511530

512531
expect(tokenFetcher).toHaveBeenCalledTimes(4);
532+
// Initial setConfig
513533
expect(tokenFetcher).toHaveBeenNthCalledWith(1, {
514534
forceRefreshToken: false,
515535
});
536+
// Initial fresh token fetch
516537
expect(tokenFetcher).toHaveBeenNthCalledWith(2, {
517538
forceRefreshToken: true,
518539
});
540+
// Reauth attempt
519541
expect(tokenFetcher).toHaveBeenNthCalledWith(3, {
520542
forceRefreshToken: true,
521543
});
544+
// Scheduled refetch
522545
expect(tokenFetcher).toHaveBeenNthCalledWith(4, {
523546
forceRefreshToken: true,
524547
});
548+
549+
// Confirm that auth state changed exactly twice, and was never
550+
// set to false.
525551
expect(onChange).toHaveBeenCalledTimes(2);
552+
// Initial setConfig
526553
expect(onChange).toHaveBeenNthCalledWith(1, true);
554+
// Refetch after reauth
527555
expect(onChange).toHaveBeenNthCalledWith(2, true);
528556
vi.useRealTimers();
529557
});
530558
});
531559

560+
// When awaiting server confirmation of a fresh token, a subsequent
561+
// auth error (from an Authenticate request) will cause the client to go to
562+
// an unauthenticated state. This test covers a race condition where an
563+
// Authenticate request for a fresh token is sent, and then the client app
564+
// goes to background and misses the Transition response. If the client
565+
// becomes active after the new token has expired, a new Authenticate request
566+
// will be sent with the expired token, leading to an error response and
567+
// unauthenticated state.
532568
test("Client retries token validation on error", async () => {
533569
await withInMemoryWebSocket(async ({ address, receive, send, close }) => {
534570
const client = testReactClient(address);
535571
const ts = Math.ceil(Date.now() / 1000);
536572
const tokens = [
537-
jwtEncode({ iat: ts, exp: ts + 1000 }, "token1"),
538-
jwtEncode({ iat: ts, exp: ts + 1000 }, "token2"),
539-
jwtEncode({ iat: ts, exp: ts + 1000 }, "token3"),
573+
jwtEncode({ iat: ts, exp: ts + 60 }, "token1"),
574+
jwtEncode({ iat: ts, exp: ts + 60 }, "token2"),
575+
jwtEncode({ iat: ts, exp: ts + 60 }, "token3"),
540576
];
541577
const tokenFetcher = vi.fn(async (_opts) => tokens.shift()!);
542578
const onChange = vi.fn();
@@ -563,14 +599,16 @@ describe.sequential.skip("auth websocket tests", () => {
563599

564600
expect((await receive()).type).toEqual("Authenticate");
565601

566-
// Simulating an unexpected network error
602+
// Simulating an auth error while waiting for server confirmation of a
603+
// fresh token.
567604
send({
568605
type: "AuthError",
569606
error: "bla",
570607
baseVersion: 1,
571608
});
572609
close();
573610

611+
// The client should reattempt reauthentication.
574612
expect((await receive()).type).toEqual("Connect");
575613
expect((await receive()).type).toEqual("Authenticate");
576614
expect((await receive()).type).toEqual("ModifyQuerySet");
@@ -588,21 +626,27 @@ describe.sequential.skip("auth websocket tests", () => {
588626
modifications: [],
589627
});
590628

629+
// Flush
591630
await new Promise((resolve) => setTimeout(resolve));
592631
await client.close();
593632

594633
expect(tokenFetcher).toHaveBeenCalledTimes(3);
634+
// Initial setConfig
595635
expect(tokenFetcher).toHaveBeenNthCalledWith(1, {
596636
forceRefreshToken: false,
597637
});
638+
// Initial fresh token fetch
598639
expect(tokenFetcher).toHaveBeenNthCalledWith(2, {
599640
forceRefreshToken: true,
600641
});
642+
// Reauth second attempt
601643
expect(tokenFetcher).toHaveBeenNthCalledWith(3, {
602644
forceRefreshToken: true,
603645
});
604646
expect(onChange).toHaveBeenCalledTimes(2);
647+
// Initial setConfig
605648
expect(onChange).toHaveBeenNthCalledWith(1, true);
649+
// Reauth second attempt
606650
expect(onChange).toHaveBeenNthCalledWith(2, true);
607651
});
608652
});

0 commit comments

Comments
 (0)