Cover shared zod schemas, extract traversal/refs/downloads, and tts filename/error handling. Document the test setup in the README and docs.
92 lines
3.0 KiB
TypeScript
92 lines
3.0 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import type { AssetRef } from '@tts/shared';
|
|
import { downloadAll, downloadAsset, guessMimeType } from './download.js';
|
|
|
|
const ref = (url: string): AssetRef => ({
|
|
kind: 'image',
|
|
url,
|
|
ownerGuid: 'g1',
|
|
});
|
|
|
|
function blobResponse(): Response {
|
|
return new Response(new Blob(['data']), { status: 200 });
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
describe('guessMimeType', () => {
|
|
it('maps common image extensions', () => {
|
|
expect(guessMimeType('https://example.com/a.png')).toBe('image/png');
|
|
expect(guessMimeType('https://example.com/a.jpg')).toBe('image/jpeg');
|
|
expect(guessMimeType('https://example.com/a.jpeg')).toBe('image/jpeg');
|
|
expect(guessMimeType('https://example.com/a.gif')).toBe('image/gif');
|
|
expect(guessMimeType('https://example.com/a.webp')).toBe('image/webp');
|
|
expect(guessMimeType('https://example.com/a.pdf')).toBe('application/pdf');
|
|
});
|
|
|
|
it('ignores query strings when reading the extension', () => {
|
|
expect(guessMimeType('https://example.com/a.png?v=2')).toBe('image/png');
|
|
});
|
|
|
|
it('falls back to octet-stream for unknown extensions', () => {
|
|
expect(guessMimeType('https://example.com/a.xyz')).toBe(
|
|
'application/octet-stream',
|
|
);
|
|
expect(guessMimeType('https://example.com/noext')).toBe(
|
|
'application/octet-stream',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('downloadAsset', () => {
|
|
it('returns a blob on success', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(blobResponse()));
|
|
const blob = await downloadAsset('https://example.com/a.png');
|
|
expect(blob).toBeInstanceOf(Blob);
|
|
});
|
|
|
|
it('throws on a non-OK response', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockResolvedValue(new Response(null, { status: 404 })),
|
|
);
|
|
await expect(downloadAsset('https://example.com/a.png')).rejects.toThrow(
|
|
'Failed to download asset (404)',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('downloadAll', () => {
|
|
it('downloads every ref and reports progress', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockImplementation(() => blobResponse()));
|
|
const refs = [ref('https://example.com/1.png'), ref('https://example.com/2.png')];
|
|
const progress: number[] = [];
|
|
const results = await downloadAll(refs, {
|
|
concurrency: 2,
|
|
onProgress: (p) => progress.push(p.done),
|
|
});
|
|
expect(results).toHaveLength(2);
|
|
expect(progress).toEqual([1, 2]);
|
|
});
|
|
|
|
it('respects the concurrency limit', async () => {
|
|
let inFlight = 0;
|
|
let maxInFlight = 0;
|
|
const fetchMock = vi.fn().mockImplementation(async () => {
|
|
inFlight += 1;
|
|
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
await new Promise((r) => setTimeout(r, 5));
|
|
inFlight -= 1;
|
|
return blobResponse();
|
|
});
|
|
vi.stubGlobal('fetch', fetchMock);
|
|
|
|
const refs = Array.from({ length: 6 }, (_, i) =>
|
|
ref(`https://example.com/${i}.png`),
|
|
);
|
|
await downloadAll(refs, { concurrency: 2 });
|
|
expect(maxInFlight).toBeLessThanOrEqual(2);
|
|
});
|
|
}); |