From 248e4700a32ab4f8c11f0756f0467e4e6897a67e Mon Sep 17 00:00:00 2001 From: kolaente Date: Mon, 21 Sep 2026 19:04:37 +0200 Subject: [PATCH] test(caldav): pin one-time secrets and durable token changes --- .../src/client/queries/caldavTokens.test.ts | 16 ++++++++++++ .../tests/e2e/user/settings/caldav.spec.ts | 26 ++++++++++++++----- 2 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 frontend/src/client/queries/caldavTokens.test.ts diff --git a/frontend/src/client/queries/caldavTokens.test.ts b/frontend/src/client/queries/caldavTokens.test.ts new file mode 100644 index 000000000..649498e59 --- /dev/null +++ b/frontend/src/client/queries/caldavTokens.test.ts @@ -0,0 +1,16 @@ +import {it, expect, vi} from 'vitest' +import {QueryClient} from '@tanstack/vue-query' +import {caldavTokenKeys, createCaldavTokenMutationOptions} from './caldavTokens' +const sdk = vi.hoisted(() => ({caldavTokensCreate: vi.fn()})) +vi.mock('@/client/generated', () => sdk) +vi.mock('@/message', () => ({success: vi.fn(), error: vi.fn()})) + +it('returns the one-time secret without putting it in the shared list cache', async () => { + const client = new QueryClient() + client.setQueryData(caldavTokenKeys.all, [{id: 1}]) + sdk.caldavTokensCreate.mockResolvedValue({data: {id: 2, token: 'one-time-secret'}}) + const created = await client.getMutationCache().build(client, createCaldavTokenMutationOptions()).execute() + expect(created.token).toBe('one-time-secret') + expect(client.getQueryData(caldavTokenKeys.all)).toEqual([{id: 1}]) + expect(client.getQueryState(caldavTokenKeys.all)?.isInvalidated).toBe(true) +}) diff --git a/frontend/tests/e2e/user/settings/caldav.spec.ts b/frontend/tests/e2e/user/settings/caldav.spec.ts index dfc839807..f166c631c 100644 --- a/frontend/tests/e2e/user/settings/caldav.spec.ts +++ b/frontend/tests/e2e/user/settings/caldav.spec.ts @@ -1,18 +1,23 @@ +import type {Page} from '@playwright/test' import {test, expect} from '../../../support/fixtures' import {gotoUserSettings} from '../../../support/userSettings' import {TokenFactory} from '../../../factories/token' test.describe('CalDAV', () => { + // Rows carrying a are the data rows; this drops the -only header row. + const dataRows = (page: Page) => page.locator('table.table tr').filter({has: page.locator('td')}) + test('generates a token that authenticates against the caldav endpoint', async ({ authenticatedPage: page, currentUser, apiContext, }) => { await gotoUserSettings(page, 'caldav') const created = page.waitForResponse(r => - r.url().includes('/user/settings/token/caldav') && r.request().method() === 'PUT', + r.url().includes('/user/settings/token/caldav') && r.request().method() === 'POST', ) await page.getByRole('button', {name: 'Create a CalDAV token'}).click() await created + await expect(dataRows(page)).toHaveCount(1) // Banner renders the one-time token string; capture it. const banner = page.locator('.message').filter({hasText: 'Here is your new token'}) @@ -29,26 +34,33 @@ test.describe('CalDAV', () => { headers: {Authorization: `Basic ${basic}`, Depth: '0'}, }) expect(resp.status()).toBeLessThan(300) + await page.reload() + await expect(banner).toHaveCount(0) + await expect(dataRows(page)).toHaveCount(1) }) test('deleting a token revokes caldav access', async ({ - authenticatedPage: page, currentUser, + authenticatedPage: page, currentUser, apiContext, userToken, }) => { const tokenValue = 'fixed-caldav-token-123456789012345678901234567890' // kind=4 is TokenCaldavAuth (see pkg/user/token.go) await TokenFactory.create(1, {user_id: currentUser.id, kind: 4, token: tokenValue}, false) await gotoUserSettings(page, 'caldav') - // Filter to data rows (rows containing a ) to exclude the -only header row. - const dataRows = page.locator('table.table tr').filter({has: page.locator('td')}) - await expect(dataRows).toHaveCount(1) + await expect(dataRows(page)).toHaveCount(1) const deleted = page.waitForResponse(r => /\/user\/settings\/token\/caldav\/\d+/.test(r.url()) && r.request().method() === 'DELETE', ) - await dataRows.getByRole('button', {name: 'Delete'}).click() + await dataRows(page).getByRole('button', {name: 'Delete'}).click() await deleted - await expect(dataRows).toHaveCount(0) + await expect(dataRows(page)).toHaveCount(0) + await page.reload() + await expect(dataRows(page)).toHaveCount(0) + const stored = await apiContext.get('user/settings/token/caldav', { + headers: {Authorization: `Bearer ${userToken}`}, + }) + expect(await stored.json()).toEqual([]) // NOTE: the factory seeds the plaintext token as-is, but caldav tokens are // stored bcrypt-hashed. We assert the row is gone in the UI rather than