Files
splatoon2.ink/workers/updater/admin.spec.mjs
2026-09-13 10:06:31 -07:00

90 lines
3.6 KiB
JavaScript

import { expect, it, vi, afterEach } from 'vitest';
import { adminRequest } from './src/admin/routes.mjs';
import { verifyAccess } from './src/admin/access.mjs';
vi.mock('./src/admin/access.mjs', () => ({ verifyAccess: vi.fn() }));
afterEach(() => vi.resetAllMocks());
const url = 'https://admin.example.test';
const post = (mode = 'both', origin = url) =>
new Request(url + '/admin/api/run', {
method: 'POST',
headers: { Origin: origin, 'Content-Type': 'application/json' },
body: JSON.stringify({ mode }),
});
it('requires Access before exposing the panel, status or actions', async () => {
verifyAccess.mockResolvedValue(null);
const get = vi.fn();
for (const request of [
new Request(url + '/'),
new Request(url + '/admin/'),
new Request(url + '/admin/api/status'),
post(),
])
expect((await adminRequest(request, {}, get)).status).toBe(401);
expect(get).not.toHaveBeenCalled();
});
it('rejects cross-origin requests and unknown modes before scheduling work', async () => {
verifyAccess.mockResolvedValue({ email: 'admin@example.test' });
const get = vi.fn();
expect((await adminRequest(post('both', 'https://other.test'), {}, get)).status).toBe(403);
expect((await adminRequest(post('force'), {}, get)).status).toBe(400);
expect(get).not.toHaveBeenCalled();
});
it('returns an accepted run immediately and reports overlap without starting another', async () => {
verifyAccess.mockResolvedValue({ email: 'admin@example.test' });
const startManual = vi.fn(async mode => ({
ok: true,
run: { id: 'one', mode, status: 'queued' },
}));
expect((await adminRequest(post('social'), {}, () => ({ startManual }))).status).toBe(202);
expect(startManual).toHaveBeenCalledWith('social');
startManual.mockResolvedValue({ ok: false, busy: true });
expect((await adminRequest(post(), {}, () => ({ startManual }))).status).toBe(409);
});
it.each(['/', '/admin/'])(
'serves the mobile panel at %s with no-store and a nonce-based content policy',
async path => {
verifyAccess.mockResolvedValue({ email: 'admin@example.test' });
const response = await adminRequest(new Request(url + path), {}, vi.fn());
expect(response.headers.get('cache-control')).toBe('no-store');
expect(response.headers.get('content-security-policy')).toContain('frame-ancestors \'none\'');
expect(await response.text()).not.toContain('__NONCE__');
},
);
it('protects scheduling changes with Access, same-origin checks and boolean validation', async () => {
const request = (enabled, origin = url) => new Request(url + '/admin/api/scheduling', {
method: 'POST',
headers: { Origin: origin, 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled }),
});
const setAutomaticScheduling = vi.fn(async enabled => ({ ok: true, automaticSchedulingEnabled: enabled }));
const scheduler = () => ({ setAutomaticScheduling });
verifyAccess.mockResolvedValue(null);
expect((await adminRequest(request(false), {}, scheduler)).status).toBe(401);
verifyAccess.mockResolvedValue({ email: 'admin@example.test' });
expect((await adminRequest(request(false, 'https://other.test'), {}, scheduler)).status).toBe(403);
expect((await adminRequest(request('false'), {}, scheduler)).status).toBe(400);
expect(setAutomaticScheduling).not.toHaveBeenCalled();
expect((await adminRequest(request(false), {}, scheduler)).status).toBe(200);
expect(setAutomaticScheduling).toHaveBeenCalledWith(false);
setAutomaticScheduling.mockResolvedValue({ ok: false, busy: true });
expect((await adminRequest(request(true), {}, scheduler)).status).toBe(409);
});