import { afterEach, describe, expect, it, vi } from 'vitest'; import { contextualLaunchUrl, loadCatalogue } from './catalogue'; import { catalogue, catalogueFetch } from './test/fixtures'; afterEach(() => vi.unstubAllGlobals()); describe('catalogue loading', () => { it('loads and resolves internal apps using the shared contract', async () => { vi.stubGlobal('fetch', vi.fn(catalogueFetch())); const loaded = await loadCatalogue('/toolbox.catalog.json'); expect(loaded.catalogue.name).toBe('add·ideas Toolbox'); expect(loaded.homeUrl).toBe('http://localhost:3000/'); expect(loaded.apps).toHaveLength(6); expect(loaded.apps[0]).toMatchObject({ id: 'de.add-ideas.pdf-tools', name: 'PDF Workbench', entryUrl: 'http://localhost:3000/apps/pdf/', sourceUrl: 'https://git.add-ideas.de/lotobo/pdf-tools', }); }); it('keeps valid apps available when another manifest fails', async () => { const twoApps = { ...catalogue, apps: [ ...catalogue.apps, { manifest: './apps/missing/toolbox-app.json', enabled: true }, ], }; vi.stubGlobal( 'fetch', vi.fn( catalogueFetch({ '/toolbox.catalog.json': Response.json(twoApps), }) ) ); const loaded = await loadCatalogue('/toolbox.catalog.json'); expect(loaded.apps.map((app) => app.id)).toEqual([ 'de.add-ideas.pdf-tools', 'de.add-ideas.xslt-tools', 'de.add-ideas.onenote-tools', 'de.add-ideas.svg-tools', 'de.add-ideas.auth-tools', 'de.add-ideas.sudoku-tools', ]); expect(loaded.unavailable).toHaveLength(1); expect(loaded.unavailable[0]?.reason).toMatch(/HTTP 404/); }); it('rejects cross-origin manifest references', async () => { vi.stubGlobal( 'fetch', vi.fn( catalogueFetch({ '/toolbox.catalog.json': Response.json({ ...catalogue, apps: [ { manifest: 'https://example.invalid/toolbox-app.json', enabled: true, }, ], }), }) ) ); await expect(loadCatalogue('/toolbox.catalog.json')).rejects.toThrow( /share the page origin/i ); }); it('rejects a cross-origin catalogue home', async () => { vi.stubGlobal( 'fetch', vi.fn( catalogueFetch({ '/toolbox.catalog.json': Response.json({ ...catalogue, home: 'https://example.invalid/', }), }) ) ); await expect(loadCatalogue('/toolbox.catalog.json')).rejects.toThrow( /share the page origin/i ); }); it('adds catalogue context only to same-origin launch links', () => { const contextual = new URL( contextualLaunchUrl( 'http://localhost:3000/apps/pdf/', 'http://localhost:3000/toolbox.catalog.json' ) ); expect(contextual.searchParams.get('toolbox')).toBe( 'http://localhost:3000/toolbox.catalog.json' ); expect( contextualLaunchUrl( 'https://example.com/tool', 'http://localhost:3000/toolbox.catalog.json' ) ).toBe('https://example.com/tool'); }); });