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.
This commit is contained in:
kolaente 2026-06-19 20:51:51 +02:00
parent 95e4cf43b5
commit badb000269
2 changed files with 35 additions and 2 deletions

View File

@ -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)
})

View File

@ -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'})
})