mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
650451bdf7
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.
78 lines
3.1 KiB
TypeScript
78 lines
3.1 KiB
TypeScript
// FE-MOB-WEBPUSH-001 to FE-MOB-WEBPUSH-004: the phone device card.
|
|
//
|
|
// Markup only, like its desktop twin: useWebPush is mocked and has its own
|
|
// spec, so these cases pin what each hook state renders 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 '../../../helpers/render';
|
|
import type { WebPushCardState } from '../../../../src/hooks/useWebPush';
|
|
import MWebPushCard from '../../../../src/mobile/screens/settings/MWebPushCard';
|
|
|
|
const hookState = vi.hoisted(() => ({ current: null as unknown as WebPushCardState }));
|
|
vi.mock('../../../../src/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('MWebPushCard', () => {
|
|
it('FE-MOB-WEBPUSH-001: explains itself and offers to switch on', async () => {
|
|
render(<MWebPushCard />);
|
|
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-m-act');
|
|
expect(screen.getByRole('button', { name: 'Send test' })).toBeDisabled();
|
|
|
|
await userEvent.click(turnOn);
|
|
expect(hookState.current.toggle).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-MOB-WEBPUSH-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(<MWebPushCard />);
|
|
expect(screen.getByRole('status')).toHaveTextContent('On for this device');
|
|
expect(screen.getByRole('button', { name: 'Turn off for this device' }).className).not.toContain('bg-m-act');
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: 'Send test' }));
|
|
expect(hookState.current.sendTest).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it('FE-MOB-WEBPUSH-003: a state that cannot be switched shows its sentence and no buttons', () => {
|
|
hookState.current = state({ state: 'denied', canSwitch: false, stateText: 'Notifications are blocked.' });
|
|
render(<MWebPushCard />);
|
|
expect(screen.getByRole('status')).toHaveTextContent('Notifications are blocked.');
|
|
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('FE-MOB-WEBPUSH-004: the switch is disabled while busy', () => {
|
|
hookState.current = state({ busy: true });
|
|
render(<MWebPushCard />);
|
|
expect(screen.getByRole('button', { name: 'Turn on for this device' })).toBeDisabled();
|
|
});
|
|
});
|