Files
Maurice 2c3fb15263 style(server): format the files the health lanes added or moved
Prettier on every new or moved file that was not formatted, so lint:format
holds them; the files taken off the format baseline are the ones that moved
or were formatted on the way.
2026-10-10 20:16:04 +02:00

137 lines
5.6 KiB
TypeScript

import {
COLLAB_POLL_OPTIONS,
JOURNEY_ENTRY_PROS_CONS,
JOURNEY_ENTRY_TAGS,
MCP_TOKEN_API_SCOPES,
OAUTH_CLIENT_ALLOWED_SCOPES,
OAUTH_CLIENT_REDIRECT_URIS,
OAUTH_CONSENT_SCOPES,
OAUTH_TOKEN_SCOPES,
RESERVATION_METADATA,
USER_MFA_BACKUP_CODES,
} from '../../../src/db/json-columns';
import {
decodeJson,
decodeJsonResult,
encodeJson,
logJsonFailure,
type JsonColumn,
} from '../../../src/utils/json-column';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { z } from 'zod';
const logMock = vi.hoisted(() => ({ logInfo: vi.fn(), logError: vi.fn(), logWarn: vi.fn(), logDebug: vi.fn() }));
vi.mock('../../../src/nest/audit/audit-log.logger', () => logMock);
const LIST: JsonColumn<string[]> = { column: 't.list', schema: z.array(z.string()), fallback: () => [] };
beforeEach(() => vi.clearAllMocks());
describe('decodeJsonResult', () => {
it('JSONCOL-001: decodes text of the right shape', () => {
expect(decodeJsonResult(LIST, '["a","b"]')).toEqual({ ok: true, value: ['a', 'b'] });
});
it('JSONCOL-002: names why a value did not decode', () => {
expect(decodeJsonResult(LIST, null)).toEqual({ ok: false, reason: 'empty' });
expect(decodeJsonResult(LIST, undefined)).toEqual({ ok: false, reason: 'empty' });
expect(decodeJsonResult(LIST, '')).toEqual({ ok: false, reason: 'empty' });
expect(decodeJsonResult(LIST, '{not json')).toEqual({ ok: false, reason: 'invalid-json' });
expect(decodeJsonResult(LIST, '{"a":1}')).toEqual({ ok: false, reason: 'schema' });
});
it('JSONCOL-003: checks a value a driver handed back already parsed', () => {
expect(decodeJsonResult(LIST, ['x'])).toEqual({ ok: true, value: ['x'] });
expect(decodeJsonResult(LIST, { a: 1 })).toEqual({ ok: false, reason: 'schema' });
});
it('JSONCOL-004: unwraps one level of double encoding only where the column says so', () => {
const doubled = JSON.stringify(JSON.stringify({ price: '12' }));
expect(decodeJsonResult(RESERVATION_METADATA, doubled)).toEqual({ ok: true, value: { price: '12' } });
expect(decodeJsonResult(LIST, JSON.stringify(JSON.stringify(['a'])))).toEqual({ ok: false, reason: 'schema' });
expect(decodeJsonResult(RESERVATION_METADATA, JSON.stringify('{broken'))).toEqual({
ok: false,
reason: 'invalid-json',
});
});
});
describe('decodeJson', () => {
it('JSONCOL-010: returns the decoded value without logging', () => {
expect(decodeJson(LIST, '["a"]')).toEqual(['a']);
expect(logMock.logWarn).not.toHaveBeenCalled();
});
it('JSONCOL-011: an empty column falls back quietly', () => {
expect(decodeJson(LIST, null)).toEqual([]);
expect(logMock.logWarn).not.toHaveBeenCalled();
});
it('JSONCOL-012: broken text and the wrong shape fall back and are logged with the column and context', () => {
expect(decodeJson(LIST, '{not json', 'row 7')).toEqual([]);
expect(logMock.logWarn).toHaveBeenLastCalledWith(
'[json] t.list (row 7): stored value is not valid JSON, using the fallback',
);
expect(decodeJson(LIST, '{"a":1}')).toEqual([]);
expect(logMock.logWarn).toHaveBeenLastCalledWith(
'[json] t.list: stored value has the wrong shape, using the fallback',
);
});
it('JSONCOL-013: every fallback is a fresh value', () => {
const a = decodeJson(RESERVATION_METADATA, null);
a.price = '1';
expect(decodeJson(RESERVATION_METADATA, null)).toEqual({});
});
});
describe('logJsonFailure / encodeJson', () => {
it('JSONCOL-020: logs the column, the context and the reason', () => {
logJsonFailure(LIST, 'schema');
expect(logMock.logWarn).toHaveBeenCalledWith('[json] t.list: stored value has the wrong shape, using the fallback');
});
it('JSONCOL-021: encodes a value of the right shape and refuses another', () => {
expect(encodeJson(LIST, ['a', 'b'])).toBe('["a","b"]');
expect(() => encodeJson(LIST, [1] as unknown as string[])).toThrow();
});
});
describe('the declared columns', () => {
it('JSONCOL-030: reservation metadata is an object, and anything else reads as an empty one', () => {
expect(decodeJson(RESERVATION_METADATA, '{"airline":"LH","legs":[1,2]}')).toEqual({ airline: 'LH', legs: [1, 2] });
expect(decodeJson(RESERVATION_METADATA, '[1,2]')).toEqual({});
expect(decodeJson(RESERVATION_METADATA, 'null')).toEqual({});
expect(decodeJson(RESERVATION_METADATA, '{oops')).toEqual({});
});
it('JSONCOL-031: the OAuth lists fail closed to an empty list', () => {
for (const col of [
OAUTH_TOKEN_SCOPES,
OAUTH_CLIENT_ALLOWED_SCOPES,
OAUTH_CLIENT_REDIRECT_URIS,
OAUTH_CONSENT_SCOPES,
]) {
expect(decodeJson(col, '["trips:read"]')).toEqual(['trips:read']);
expect(decodeJson(col, 'garbage')).toEqual([]);
expect(decodeJson(col, '[1]')).toEqual([]);
}
});
it('JSONCOL-032: the lenient lists keep whatever list is stored', () => {
expect(decodeJson(MCP_TOKEN_API_SCOPES, '["a",1]')).toEqual(['a', 1]);
expect(decodeJson(USER_MFA_BACKUP_CODES, '["h1",null]')).toEqual(['h1', null]);
expect(decodeJson(COLLAB_POLL_OPTIONS, '["Yes",{"label":"No"}]')).toEqual(['Yes', { label: 'No' }]);
expect(decodeJson(COLLAB_POLL_OPTIONS, '{"a":1}')).toEqual([]);
});
it('JSONCOL-033: journey entry tags and pros/cons', () => {
expect(decodeJson(JOURNEY_ENTRY_TAGS, '["beach"]')).toEqual(['beach']);
expect(decodeJson(JOURNEY_ENTRY_TAGS, '{bad')).toEqual([]);
expect(decodeJson(JOURNEY_ENTRY_PROS_CONS, '{"pros":["sun"],"cons":[]}')).toEqual({ pros: ['sun'], cons: [] });
expect(decodeJson(JOURNEY_ENTRY_PROS_CONS, 'null')).toBeNull();
expect(decodeJson(JOURNEY_ENTRY_PROS_CONS, '{bad')).toBeNull();
});
});