147 lines
4.1 KiB
TypeScript
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);
|
|
});
|
|
});
|