Files
TREK/client/src/components/Settings/WebPushCard.test.tsx
T
Maurice 650451bdf7 feat(notifications): Web Push delivers notifications to every device, even with TREK closed
A fourth built-in channel next to email, webhook and ntfy. Browsers
register per device, the server encrypts per RFC 8291 and signs per RFC
8292 with node:crypto and sends only to known push services through the
SSRF guard. The event matrix decides per category what arrives by push;
admins switch the channel on under Notifications.

VAPID keys come from the environment or are generated once, only while
neither stored row exists, and kept encrypted. A pair that does not
decrypt or match is never overwritten and never swapped for another one:
push stays off with an error in the log and every device stays
registered until the key is fixed. The subject is VAPID_SUBJECT, then an
https APP_URL, then the project URL.

Only chat messages collapse into one notification per trip, and a
replacement still alerts. A 403 removes a device only after three
failures in a row; 404 and 410 remove it at once. Password change, reset
and an admin setting a password drop the user's devices with the other
credentials. The request log redacts subscriptions and endpoints, boot
errors print *** for every secret variable, and logout forgets the
device with a plain fetch so an expired session cannot redirect midway.

Addresses discussion #894.
2026-09-24 23:17:03 +00:00

73 lines
3.0 KiB
TypeScript

/**
* FE-COMP-WEBPUSHCARD-001 to FE-COMP-WEBPUSHCARD-004: the desktop device card.
*
* Markup only: `useWebPush` is mocked and has its own spec, so these cases pin
* what each hook state puts on screen and that the buttons reach the hook.
*/
import { describe, it, expect, vi, beforeEach } from 'vitest'
import userEvent from '@testing-library/user-event'
import { render, screen } from '../../../tests/helpers/render'
import type { WebPushCardState } from '../../hooks/useWebPush'
import WebPushCard from './WebPushCard'
const hookState = vi.hoisted(() => ({ current: null as unknown as WebPushCardState }))
vi.mock('../../hooks/useWebPush', () => ({ useWebPush: () => hookState.current }))
function state(over: Partial<WebPushCardState> = {}): WebPushCardState {
return {
state: 'off',
stateText: null,
canSwitch: true,
on: false,
switchLabel: 'Turn on for this device',
busy: false,
canTest: false,
toggle: vi.fn(),
sendTest: vi.fn(),
...over,
}
}
beforeEach(() => {
hookState.current = state()
})
describe('WebPushCard', () => {
it('FE-COMP-WEBPUSHCARD-001: explains itself and offers to switch on', async () => {
render(<WebPushCard />)
expect(screen.getByText('Push notifications on this device')).toBeInTheDocument()
expect(screen.getByText(/Shows TREK notifications on this device/)).toBeInTheDocument()
expect(screen.queryByRole('status')).not.toBeInTheDocument()
const turnOn = screen.getByRole('button', { name: 'Turn on for this device' })
expect(turnOn.className).toContain('bg-accent')
expect(screen.getByRole('button', { name: 'Send test' })).toBeDisabled()
await userEvent.click(turnOn)
expect(hookState.current.toggle).toHaveBeenCalledTimes(1)
})
it('FE-COMP-WEBPUSHCARD-002: when on, says so, offers to switch off and to test', async () => {
hookState.current = state({ state: 'on', on: true, stateText: 'On for this device', switchLabel: 'Turn off for this device', canTest: true })
render(<WebPushCard />)
expect(screen.getByRole('status')).toHaveTextContent('On for this device')
expect(screen.getByRole('button', { name: 'Turn off for this device' }).className).not.toContain('bg-accent')
await userEvent.click(screen.getByRole('button', { name: 'Send test' }))
expect(hookState.current.sendTest).toHaveBeenCalledTimes(1)
})
it('FE-COMP-WEBPUSHCARD-003: a state that cannot be switched shows its sentence and no buttons', () => {
hookState.current = state({ state: 'ios-install', canSwitch: false, stateText: 'Add TREK to the Home Screen first.' })
render(<WebPushCard />)
expect(screen.getByRole('status')).toHaveTextContent('Add TREK to the Home Screen first.')
expect(screen.queryByRole('button')).not.toBeInTheDocument()
})
it('FE-COMP-WEBPUSHCARD-004: the switch is disabled while busy', () => {
hookState.current = state({ busy: true })
render(<WebPushCard />)
expect(screen.getByRole('button', { name: 'Turn on for this device' })).toBeDisabled()
})
})