Add fetchModFromUrl/fetchModFileFromUrl and accept a fileUrl query param on /items routes so the frontend can download saves directly from a search result's file_url, with no key required.
92 lines
2.9 KiB
TypeScript
92 lines
2.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { fetchModFileFromUrl, fetchModFromUrl, getFileName } from './index.js';
|
|
import { ItemNotFoundError, NoFileError, SteamApiError, TtsError } from './errors.js';
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals();
|
|
});
|
|
|
|
// A minimal valid BSON document: { GameMode: 'Tabletop' }.
|
|
const bsonBytes = new Uint8Array([
|
|
28, 0, 0, 0, 2, 71, 97, 109, 101, 77, 111, 100, 101, 0, 9, 0, 0, 0, 84, 97,
|
|
98, 108, 101, 116, 111, 112, 0, 0,
|
|
]);
|
|
|
|
function bsonResponse(): Response {
|
|
return new Response(bsonBytes);
|
|
}
|
|
|
|
describe('fetchModFromUrl', () => {
|
|
it('downloads and deserializes a save from a URL', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(bsonResponse()));
|
|
const mod = await fetchModFromUrl('https://example.com/save.json');
|
|
expect(mod.GameMode).toBe('Tabletop');
|
|
});
|
|
|
|
it('throws on a failed download', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(new Response(null, { status: 404 })));
|
|
await expect(fetchModFromUrl('https://example.com/save.json')).rejects.toThrow(
|
|
'Failed to download save file (404)',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('fetchModFileFromUrl', () => {
|
|
it('returns bytes and a filename derived from the URL', async () => {
|
|
vi.stubGlobal('fetch', vi.fn().mockResolvedValue(bsonResponse()));
|
|
const { data, filename } = await fetchModFileFromUrl(
|
|
'https://example.com/files/mod.json',
|
|
);
|
|
expect(new Uint8Array(data)).toEqual(bsonBytes);
|
|
expect(filename).toBe('mod.json');
|
|
});
|
|
});
|
|
|
|
describe('getFileName', () => {
|
|
it('parses a quoted content-disposition filename', () => {
|
|
expect(
|
|
getFileName('https://example.com/save', 'attachment; filename="mod.json"'),
|
|
).toBe('mod.json');
|
|
});
|
|
|
|
it('parses an unquoted filename', () => {
|
|
expect(
|
|
getFileName('https://example.com/save', 'attachment; filename=mod.json'),
|
|
).toBe('mod.json');
|
|
});
|
|
|
|
it('falls back to the URL path when there is no disposition', () => {
|
|
expect(getFileName('https://example.com/files/mod.json', null)).toBe(
|
|
'mod.json',
|
|
);
|
|
});
|
|
|
|
it('falls back to a default when the URL has no path', () => {
|
|
expect(getFileName('https://example.com', null)).toBe('save.json');
|
|
});
|
|
});
|
|
|
|
describe('error classes', () => {
|
|
it('ItemNotFoundError carries a 404 status', () => {
|
|
const err = new ItemNotFoundError('123');
|
|
expect(err).toBeInstanceOf(TtsError);
|
|
expect(err.status).toBe(404);
|
|
expect(err.message).toContain('123');
|
|
});
|
|
|
|
it('NoFileError carries a 404 status', () => {
|
|
const err = new NoFileError('123');
|
|
expect(err.status).toBe(404);
|
|
expect(err.message).toContain('123');
|
|
});
|
|
|
|
it('SteamApiError defaults to a 502 status', () => {
|
|
const err = new SteamApiError('boom');
|
|
expect(err.status).toBe(502);
|
|
});
|
|
|
|
it('TtsError defaults to a 500 status', () => {
|
|
const err = new TtsError('boom');
|
|
expect(err.status).toBe(500);
|
|
});
|
|
}); |