feat(core): add secure recipe APIs, precise scaling and versioned imports

This commit is contained in:
OpenCode committed 2026-10-24 15:40:44 +02:00
commit 5d71d32e20
31 files changed
+6293

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
import { describe, it, expect } from 'vitest';
import { parseImport, exportRecipes } from '../../src/lib/import';
import { recipeSchema } from '../../src/lib/recipe';
import { examples, pasta } from '../fixtures';
describe('Versioned, safe, lossless recipe interchange', () => {
it.each(['json', 'xml'] as const)('roundtrips all fields in %s', (format) => {
expect(parseImport(exportRecipes(examples, format), format)).toEqual({
version: 1,
recipes: examples,
});
});
it('preserves XML escaped punctuation, unicode and literal ampersands', () => {
const r = {
...pasta,
title: 'Öl & Gemüse',
tips: 'Mit "Liebe" & Geduld. 1 < 2. &amp; bleibt Text.',
};
expect(parseImport(exportRecipes([r], 'xml'), 'xml').recipes[0]).toEqual(r);
});
it('rejects unknown versions and missing mandatory fields', () => {
expect(() => parseImport('{"version":2,"recipes":[]}', 'json')).toThrow();
expect(() => parseImport('{"version":1,"recipes":[{"title":"Test"}]}', 'json')).toThrow();
});
it('rejects HTML payloads, invalid image paths and broken references', () => {
expect(() => recipeSchema.parse({ ...pasta, title: '<script>alert(1)</script>' })).toThrow();
expect(() => recipeSchema.parse({ ...pasta, image: '/media/../../etc/passwd' })).toThrow();
expect(() =>
recipeSchema.parse({ ...pasta, steps: [{ ...pasta.steps[0], text: '{{unknown}}' }] }),
).toThrow();
});
it('rejects duplicate ingredient, step and timer IDs', () => {
expect(() =>
recipeSchema.parse({ ...pasta, ingredients: [...pasta.ingredients, pasta.ingredients[0]] }),
).toThrow();
expect(() =>
recipeSchema.parse({ ...pasta, steps: [pasta.steps[1], pasta.steps[1]] }),
).toThrow();
});
it.each([
'<!DOCTYPE x [<!ENTITY a SYSTEM "file:///etc/passwd">]><x>&a;</x>',
'<!DOCTYPE x [<!ENTITY a "&a;&a;">]><x>&a;</x>',
'<hamham><broken></hamham>',
])('rejects unsafe or malformed XML', (raw) => {
expect(() => parseImport(raw, 'xml')).toThrow();
});
it('enforces byte limits before parsing', () => {
expect(() => parseImport('x'.repeat(2 * 1024 * 1024 + 1), 'json')).toThrow(/2 MB/);
});
});