From badb00026906efd919eea8318daff9b7c0663ef9 Mon Sep 17 00:00:00 2001 From: kolaente Date: Fri, 19 Jun 2026 20:51:51 +0200 Subject: [PATCH] test(auth): actually exercise the Web Locks branch and assert session is preserved on retry Two test-quality fixes from review of #2948 (no implementation change): - auth.test.ts: the "Web Locks available" test never stubbed navigator.locks, which is undefined in happy-dom, so it silently ran the insecure-HTTP fallback branch instead. It now stubs a minimal navigator.locks.request shim, asserts that request was actually called with 'vikunja-token-refresh', and still asserts the in-flight dedup collapses concurrent calls into one POST. The sibling test keeps setting navigator.locks = undefined, so both branches are covered. - auth.renewToken.test.ts: getTokenMock always returned null, so even after the retry "succeeded" the follow-up checkAuth() marked the store unauthenticated; the test only checked "not redirected to login" and could pass with the session lost. The retry mock now hands back a fresh, unexpired user JWT and the test asserts store.authenticated is true, proving the D-retry actually recovered the session. --- frontend/src/helpers/auth.test.ts | 13 +++++++++++ frontend/src/stores/auth.renewToken.test.ts | 24 +++++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/frontend/src/helpers/auth.test.ts b/frontend/src/helpers/auth.test.ts index 69af2335f..46fc9232c 100644 --- a/frontend/src/helpers/auth.test.ts +++ b/frontend/src/helpers/auth.test.ts @@ -49,6 +49,16 @@ describe('refreshToken in-flight dedup', () => { }) it('coalesces concurrent calls into a single POST when Web Locks is available', async () => { + // Stub a minimal Web Locks API: happy-dom leaves navigator.locks + // undefined, so without this the test would silently fall through to + // the insecure-HTTP branch and never exercise navigator.locks.request. + const requestSpy = vi.fn((_name: string, cb: () => unknown) => cb()) + Object.defineProperty(navigator, 'locks', { + value: {request: requestSpy}, + configurable: true, + writable: true, + }) + const p1 = refreshToken(true) const p2 = refreshToken(true) @@ -58,6 +68,9 @@ describe('refreshToken in-flight dedup', () => { settlePost() await Promise.all([p1, p2]) + // The Web Locks branch actually ran... + expect(requestSpy).toHaveBeenCalledWith('vikunja-token-refresh', expect.any(Function)) + // ...and the in-flight dedup still collapsed both calls into one POST. expect(postCallCount).toBe(1) }) diff --git a/frontend/src/stores/auth.renewToken.test.ts b/frontend/src/stores/auth.renewToken.test.ts index d5bedc098..331f86856 100644 --- a/frontend/src/stores/auth.renewToken.test.ts +++ b/frontend/src/stores/auth.renewToken.test.ts @@ -57,6 +57,18 @@ function refreshError() { }) } +// A JWT carrying a not-yet-expired user session, so the checkAuth() call that +// renewToken() runs after a successful refresh treats the session as live. +function freshUserJwt() { + const payload = { + id: 1, + type: AUTH_TYPES.USER, + exp: Math.floor(Date.now() / 1000) + 3600, + } + const encoded = btoa(JSON.stringify(payload)) + return `header.${encoded}.signature` +} + describe('auth store renewToken retry (issue #2863)', () => { beforeEach(() => { setActivePinia(createPinia()) @@ -79,15 +91,23 @@ describe('auth store renewToken retry (issue #2863)', () => { const store = useAuthStore() setupExpiredUserSession(store) + // The retry "succeeds" only if it actually leaves a usable token behind: + // renewToken() runs checkAuth() afterwards, which reads getToken(). Start + // with no token, then hand back a fresh JWT once the refresh resolves. + getTokenMock.mockReturnValue(null) refreshTokenMock .mockRejectedValueOnce(refreshError()) - .mockResolvedValueOnce(undefined) + .mockImplementationOnce(async () => { + getTokenMock.mockReturnValue(freshUserJwt()) + }) await store.renewToken() // Two refresh attempts: the initial one and the single retry. expect(refreshTokenMock).toHaveBeenCalledTimes(2) - // The retry succeeded, so the user is not bounced to login. + // The retry recovered the session: the user is still authenticated... + expect(store.authenticated).toBe(true) + // ...and was not bounced to login. expect(routerPushMock).not.toHaveBeenCalledWith({name: 'user.login'}) })