You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
DateKeys-App/src/lib/inspector/format.test.ts

188 lines
7.8 KiB

import { describe, expect, it } from 'vitest';
import { ERROR_CODES, type InspectView } from '../dkc/index.ts';
import {
cliJSON,
displayText,
errorGloss,
escapeInvisible,
formatByteCount,
formatDateTime,
formatInteger,
formatRelative,
INSPECT_STEPS,
OPEN_STEPS_TIME_AND_KEY,
OPEN_STEPS_TIME_ONLY,
openStepGloss,
instantToEpochMs,
policyGloss,
printableText,
safeFileName,
stepGloss,
stepName,
viewerTimeZone,
} from './format.ts';
const te = new TextEncoder();
describe('steps and codes', () => {
it('names the eight steps as the CLI and explains each one', () => {
expect(INSPECT_STEPS.map(stepName)).toEqual([
'parse DKC1',
'prelude',
'public header',
'header validation',
'sealed control structure',
'payload structure',
'condition',
'tlock stanza',
]);
for (const s of INSPECT_STEPS) expect(stepGloss(s)).toMatch(/^\p{Lu}.*\.$/u);
expect(stepName(9)).toBe('step 9');
expect(stepGloss(9)).toBe('');
});
it('explains every check of the opening, step 17 included', () => {
const names = new Set([...OPEN_STEPS_TIME_ONLY, ...OPEN_STEPS_TIME_AND_KEY].map(([, name]) => name));
expect(names.size).toBe(11);
expect(names.has('open payload')).toBe(true);
const glosses = [...names].map(openStepGloss);
for (const g of glosses) expect(g).toMatch(/^\p{Lu}.*\.$/u);
expect(new Set(glosses).size).toBe(11);
expect(openStepGloss('tlock stanza')).toBe('');
// The same order as the reference, time_and_key with one more check.
expect(OPEN_STEPS_TIME_AND_KEY.filter(([, n]) => n !== 'access credential' && n !== 'open access layer')).toEqual(OPEN_STEPS_TIME_ONLY);
});
it('explains every normative code in Spanish', () => {
const glosses = ERROR_CODES.map(errorGloss);
for (const g of glosses) expect(g).toMatch(/^\p{Lu}.*\.$/u);
expect(new Set(glosses).size).toBe(ERROR_CODES.length);
});
it('explains the access policies', () => {
expect(policyGloss('time_only')).toMatch(/^Solo tiempo/);
expect(policyGloss('time_and_key')).toMatch(/\.dkk/);
expect(policyGloss('policy(7)')).toBe('Política no definida en V1.');
});
});
describe('text', () => {
it('replaces what is not printable in a file name', () => {
expect(safeFileName('informe 2026 — ñ.pdf')).toBe('informe 2026 — ñ.pdf');
expect(safeFileName('a\u202eb\tc\nd\u{1f600}')).toBe('a_b_c_d\u{1f600}');
});
it('shows printable UTF-8 as text', () => {
expect(printableText(te.encode('public label'))).toBe('public label');
expect(printableText(te.encode('línea 1\n\tlínea 2 — ok ✓'))).toBe('línea 1\n\tlínea 2 — ok ✓');
});
it('refuses invalid UTF-8 and every invisible or control character', () => {
expect(printableText(new Uint8Array([0xa2, 0x00]))).toBeUndefined();
expect(printableText(new Uint8Array([0xc0, 0x80]))).toBeUndefined();
for (const s of ['a\rb', 'a\u0000b', 'a\u007fb', 'a\u0085b', 'a\u00a0b', 'abc\u202edcba', 'a\u200db', '\ufeffabc', 'a\u2028b', 'a\ue000b']) {
expect(printableText(te.encode(s)), JSON.stringify(s)).toBeUndefined();
}
});
it('escapes identifiers that hold non-printable characters', () => {
expect(displayText('org.example.label')).toEqual({ text: 'org.example.label', escaped: false });
expect(displayText('ñandú.example')).toEqual({ text: 'ñandú.example', escaped: false });
expect(displayText('org.example\u202e.label')).toEqual({ text: '"org.example\\u202e.label"', escaped: true });
expect(displayText('a\u200bb')).toEqual({ text: '"a\\u200bb"', escaped: true });
expect(displayText('tab\there')).toEqual({ text: '"tab\\there"', escaped: true });
});
});
describe('escapeInvisible', () => {
const c = (...codes: number[]): string => String.fromCodePoint(...codes);
const bs = c(0x5c);
it('leaves printable text, line feeds and tabs alone', () => {
const s = 'capsule: profile "x" is not pinned\n\tnote: ñ ✓ 😀';
expect(escapeInvisible(s)).toBe(s);
});
it('writes bidi overrides, format and control characters as JSON escapes', () => {
expect(escapeInvisible(`extension org.example${c(0x202e)}.must v1`)).toBe(`extension org.example${bs}u202e.must v1`);
expect(escapeInvisible(`a${c(0x200d)}b${c(0xfeff)}${c(0x0d)}${c(0x85)}${c(0xa0)}`)).toBe(
`a${bs}u200db${bs}ufeff${bs}u000d${bs}u0085${bs}u00a0`,
);
expect(escapeInvisible(c(0xe0001))).toBe(`${bs}udb40${bs}udc01`);
});
it('keeps the CLI JSON valid and equivalent', () => {
const view = { file: `x${c(0x202e)}.dkc`, valid: true, checks: [] } as unknown as InspectView;
const shown = escapeInvisible(cliJSON(view));
expect(shown).not.toBe(cliJSON(view));
expect(JSON.parse(shown)).toEqual(view);
});
});
describe('numbers and dates', () => {
it('formats integers and byte counts in Spanish', () => {
expect(formatInteger(891)).toBe('891');
expect(formatInteger(78799)).toBe('78.799');
expect(formatInteger(1048576)).toBe('1.048.576');
expect(formatByteCount(1)).toBe('1 byte');
expect(formatByteCount(0)).toBe('0 bytes');
expect(formatByteCount(1234)).toBe('1.234 bytes');
});
it('converts instants to milliseconds within the range of Date', () => {
expect(instantToEpochMs({ seconds: 1692809364, nanos: 0 })).toBe(1692809364000);
expect(instantToEpochMs({ seconds: 1, nanos: 999_999_999 })).toBe(1999);
expect(instantToEpochMs({ seconds: 253402300799, nanos: 0 })).toBe(253402300799000);
expect(instantToEpochMs({ seconds: 8.64e12 + 1, nanos: 0 })).toBeUndefined();
});
it('writes long dates in UTC and in a given zone', () => {
const t = Date.UTC(2023, 7, 23, 16, 49, 24);
// The joiner of date and time depends on the ICU version: ", " or " a las ".
expect(formatDateTime(t, 'UTC')).toMatch(/^23 de agosto de 2023(, | a las )16:49:24 UTC$/);
expect(formatDateTime(t, 'Europe/Madrid')).toMatch(/^23 de agosto de 2023(, | a las )18:49:24 CEST$/);
expect(formatDateTime(Date.UTC(2023, 0, 5, 1, 2, 3), 'Europe/Madrid')).toMatch(/^5 de enero de 2023(, | a las )02:02:03 CET$/);
expect(formatDateTime(t)).toMatch(/^23 de agosto de 2023(, | a las )\d\d:\d\d:24 /);
expect(typeof viewerTimeZone()).toBe('string');
});
it('says how far a date is from now', () => {
const now = Date.UTC(2026, 8, 25, 12, 0, 0);
expect(formatRelative(Date.UTC(2023, 7, 23), now)).toBe('hace 3 años');
expect(formatRelative(Date.UTC(2027, 0, 1), now)).toBe('dentro de 3 meses');
expect(formatRelative(now + 86400_000, now)).toBe('mañana');
expect(formatRelative(now - 2 * 3600_000, now)).toBe('hace 2 horas');
expect(formatRelative(now + 30_000, now)).toBe('dentro de 30 segundos');
expect(formatRelative(now + 400, now)).toBe('ahora');
});
});
describe('cliJSON', () => {
const view = {
file: 'x.dkc',
capsule_id: '00',
valid: false,
error: 'ERR_EXTENSION_CRITICAL_UNKNOWN',
checks: [
{ step: 1, name: 'parse DKC1', ok: true, detail: 'magic DKC1' },
{ step: 4, name: 'header validation', ok: false, detail: 'extension <a>&b\u2028\u2029 "q" \\ \n\t\b\f\u0001: X', error: 'X' },
],
} as InspectView;
it('writes the JSON of the Go encoder with two-space indentation and a final newline', () => {
const out = cliJSON(view);
expect(out.endsWith('}\n')).toBe(true);
expect(out.split('\n').slice(0, 4)).toEqual(['{', ' "file": "x.dkc",', ' "capsule_id": "00",', ' "valid": false,']);
expect(out).toContain('\n "checks": [\n {\n "step": 1,\n');
expect(JSON.parse(out)).toEqual(view);
});
it('escapes HTML characters and line separators as Go does', () => {
const out = cliJSON(view);
expect(out).toContain(
'"detail": "extension \\u003ca\\u003e\\u0026b\\u2028\\u2029 \\"q\\" \\\\ \\n\\t\\b\\f\\u0001: X"',
);
expect(out).not.toMatch(/[<>&\u2028\u2029]/);
});
});

Powered by TurnKey Linux.