Warning, /firebird/packages/firebird-ng/display/src/lib/utils/data-fetching.utils.spec.ts is written in an unsupported language. File is not indexed.
0001 import { afterEach, describe, expect, it, vi } from 'vitest';
0002 import JSZip from 'jszip';
0003 import { fetchTextFile, isDexZipMember, loadJSONFileEvents, loadZipFileEvents, readDexFile, readDexZip } from './data-fetching.utils';
0004
0005 const DEX = {
0006 type: 'firebird-dex-json',
0007 version: '1.0',
0008 events: [{ id: 'event_0', pieces: [] }],
0009 };
0010
0011 describe('readDexFile', () => {
0012 it('reads a plain .firebird.json file', async () => {
0013 const file = new File([JSON.stringify(DEX)], 'events.firebird.json');
0014 expect(await readDexFile(file)).toEqual(DEX);
0015 });
0016
0017 it('reads the .json member of a dropped .zip', async () => {
0018 const zip = new JSZip();
0019 zip.file('events.firebird.json', JSON.stringify(DEX));
0020 const blob = await zip.generateAsync({ type: 'blob' });
0021 const file = new File([blob], 'events.firebird.zip');
0022 expect(await readDexFile(file)).toEqual(DEX);
0023 });
0024
0025 it('throws on a file that is not JSON', async () => {
0026 const file = new File(['not json'], 'notes.json');
0027 await expect(readDexFile(file)).rejects.toThrow();
0028 });
0029 });
0030
0031 describe('DEX zip members', () => {
0032 async function zipOf(members: Array<[string, string | null]>): Promise<Blob> {
0033 const zip = new JSZip();
0034 for (const [name, text] of members) {
0035 if (text === null) zip.folder(name);
0036 else zip.file(name, text);
0037 }
0038 return zip.generateAsync({ type: 'blob' });
0039 }
0040
0041 it('accepts .json files only, outside __MACOSX and not dot-files', () => {
0042 expect(isDexZipMember('events.firebird.json')).toBe(true);
0043 expect(isDexZipMember('data/events.JSON')).toBe(true);
0044 expect(isDexZipMember('__MACOSX/._events.firebird.json')).toBe(false);
0045 expect(isDexZipMember('data/__MACOSX/events.json')).toBe(false);
0046 expect(isDexZipMember('._events.json')).toBe(false);
0047 expect(isDexZipMember('data/.hidden.json')).toBe(false);
0048 expect(isDexZipMember('folder.json/')).toBe(false);
0049 expect(isDexZipMember('readme.txt')).toBe(false);
0050 });
0051
0052 it('skips the macOS metadata members that break JSON parsing', async () => {
0053 const blob = await zipOf([
0054 ['__MACOSX/', null],
0055 ['__MACOSX/._events.firebird.json', '\u0000\u0005binary resource fork'],
0056 ['.DS_Store', 'junk'],
0057 ['events.firebird.json', JSON.stringify(DEX)],
0058 ]);
0059 expect(await readDexZip(blob, 'mac.zip')).toEqual(DEX);
0060 });
0061
0062 it('reads only the first DEX member, in archive order (pyrobird reads the same one)', async () => {
0063 const second = { ...DEX, events: [{ id: 'event_9', pieces: [] }] };
0064 const blob = await zipOf([
0065 ['notes.txt', 'hello'],
0066 ['first.firebird.json', JSON.stringify(DEX)],
0067 ['second.firebird.json', JSON.stringify(second)],
0068 ]);
0069 expect(await readDexZip(blob, 'two.zip')).toEqual(DEX);
0070 });
0071
0072 it('inflates nothing but the chosen member', async () => {
0073 const blob = await zipOf([
0074 ['broken.txt', 'not json at all'],
0075 ['events.firebird.json', JSON.stringify(DEX)],
0076 ]);
0077 const archive = await JSZip.loadAsync(blob);
0078 const inflated: string[] = [];
0079 for (const file of Object.values(archive.files)) {
0080 const original = file.async.bind(file);
0081 (file as unknown as { async: unknown }).async = ((type: 'string') => {
0082 inflated.push(file.name);
0083 return original(type);
0084 }) as never;
0085 }
0086 const loadAsync = vi.spyOn(JSZip, 'loadAsync').mockResolvedValue(archive);
0087 expect(await readDexZip(blob, 'events.zip')).toEqual(DEX);
0088 expect(inflated).toEqual(['events.firebird.json']);
0089 loadAsync.mockRestore();
0090 });
0091
0092 it('says so when no member holds DEX', async () => {
0093 const blob = await zipOf([['__MACOSX/._events.json', 'x'], ['readme.txt', 'x']]);
0094 await expect(readDexZip(blob, 'empty.zip')).rejects.toThrow("'empty.zip' holds no .json member");
0095 });
0096 });
0097
0098 describe('fetching', () => {
0099 afterEach(() => vi.unstubAllGlobals());
0100
0101 it('rejects a failed binary fetch with the HTTP status instead of unzipping the error page', async () => {
0102 vi.stubGlobal('fetch', async () => new Response('<html>gone</html>', { status: 404, statusText: 'Not Found' }));
0103 await expect(loadZipFileEvents('https://h/e.firebird.zip'))
0104 .rejects.toThrow('HTTP 404 Not Found (https://h/e.firebird.zip)');
0105 });
0106
0107 it("carries the server's JSON error message", async () => {
0108 vi.stubGlobal('fetch', async () => new Response(
0109 JSON.stringify({ error: 'Event 50-60 is out of range: the file holds 2 events (0..1)' }),
0110 { status: 400, statusText: 'BAD REQUEST' }));
0111 await expect(fetchTextFile('https://h/api/v1/convert/auto/50-60?f=x.root'))
0112 .rejects.toThrow('HTTP 400 BAD REQUEST: Event 50-60 is out of range: the file holds 2 events (0..1)');
0113 });
0114 });
0115
0116 describe('HTML answers for missing files', () => {
0117 afterEach(() => vi.unstubAllGlobals());
0118 const indexPage = '<!doctype html><html><body>app</body></html>';
0119
0120 it('says that a JSON request got an HTML page', async () => {
0121 vi.stubGlobal('fetch', async () => new Response(indexPage, { status: 200 }));
0122 await expect(loadJSONFileEvents('assets/data/missing.firebird.json'))
0123 .rejects.toThrow("'assets/data/missing.firebird.json' is not JSON (the server sent an HTML page instead; the file is probably missing)");
0124 });
0125
0126 it('says that a zip request got an HTML page', async () => {
0127 vi.stubGlobal('fetch', async () => new Response(indexPage, { status: 200 }));
0128 await expect(loadZipFileEvents('assets/data/missing.firebird.zip'))
0129 .rejects.toThrow("'assets/data/missing.firebird.zip' is not a readable zip archive (the server sent an HTML page instead");
0130 });
0131 });