82 lines
2.5 KiB
TypeScript
82 lines
2.5 KiB
TypeScript
/**
|
|
* 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.');
|
|
});
|
|
});
|