Files
kochwas/tests/integration/importer.test.ts
2026-04-17 15:11:23 +02:00

114 lines
3.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { createServer, type Server } from 'node:http';
import type { AddressInfo } from 'node:net';
import { mkdtemp, rm, readFile } from 'node:fs/promises';
import { readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { openInMemoryForTest } from '../../src/lib/server/db';
import { addDomain } from '../../src/lib/server/domains/repository';
import { importRecipe, previewRecipe, ImporterError } from '../../src/lib/server/recipes/importer';
const here = dirname(fileURLToPath(import.meta.url));
const fixtureHtml = readFileSync(join(here, '../fixtures', 'chefkoch-schupfnudeln.html'), 'utf8');
// 1×1 PNG
const PNG = Buffer.from(
'89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c489' +
'0000000d49444154789c6300010000000500010d0a2db40000000049454e44ae426082',
'hex'
);
let server: Server;
let baseUrl: string;
let imgDir: string;
beforeEach(async () => {
server = createServer((req, res) => {
if (req.url?.startsWith('/recipe')) {
// Return our fixture but swap the image URL to point to our local server
const patched = fixtureHtml.replace(
/https?:\/\/img\.chefkoch-cdn\.de\/[^"']+/g,
`${baseUrl}/image.png`
);
res.writeHead(200, { 'content-type': 'text/html' });
res.end(patched);
return;
}
if (req.url?.startsWith('/image')) {
res.writeHead(200, { 'content-type': 'image/png' });
res.end(PNG);
return;
}
if (req.url === '/bare') {
res.writeHead(200, { 'content-type': 'text/html' });
res.end('<html><body>no recipe</body></html>');
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>((r) => server.listen(0, '127.0.0.1', r));
const addr = server.address() as AddressInfo;
baseUrl = `http://127.0.0.1:${addr.port}`;
imgDir = await mkdtemp(join(tmpdir(), 'kochwas-imp-'));
});
afterEach(async () => {
await new Promise<void>((r) => server.close(() => r()));
await rm(imgDir, { recursive: true, force: true });
});
describe('previewRecipe', () => {
it('throws DOMAIN_BLOCKED if host not whitelisted', async () => {
const db = openInMemoryForTest();
// note: no domain added
await expect(previewRecipe(db, `${baseUrl}/recipe`)).rejects.toMatchObject({
code: 'DOMAIN_BLOCKED'
});
});
it('returns parsed recipe for whitelisted domain', async () => {
const db = openInMemoryForTest();
addDomain(db, '127.0.0.1');
const r = await previewRecipe(db, `${baseUrl}/recipe`);
expect(r.title.toLowerCase()).toContain('schupfnudel');
expect(r.source_url).toBe(`${baseUrl}/recipe`);
expect(r.ingredients.length).toBeGreaterThan(0);
});
it('throws NO_RECIPE_FOUND when HTML has no Recipe JSON-LD', async () => {
const db = openInMemoryForTest();
addDomain(db, '127.0.0.1');
await expect(previewRecipe(db, `${baseUrl}/bare`)).rejects.toMatchObject({
code: 'NO_RECIPE_FOUND'
});
});
});
describe('importRecipe', () => {
it('imports, persists, and is idempotent', async () => {
const db = openInMemoryForTest();
addDomain(db, '127.0.0.1');
const first = await importRecipe(db, imgDir, `${baseUrl}/recipe`);
expect(first.duplicate).toBe(false);
expect(first.id).toBeGreaterThan(0);
expect(first.recipe.ingredients.length).toBeGreaterThan(0);
expect(first.recipe.image_path).not.toBeNull();
const saved = await readFile(join(imgDir, first.recipe.image_path!));
expect(saved.equals(PNG)).toBe(true);
const second = await importRecipe(db, imgDir, `${baseUrl}/recipe`);
expect(second.duplicate).toBe(true);
expect(second.id).toBe(first.id);
});
it('surfaces ImporterError type', async () => {
const db = openInMemoryForTest();
await expect(importRecipe(db, imgDir, `${baseUrl}/recipe`)).rejects.toBeInstanceOf(
ImporterError
);
});
});