/** * 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://instance.test', 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('says the code is not valid for this alert, without inventing a replacement', () => { // JoinAlert returns this one code for any code that does not belong to the // alert, so the client cannot say which happened — and with rotation gone // there is no "new one" to ask for. const err = new ApiError(400, 'invalid subscribe token', 'ERR_ALERT_TOKEN_INVALID'); const copy = joinErrorMessage(err); expect(copy).toBe('This code is not valid for this alert.'); expect(copy).not.toMatch(/expired/i); expect(copy).not.toMatch(/new one|rotate|rotated/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.'); }); });