Files
dockhand-upstream/tests/scanner-json.test.ts
T

147 lines
4.1 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
// extractJson and sanitizeJsonString are pure functions defined in scanner.ts,
// but that module has heavy side-effect imports (docker, db). We replicate the
// function bodies here to keep unit tests dependency-free.
function extractJson(output: string): string {
const firstBrace = output.indexOf('{');
const lastBrace = output.lastIndexOf('}');
if (firstBrace === -1 || lastBrace === -1 || lastBrace <= firstBrace) {
throw new Error('No JSON object found in scanner output');
}
return output.slice(firstBrace, lastBrace + 1);
}
function sanitizeJsonString(json: string): string {
let result = '';
let inString = false;
let escaped = false;
let sanitized = 0;
for (let i = 0; i < json.length; i++) {
const ch = json.charCodeAt(i);
if (escaped) {
const ch2 = json[i];
if ('"\\\/bfnrtu'.includes(ch2)) {
result += ch2;
} else if (ch < 0x20) {
result += '\\';
if (ch === 0x0A) result += '\\n';
else if (ch === 0x0D) result += '\\r';
else if (ch === 0x09) result += '\\t';
else result += `\\u${ch.toString(16).padStart(4, '0')}`;
sanitized++;
} else {
result += '\\' + ch2;
sanitized++;
}
escaped = false;
continue;
}
if (inString) {
if (ch === 0x5C) {
result += json[i];
escaped = true;
} else if (ch === 0x22) {
result += json[i];
inString = false;
} else if (ch < 0x20) {
if (ch === 0x0A) result += '\\n';
else if (ch === 0x0D) result += '\\r';
else if (ch === 0x09) result += '\\t';
else result += `\\u${ch.toString(16).padStart(4, '0')}`;
sanitized++;
} else {
result += json[i];
}
} else {
if (ch === 0x22) {
inString = true;
}
result += json[i];
}
}
return result;
}
describe('extractJson', () => {
test('extracts JSON from clean output', () => {
expect(extractJson('{"key": "value"}')).toBe('{"key": "value"}');
});
test('extracts JSON with leading noise', () => {
const output = 'Scanning image...\nPulling layers...\n{"results": []}';
expect(extractJson(output)).toBe('{"results": []}');
});
test('extracts JSON with trailing noise', () => {
const output = '{"results": []}\nDone in 5.2s';
expect(extractJson(output)).toBe('{"results": []}');
});
test('extracts JSON surrounded by noise', () => {
const output = 'INFO: starting\n{"data": true}\nINFO: done';
expect(extractJson(output)).toBe('{"data": true}');
});
test('throws for output with no JSON', () => {
expect(() => extractJson('no json here')).toThrow('No JSON object found');
});
test('throws for empty input', () => {
expect(() => extractJson('')).toThrow('No JSON object found');
});
test('handles nested braces', () => {
const json = '{"outer": {"inner": {"deep": 1}}}';
expect(extractJson('prefix ' + json + ' suffix')).toBe(json);
});
});
describe('sanitizeJsonString', () => {
test('passes clean JSON through unchanged', () => {
const json = '{"key": "value", "num": 42}';
expect(sanitizeJsonString(json)).toBe(json);
});
test('escapes raw newlines inside strings', () => {
const dirty = '{"desc": "line1\nline2"}';
const result = sanitizeJsonString(dirty);
expect(() => JSON.parse(result)).not.toThrow();
const parsed = JSON.parse(result);
expect(parsed.desc).toBe('line1\nline2');
});
test('escapes raw tabs inside strings', () => {
const dirty = '{"desc": "col1\tcol2"}';
const result = sanitizeJsonString(dirty);
expect(() => JSON.parse(result)).not.toThrow();
});
test('handles already-escaped sequences', () => {
const clean = '{"desc": "line1\\nline2"}';
const result = sanitizeJsonString(clean);
expect(result).toBe(clean);
});
test('escapes null bytes inside strings', () => {
const dirty = '{"desc": "has\x00null"}';
const result = sanitizeJsonString(dirty);
expect(() => JSON.parse(result)).not.toThrow();
});
test('does not modify control chars outside strings', () => {
const json = '{\n "key": "value"\n}';
expect(sanitizeJsonString(json)).toBe(json);
});
test('handles empty strings', () => {
const json = '{"key": ""}';
expect(sanitizeJsonString(json)).toBe(json);
});
});