68 lines
2.7 KiB
JavaScript
68 lines
2.7 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import AdmZip from 'adm-zip';
|
|
import { processFile, unpackZip, fetchUrl, MAX_FILE } from '../../lib/icons/ingest.js';
|
|
|
|
const PNG = Buffer.from([0x89,0x50,0x4e,0x47,0x0d,0x0a,0x1a,0x0a, 0,0,0,0]);
|
|
|
|
describe('processFile', () => {
|
|
it('slugifies name, keeps png', () => {
|
|
const r = processFile({ name: 'My Router.png', buffer: PNG });
|
|
expect(r.name).toBe('my-router.png');
|
|
expect(r.buffer).toBe(PNG);
|
|
});
|
|
it('sanitizes svg', () => {
|
|
const r = processFile({ name: 'x.svg', buffer: Buffer.from('<svg><script>1</script><path/></svg>') });
|
|
expect(r.buffer.toString()).not.toMatch(/script/i);
|
|
});
|
|
it('rejects non-image extension', () => {
|
|
expect(() => processFile({ name: 'x.exe', buffer: PNG })).toThrow();
|
|
});
|
|
it('rejects oversize', () => {
|
|
expect(() => processFile({ name: 'x.png', buffer: Buffer.alloc(MAX_FILE + 1, 1) })).toThrow();
|
|
});
|
|
it('rejects png with bad magic', () => {
|
|
expect(() => processFile({ name: 'x.png', buffer: Buffer.from('not a png') })).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('unpackZip', () => {
|
|
it('extracts images, skips non-image junk', () => {
|
|
const z = new AdmZip();
|
|
z.addFile('a.png', PNG);
|
|
z.addFile('notes.txt', Buffer.from('hi'));
|
|
const out = unpackZip(z.toBuffer());
|
|
expect(out.map(f => f.name)).toEqual(['a.png']);
|
|
});
|
|
|
|
it('skips path-traversal entries', () => {
|
|
// adm-zip's addFile() sanitizes '../' at write time (zipnamefix), so it
|
|
// can't produce a real traversal entry. Build one by mutating the entry
|
|
// name at the raw level *after* addFile — this survives serialization and
|
|
// stores '../evil.png' verbatim in the zip bytes.
|
|
const z = new AdmZip();
|
|
z.addFile('a.png', PNG);
|
|
z.addFile('placeholder.png', PNG);
|
|
const entries = z.getEntries();
|
|
entries[1].entryName = '../evil.png';
|
|
const buf = z.toBuffer();
|
|
// Sanity check: the traversal entry name really is in the serialized bytes.
|
|
expect(buf.includes(Buffer.from('../evil.png'))).toBe(true);
|
|
const out = unpackZip(buf);
|
|
expect(out.map(f => f.name)).toEqual(['a.png']);
|
|
});
|
|
});
|
|
|
|
describe('fetchUrl', () => {
|
|
it('rejects non-http schemes', async () => {
|
|
await expect(fetchUrl('file:///etc/passwd')).rejects.toThrow();
|
|
});
|
|
it('rejects localhost/private hosts', async () => {
|
|
await expect(fetchUrl('http://127.0.0.1/x.png')).rejects.toThrow();
|
|
});
|
|
it('fetches via injected fetcher', async () => {
|
|
const fake = async () => ({ ok: true, arrayBuffer: async () => PNG.buffer.slice(PNG.byteOffset, PNG.byteOffset + PNG.length), headers: new Map([['content-type','image/png']]) });
|
|
const r = await fetchUrl('https://example.com/x.png', { fetcher: fake });
|
|
expect(Buffer.isBuffer(r.buffer)).toBe(true);
|
|
});
|
|
});
|