import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { Hono } from 'hono';
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { registerStaticFrontend } from '../src/webui/static-frontend';
let root: string;
const html = '
SnowLumaapp';
beforeEach(() => {
root = mkdtempSync(path.join(tmpdir(), 'snowluma-presentation-'));
writeFileSync(path.join(root, 'index.html'), html);
writeFileSync(path.join(root, 'asset.js'), 'window.asset = true;');
});
afterEach(() => rmSync(root, { recursive: true, force: true }));
describe('runtime target document delivery', () => {
it.each(['/', '/index.html', '/settings?tab=appearance'])(
'injects on %s before authentication',
async (url) => {
const app = new Hono();
registerStaticFrontend(app, root, 'maibot');
const response = await app.request(url);
expect(response.status).toBe(200);
expect(response.headers.get('cache-control')).toBe('no-store');
expect(await response.text()).toContain('"target":"maibot"');
},
);
it('reuses the exact same built frontend for an unconfigured deployment', async () => {
const app = new Hono();
registerStaticFrontend(app, root, null);
expect(await (await app.request('/index.html')).text()).toBe(html);
});
it('does not inject into assets or API error responses', async () => {
const app = new Hono();
registerStaticFrontend(app, root, 'maibot');
expect(await (await app.request('/asset.js')).text()).toBe('window.asset = true;');
for (const url of ['/api/missing', '/avatar/missing', '/ui-asset/missing']) {
expect((await app.request(url)).status).toBe(404);
}
});
it('does not serve a protected static path through a second percent decode', async () => {
mkdirSync(path.join(root, 'admin'));
writeFileSync(path.join(root, 'admin', 'private.txt'), 'protected fixture');
const app = new Hono();
app.use('/admin/*', async (c) => c.text('unauthorized', 401));
registerStaticFrontend(app, root, null);
for (const url of ['/admin/private.txt', '/%61dmin/private.txt']) {
const response = await app.request(url);
expect(response.status).toBe(401);
expect(await response.text()).toBe('unauthorized');
}
// The router sees /%61dmin; decoding again would reach /admin
// after the middleware for that prefix has already been skipped.
const response = await app.request('/%%36%31dmin/private.txt');
expect(await response.text()).toBe(html);
});
it.each(['hello world.js', '图标.js'])(
'still serves ordinarily encoded asset names: %s',
async (filename) => {
writeFileSync(path.join(root, filename), 'window.encodedAsset = true;');
const app = new Hono();
registerStaticFrontend(app, root, null);
const response = await app.request(`/${encodeURIComponent(filename)}`);
expect(response.status).toBe(200);
expect(await response.text()).toBe('window.encodedAsset = true;');
},
);
});