Files
kochwas/tests/integration/importer.test.ts
hsiegeln 2807dd1cab
All checks were successful
Build & Publish Docker Image / build-and-push (push) Successful in 1m14s
feat(import): manuelle URL-Importe von allen Domains zulassen
Der User pastet bewusst eine URL und erwartet, dass der Import
klappt — die Whitelist-Prüfung (DOMAIN_BLOCKED) im previewRecipe
war da nur Reibung. Die Whitelist bleibt für die Web-Suche relevant
(dort muss das Crawl-Feld eingeschränkt werden), für Imports nicht
mehr.

Dropped: isDomainAllowed + whitelist.ts, DOMAIN_BLOCKED-Code in
ImporterError, die zugehörige Branch in mapImporterError. Tests
entsprechend angepasst: statt "DOMAIN_BLOCKED wenn nicht whitelisted"
prüft der Preview-Test jetzt "klappt auch ohne Whitelist-Eintrag".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:18:46 +02:00

110 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 { 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('accepts any domain — manuelle URL-Importe sind nicht auf die Whitelist beschränkt', async () => {
const db = openInMemoryForTest();
// keine Domain in der Whitelist — preview muss trotzdem klappen
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();
await expect(previewRecipe(db, `${baseUrl}/bare`)).rejects.toMatchObject({
code: 'NO_RECIPE_FOUND'
});
});
it('throws INVALID_URL for malformed input', async () => {
const db = openInMemoryForTest();
await expect(previewRecipe(db, 'not a url')).rejects.toMatchObject({
code: 'INVALID_URL'
});
});
});
describe('importRecipe', () => {
it('imports, persists, and is idempotent', async () => {
const db = openInMemoryForTest();
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 when no recipe on page', async () => {
const db = openInMemoryForTest();
await expect(importRecipe(db, imgDir, `${baseUrl}/bare`)).rejects.toBeInstanceOf(
ImporterError
);
});
});