// Tests fuer notes-api (Client-Funktionen mit fetch-Mock) // Phase 4.1 import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest'; import { loadNote, saveNote, triggerSync, listNotes, type NoteData, } from '../../webapp/src/notes-api'; describe('notes-api Client', () => { let originalFetch: typeof globalThis.fetch; beforeEach(() => { originalFetch = globalThis.fetch; }); afterEach(() => { globalThis.fetch = originalFetch; vi.restoreAllMocks(); }); describe('loadNote', () => { it('gibt null zurueck bei 404', async () => { globalThis.fetch = vi.fn( async () => new Response('Not Found', { status: 404, statusText: 'Not Found' }), ) as typeof fetch; const result = await loadNote('notiz.md'); expect(result).toBeNull(); }); it('parst Notiz-Antwort korrekt', async () => { const noteData: NoteData = { id: 'test.md', content: '# Test', frontmatter: { title: 'Test' }, lastModified: '2026-09-05T10:00:00Z', }; globalThis.fetch = vi.fn( async () => new Response(JSON.stringify(noteData), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) as typeof fetch; const result = await loadNote('test.md'); expect(result).toEqual(noteData); }); it('wirft bei HTTP-Fehler (500)', async () => { globalThis.fetch = vi.fn( async () => new Response('Server Error', { status: 500, statusText: 'Internal Server Error' }), ) as typeof fetch; await expect(loadNote('test.md')).rejects.toThrow('HTTP 500'); }); it('encoded den noteId in der URL', async () => { const fetchMock = vi.fn(async () => new Response('null', { status: 404 })) as typeof fetch; globalThis.fetch = fetchMock; await loadNote('path with spaces.md'); expect(fetchMock).toHaveBeenCalledWith('/api/notes/path%20with%20spaces.md'); }); }); describe('saveNote', () => { it('sendet PUT mit Content im Body', async () => { const fetchMock = vi.fn( async () => new Response( JSON.stringify({ id: 'x.md', content: 'Y', frontmatter: {}, lastModified: 'now' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }, ), ) as typeof fetch; globalThis.fetch = fetchMock; await saveNote('x.md', 'Y'); const [url, init] = fetchMock.mock.calls[0]; expect(url).toBe('/api/notes/x.md'); expect(init?.method).toBe('PUT'); expect((init?.headers as Record)['Content-Type']).toBe('application/json'); const body = JSON.parse(init?.body as string); expect(body.content).toBe('Y'); }); }); describe('triggerSync', () => { it('sendet POST an /api/sync und gibt ok zurueck', async () => { const fetchMock = vi.fn( async () => new Response(JSON.stringify({ ok: true, message: 'Sync OK' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) as typeof fetch; globalThis.fetch = fetchMock; const result = await triggerSync(); expect(result.ok).toBe(true); expect(result.message).toBe('Sync OK'); }); it('gibt Fehler zurueck bei 500', async () => { globalThis.fetch = vi.fn(async () => new Response('Fail', { status: 500 })) as typeof fetch; await expect(triggerSync()).rejects.toThrow('HTTP 500'); }); }); describe('listNotes', () => { it('parst Notiz-Liste', async () => { globalThis.fetch = vi.fn( async () => new Response(JSON.stringify({ notes: ['a.md', 'b.md'] }), { status: 200, headers: { 'Content-Type': 'application/json' }, }), ) as typeof fetch; const notes = await listNotes(); expect(notes).toEqual(['a.md', 'b.md']); }); it('gibt leeres Array bei Fehler', async () => { globalThis.fetch = vi.fn(async () => new Response('Boom', { status: 500 })) as typeof fetch; await expect(listNotes()).rejects.toThrow('HTTP 500'); }); }); });