feat: allow loading mods without a Steam API key
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.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ItemNotFoundError, SteamApiError } from '@tts/tts';
|
||||
import { ItemNotFoundError, SteamApiError, TtsError } from '@tts/tts';
|
||||
import type { TTSMod } from '@tts/shared';
|
||||
import items from './items.js';
|
||||
|
||||
@@ -16,12 +16,34 @@ describe('items route', () => {
|
||||
expect(await res.json()).toEqual({ error: 'Item ID must be a number' });
|
||||
});
|
||||
|
||||
it('returns 500 when the API key is missing', async () => {
|
||||
it('returns 500 when the API key is missing and no fileUrl is given', async () => {
|
||||
vi.spyOn(await import('@tts/tts'), 'fetchMod').mockRejectedValue(
|
||||
new TtsError('STEAM_API_KEY is not configured', 500),
|
||||
);
|
||||
const res = await items.request('/123', {}, { STEAM_API_KEY: '', PORT: 3000 });
|
||||
expect(res.status).toBe(500);
|
||||
expect(await res.json()).toEqual({ error: 'STEAM_API_KEY is not configured' });
|
||||
});
|
||||
|
||||
it('loads from a fileUrl without an API key', async () => {
|
||||
const mod: TTSMod = {
|
||||
GameMode: 'Tabletop',
|
||||
Date: '2024-01-01',
|
||||
ObjectStates: [],
|
||||
};
|
||||
const fetchModFromUrl = vi
|
||||
.spyOn(await import('@tts/tts'), 'fetchModFromUrl')
|
||||
.mockResolvedValue(mod);
|
||||
const res = await items.request(
|
||||
'/123?fileUrl=https%3A%2F%2Fexample.com%2Fsave.json',
|
||||
{},
|
||||
{ STEAM_API_KEY: '', PORT: 3000 },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(await res.json()).toEqual(mod);
|
||||
expect(fetchModFromUrl).toHaveBeenCalledWith('https://example.com/save.json');
|
||||
});
|
||||
|
||||
it('returns the parsed mod on success', async () => {
|
||||
const mod: TTSMod = {
|
||||
GameMode: 'Tabletop',
|
||||
@@ -75,6 +97,27 @@ describe('items file route', () => {
|
||||
expect(await res.arrayBuffer()).toEqual(new Uint8Array([1, 2, 3]).buffer);
|
||||
});
|
||||
|
||||
it('serves the raw file from a fileUrl without an API key', async () => {
|
||||
const fetchModFileFromUrl = vi
|
||||
.spyOn(await import('@tts/tts'), 'fetchModFileFromUrl')
|
||||
.mockResolvedValue({
|
||||
data: new Uint8Array([1, 2, 3]).buffer,
|
||||
filename: 'mod.json',
|
||||
});
|
||||
const res = await items.request(
|
||||
'/123/file?fileUrl=https%3A%2F%2Fexample.com%2Fsave.json',
|
||||
{},
|
||||
{ STEAM_API_KEY: '', PORT: 3000 },
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.headers.get('content-disposition')).toBe(
|
||||
'attachment; filename="mod.json"',
|
||||
);
|
||||
expect(fetchModFileFromUrl).toHaveBeenCalledWith(
|
||||
'https://example.com/save.json',
|
||||
);
|
||||
});
|
||||
|
||||
it('maps SteamApiError to 502', async () => {
|
||||
vi.spyOn(await import('@tts/tts'), 'fetchModFile').mockRejectedValue(
|
||||
new SteamApiError('Steam API responded 500'),
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Hono } from 'hono';
|
||||
import { itemIdSchema, type TTSMod } from '@tts/shared';
|
||||
import { fetchMod, fetchModFile, TtsError } from '@tts/tts';
|
||||
import {
|
||||
fetchMod,
|
||||
fetchModFile,
|
||||
fetchModFileFromUrl,
|
||||
fetchModFromUrl,
|
||||
TtsError,
|
||||
} from '@tts/tts';
|
||||
import type { Bindings } from '../env.js';
|
||||
|
||||
const app = new Hono<{ Bindings: Bindings }>();
|
||||
@@ -11,13 +17,13 @@ app.get('/:id', async (c) => {
|
||||
return c.json({ error: 'Item ID must be a number' }, 400);
|
||||
}
|
||||
|
||||
const apiKey = c.env.STEAM_API_KEY;
|
||||
if (!apiKey) {
|
||||
return c.json({ error: 'STEAM_API_KEY is not configured' }, 500);
|
||||
}
|
||||
|
||||
try {
|
||||
const mod = await fetchMod(id, apiKey);
|
||||
const fileUrl = c.req.query('fileUrl');
|
||||
// With no Steam API key, the caller must supply the save URL (e.g. from a
|
||||
// search result). Otherwise resolve it via the Steam API.
|
||||
const mod: TTSMod = fileUrl
|
||||
? await fetchModFromUrl(fileUrl)
|
||||
: await fetchMod(id, c.env.STEAM_API_KEY ?? '');
|
||||
return c.json<TTSMod>(mod);
|
||||
} catch (err) {
|
||||
if (err instanceof TtsError) {
|
||||
@@ -33,13 +39,11 @@ app.get('/:id/file', async (c) => {
|
||||
return c.json({ error: 'Item ID must be a number' }, 400);
|
||||
}
|
||||
|
||||
const apiKey = c.env.STEAM_API_KEY;
|
||||
if (!apiKey) {
|
||||
return c.json({ error: 'STEAM_API_KEY is not configured' }, 500);
|
||||
}
|
||||
|
||||
try {
|
||||
const { data, filename } = await fetchModFile(id, apiKey);
|
||||
const fileUrl = c.req.query('fileUrl');
|
||||
const { data, filename } = fileUrl
|
||||
? await fetchModFileFromUrl(fileUrl)
|
||||
: await fetchModFile(id, c.env.STEAM_API_KEY ?? '');
|
||||
return new Response(data, {
|
||||
headers: {
|
||||
'Content-Type': 'application/octet-stream',
|
||||
|
||||
Reference in New Issue
Block a user