/** * Error plumbing: the service answers `{"error":{"code":…,"message":…}}`, and * the join path has to tell the user something true without naming a mechanism * it cannot observe. These tests pin both halves. */ import { ApiError, configureApi, post } from '../src/services/apiClient'; import { joinErrorMessage } from '../src/services/alarmApi'; const g = globalThis as unknown as { fetch: typeof fetch }; const realFetch = g.fetch; function stubFetch(body: string, status = 400): void { g.fetch = (async () => ({ ok: false, status, text: async () => body, })) as unknown as typeof fetch; } beforeEach(() => { configureApi({ baseUrl: 'http://alarm.test', mode: 'direct', serviceRef: null, token: null, refreshToken: null, onUnauthorized: () => {}, onSessionUpdate: () => {}, }); }); afterEach(() => { g.fetch = realFetch; }); describe('apiClient error unwrapping', () => { it('extracts the service error code and the clean message', async () => { stubFetch( JSON.stringify({ error: { code: 'ERR_ALERT_TOKEN_INVALID', message: 'invalid subscribe token' }, }), ); await expect(post('joinAlert', {})).rejects.toMatchObject({ name: 'ApiError', status: 400, code: 'ERR_ALERT_TOKEN_INVALID', message: 'invalid subscribe token', }); }); it('keeps a non-JSON body as the message and reports no code', async () => { stubFetch('Bad Gateway', 502); await expect(post('joinAlert', {})).rejects.toMatchObject({ status: 502, code: null, message: 'Bad Gateway', }); }); }); describe('joinErrorMessage', () => { it('renders a token problem as a remedy, never as "expired"', () => { // JoinAlert returns this one code for a rotated token AND for a malformed // one, so the client cannot say which happened. const err = new ApiError(400, 'invalid subscribe token', 'ERR_ALERT_TOKEN_INVALID'); const copy = joinErrorMessage(err); expect(copy).toBe('This code is no longer valid — ask the owner for a new one.'); expect(copy).not.toMatch(/expired/i); }); it('passes through any other server message', () => { expect(joinErrorMessage(new ApiError(404, 'alert not found', 'ERR_NOT_FOUND'))).toBe( 'alert not found', ); }); it('falls back for errors that are not ApiError instances', () => { expect(joinErrorMessage(new Error('boom'))).toBe('boom'); expect(joinErrorMessage(undefined)).toBe('Could not subscribe this device.'); }); });