From 26b136a42c037c11746cd88c3ea8a8c8a42faded Mon Sep 17 00:00:00 2001 From: Till JS Date: Sun, 10 May 2026 16:40:30 +0200 Subject: [PATCH] =?UTF-8?q?test(api):=20Unit-Tests=20f=C3=BCr=20makeInitia?= =?UTF-8?q?lReviewRows=20und=20fetchUrlContent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - lib-reviews.test.ts: 5 Tests — subIndex-Count, userId/cardId, leere Input-Liste, Initialzustand (reps=0, lapses=0), due ist Date - lib-url-fetch.test.ts: 6 Tests — mana-search Pfad, Fallback auf direktes Fetch, HTML-Stripping, Network-Fehler, leerer Content, Truncation auf 8000 Zeichen Co-Authored-By: Claude Sonnet 4.6 --- apps/api/tests/lib-reviews.test.ts | 62 +++++++++++++++ apps/api/tests/lib-url-fetch.test.ts | 108 +++++++++++++++++++++++++++ 2 files changed, 170 insertions(+) create mode 100644 apps/api/tests/lib-reviews.test.ts create mode 100644 apps/api/tests/lib-url-fetch.test.ts diff --git a/apps/api/tests/lib-reviews.test.ts b/apps/api/tests/lib-reviews.test.ts new file mode 100644 index 0000000..7dd9d4e --- /dev/null +++ b/apps/api/tests/lib-reviews.test.ts @@ -0,0 +1,62 @@ +import { describe, it, expect } from 'vitest'; +import { makeInitialReviewRows } from '../src/lib/reviews.ts'; + +describe('makeInitialReviewRows', () => { + it('creates one row per subIndex', () => { + const now = new Date('2025-01-01T00:00:00Z'); + const rows = makeInitialReviewRows({ + userId: 'u-1', + cardId: 'c-1', + subIndices: [0, 1, 2], + now, + }); + expect(rows).toHaveLength(3); + expect(rows[0].subIndex).toBe(0); + expect(rows[1].subIndex).toBe(1); + expect(rows[2].subIndex).toBe(2); + }); + + it('sets correct userId and cardId on each row', () => { + const now = new Date(); + const rows = makeInitialReviewRows({ + userId: 'u-test', + cardId: 'c-test', + subIndices: [0], + now, + }); + expect(rows[0].userId).toBe('u-test'); + expect(rows[0].cardId).toBe('c-test'); + }); + + it('returns empty array for empty subIndices', () => { + const rows = makeInitialReviewRows({ + userId: 'u-1', + cardId: 'c-1', + subIndices: [], + now: new Date(), + }); + expect(rows).toHaveLength(0); + }); + + it('initial state has reps=0 and lapses=0', () => { + const rows = makeInitialReviewRows({ + userId: 'u-1', + cardId: 'c-1', + subIndices: [0], + now: new Date(), + }); + expect(rows[0].reps).toBe(0); + expect(rows[0].lapses).toBe(0); + }); + + it('due date is a Date instance', () => { + const now = new Date(); + const rows = makeInitialReviewRows({ + userId: 'u-1', + cardId: 'c-1', + subIndices: [0], + now, + }); + expect(rows[0].due).toBeInstanceOf(Date); + }); +}); diff --git a/apps/api/tests/lib-url-fetch.test.ts b/apps/api/tests/lib-url-fetch.test.ts new file mode 100644 index 0000000..c7675bf --- /dev/null +++ b/apps/api/tests/lib-url-fetch.test.ts @@ -0,0 +1,108 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { fetchUrlContent } from '../src/lib/url-fetch.ts'; + +function makeFetch(responses: Array<{ ok: boolean; json?: () => Promise; text?: () => Promise }>) { + let call = 0; + return vi.fn(async () => { + const r = responses[Math.min(call++, responses.length - 1)]; + return r as unknown as Response; + }); +} + +describe('fetchUrlContent', () => { + let originalFetch: typeof globalThis.fetch; + + beforeEach(() => { + originalFetch = globalThis.fetch; + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + vi.restoreAllMocks(); + }); + + it('returns mana-search markdown when available', async () => { + globalThis.fetch = makeFetch([ + { + ok: true, + json: async () => ({ + success: true, + content: { title: 'Test Page', markdown: '# Test\nContent here' }, + }), + }, + ]); + + const result = await fetchUrlContent('https://example.com'); + expect(result).toContain('# Test Page'); + expect(result).toContain('Content here'); + }); + + it('falls back to direct fetch when mana-search fails', async () => { + globalThis.fetch = makeFetch([ + { ok: false, json: async () => ({ success: false }) }, + { + ok: true, + text: async () => '

Direct content

', + }, + ]); + + const result = await fetchUrlContent('https://example.com'); + expect(result).toContain('Direct content'); + }); + + it('strips HTML tags in direct fetch fallback', async () => { + globalThis.fetch = makeFetch([ + { ok: false, json: async () => ({ success: false }) }, + { + ok: true, + text: async () => '

Clean text

', + }, + ]); + + const result = await fetchUrlContent('https://example.com'); + expect(result).toContain('Clean text'); + expect(result).not.toContain('

'); + expect(result).not.toContain('alert(1)'); + expect(result).not.toContain('body{}'); + }); + + it('returns null when mana-search returns no content and direct fetch fails', async () => { + globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error')); + + const result = await fetchUrlContent('https://example.com'); + expect(result).toBeNull(); + }); + + it('returns null when mana-search returns empty content and direct fetch returns empty', async () => { + globalThis.fetch = makeFetch([ + { + ok: true, + json: async () => ({ success: true, content: { markdown: ' ' } }), + }, + { + ok: true, + text: async () => ' ', + }, + ]); + + const result = await fetchUrlContent('https://example.com'); + expect(result).toBeNull(); + }); + + it('truncates content to 8000 characters max', async () => { + const longContent = 'A'.repeat(9000); + globalThis.fetch = makeFetch([ + { + ok: true, + json: async () => ({ + success: true, + content: { markdown: longContent }, + }), + }, + ]); + + const result = await fetchUrlContent('https://example.com'); + expect(result).not.toBeNull(); + expect(result!.length).toBeLessThanOrEqual(8000); + }); +});