import { describe, it, expect, beforeAll, vi } from 'vitest'; import { pool } from '../../lib/db/pool.js'; import { resetDb } from '../helpers/db.js'; import { migrateUp } from '../../lib/db/migrate.js'; import { ensureSpace } from '../../migrate/spaces.js'; import { importBookstack } from '../../migrate/sources/bookstack.js'; let spaceId; beforeAll(async () => { await resetDb(); await migrateUp(); spaceId = await ensureSpace('wiki', 'Wiki'); }); function fetchMock() { const books = { data: [{ id: 10, name: 'Infra Book', description: '' }] }; const chapters = { data: [{ id: 20, name: 'Storage', book_id: 10 }] }; const list = { data: [{ id: 1, name: 'Overview' }, { id: 2, name: 'Disks' }] }; const detail = (id) => ({ id, name: id === 1 ? 'Overview' : 'Disks', markdown: `# P${id}\nbody`, book_id: 10, chapter_id: id === 2 ? 20 : 0 }); return vi.fn(async (u) => { if (u.endsWith('/api/books')) return { ok: true, json: async () => books }; if (u.endsWith('/api/chapters')) return { ok: true, json: async () => chapters }; if (u.endsWith('/api/pages')) return { ok: true, json: async () => list }; return { ok: true, json: async () => detail(Number(u.split('/').pop())) }; }); } describe('bookstack importer (hierarchy)', () => { it('creates Book › Chapter › Page with parent links, idempotently', async () => { const r1 = await importBookstack({ apiUrl: 'http://bs', tokenId: 'i', tokenSecret: 's', spaceId, fetchImpl: fetchMock() }); expect(r1.created).toBe(4); // 1 book + 1 chapter + 2 pages const byTitle = {}; for (const r of (await pool.query(`SELECT id, title, parent_id FROM pages WHERE space_id=$1`, [spaceId])).rows) byTitle[r.title] = r; expect(byTitle['Infra Book'].parent_id).toBeNull(); expect(byTitle['Storage'].parent_id).toBe(byTitle['Infra Book'].id); // chapter under book expect(byTitle['Overview'].parent_id).toBe(byTitle['Infra Book'].id); // page directly under book (chapter_id 0) expect(byTitle['Disks'].parent_id).toBe(byTitle['Storage'].id); // page under chapter const r2 = await importBookstack({ apiUrl: 'http://bs', tokenId: 'i', tokenSecret: 's', spaceId, fetchImpl: fetchMock() }); expect(r2.created).toBe(0); }); });