182 lines
6.9 KiB
TypeScript
182 lines
6.9 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import {
|
|
JSON_BODY_LIMIT,
|
|
ARTIFACT_FORM_LIMIT,
|
|
BETA_REVISION_FORM_LIMIT
|
|
} from '../../src/lib/server/request-body.js';
|
|
import { MAX_ARTIFACT_BYTES } from '../../src/lib/artifact-limits.js';
|
|
|
|
vi.mock('../../src/lib/server/pipeline/worker.js', () => ({
|
|
kick: vi.fn(),
|
|
kickProvisioning: vi.fn()
|
|
}));
|
|
vi.mock('../../src/lib/server/pipeline/beta-plan-runner.js', () => ({ plannedBetaTick: vi.fn() }));
|
|
|
|
type Event = { request: Request; url: URL; params: { id: string } };
|
|
const routes = import.meta.glob<Record<string, (event: Event) => Promise<Response>>>(
|
|
'../../src/routes/api/**/+server.ts'
|
|
);
|
|
let fixture: Fixture;
|
|
let scriptId: number;
|
|
let betaId: number;
|
|
const token = 'ec_telemetry_0123456789abcdef0123456789abcdef';
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
vi.stubEnv('EC_TELEMETRY_TOKEN', token);
|
|
fixture = await newFixture();
|
|
const { createScript } = await import('../../src/lib/server/fleet/scripts.js');
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
const { script, version } = createScript({
|
|
name: 'HUD',
|
|
targetRelPath: 'hud.gsc',
|
|
content: Buffer.from('main(){}')
|
|
});
|
|
scriptId = script.id;
|
|
betaId = Number(
|
|
db()
|
|
.prepare(
|
|
"INSERT INTO betas (script_id, script_version_id, trigger_kind, state) VALUES (?, ?, 'manual', 'running')"
|
|
)
|
|
.run(script.id, version.id).lastInsertRowid
|
|
);
|
|
});
|
|
afterEach(async () => {
|
|
vi.unstubAllEnvs();
|
|
await fixture.cleanup();
|
|
});
|
|
|
|
function oversizedRequest(method: string, limit: number, declared: boolean) {
|
|
const headers: Record<string, string> = {
|
|
'Content-Type': 'application/json',
|
|
Authorization: `Bearer ${token}`
|
|
};
|
|
if (declared) headers['Content-Length'] = String(limit + 1);
|
|
return new Request('http://localhost', {
|
|
method,
|
|
headers,
|
|
body: declared ? '{}' : ' '.repeat(limit + 1)
|
|
});
|
|
}
|
|
|
|
describe('JSON route size enforcement', () => {
|
|
it.each([
|
|
['autoscaling', 'POST'],
|
|
['betas', 'POST'],
|
|
['betas/[id]/promote', 'POST'],
|
|
['betas/[id]/retry', 'POST'],
|
|
['betas/[id]/test', 'POST'],
|
|
['betas/presets', 'POST'],
|
|
['blueprints', 'POST'],
|
|
['blueprints/[id]', 'PUT'],
|
|
['configs/inspect', 'POST'],
|
|
['deployment-collections', 'POST'],
|
|
['deployment-collections/preview', 'POST'],
|
|
['deployments/house-party', 'POST'],
|
|
['deployments/preview', 'POST'],
|
|
['drift', 'POST'],
|
|
['maintenance', 'POST'],
|
|
['maintenance/[id]', 'POST'],
|
|
['plugins/github/import', 'POST'],
|
|
['plutonium', 'POST'],
|
|
['provisioning', 'POST'],
|
|
['rebalancing', 'POST'],
|
|
['scripts/[id]/scope', 'POST'],
|
|
['server-keys', 'PUT'],
|
|
['server-keys', 'DELETE'],
|
|
['servers', 'POST'],
|
|
['settings', 'PUT'],
|
|
['settings', 'PATCH'],
|
|
['settings/retention', 'POST'],
|
|
['settings/retention/preview', 'POST'],
|
|
['telemetry', 'POST'],
|
|
['watchdog', 'POST'],
|
|
['watchdog/[id]', 'POST']
|
|
])(
|
|
'%s %s preserves HTTP 413 for declared and undeclared oversized bodies',
|
|
async (path, method) => {
|
|
if (path === 'betas/[id]/test') {
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
db()
|
|
.prepare('INSERT INTO beta_test_plans (beta_id, plan) VALUES (?, ?)')
|
|
.run(betaId, '{}');
|
|
}
|
|
const route = await routes[`../../src/routes/api/${path}/+server.ts`]();
|
|
for (const declared of [true, false]) {
|
|
await expect(
|
|
route[method]({
|
|
request: oversizedRequest(method, JSON_BODY_LIMIT, declared),
|
|
url: new URL('http://localhost'),
|
|
params: { id: String(path.startsWith('betas/') ? betaId : scriptId) }
|
|
})
|
|
).rejects.toMatchObject({ status: 413 });
|
|
}
|
|
}
|
|
);
|
|
|
|
it('retains bad-request responses for malformed JSON', async () => {
|
|
for (const path of ['servers', 'plugins/github/import', 'settings']) {
|
|
const method = path === 'settings' ? 'PUT' : 'POST';
|
|
const route = await routes[`../../src/routes/api/${path}/+server.ts`]();
|
|
const response = await route[method]({
|
|
request: new Request('http://localhost', { method, body: '{' }),
|
|
url: new URL('http://localhost'),
|
|
params: { id: '' }
|
|
});
|
|
expect(response.status).toBe(400);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('artifact upload route limits', () => {
|
|
it('preserves the malformed form error on beta revisions', async () => {
|
|
const { POST } = await routes['../../src/routes/api/betas/[id]/revisions/+server.ts']();
|
|
await expect(
|
|
POST({
|
|
request: new Request('http://localhost', { method: 'POST', body: 'invalid form' }),
|
|
url: new URL('http://localhost'),
|
|
params: { id: String(betaId) }
|
|
})
|
|
).rejects.toMatchObject({
|
|
status: 400,
|
|
body: { message: 'Request body must be valid form data.' }
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
'scripts',
|
|
'configs',
|
|
'plugins',
|
|
'scripts/inspect',
|
|
'scripts/[id]/versions',
|
|
'betas/[id]/revisions'
|
|
])('%s rejects excessive declared bodies before creating versions', async (path) => {
|
|
const route = await routes[`../../src/routes/api/${path}/+server.ts`]();
|
|
const limit = path.startsWith('betas/') ? BETA_REVISION_FORM_LIMIT : ARTIFACT_FORM_LIMIT;
|
|
await expect(
|
|
route.POST({
|
|
request: oversizedRequest('POST', limit, true),
|
|
url: new URL('http://localhost'),
|
|
params: { id: String(scriptId) }
|
|
})
|
|
).rejects.toMatchObject({ status: 413 });
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
expect(db().prepare('SELECT COUNT(*) AS count FROM script_versions').get()).toEqual({
|
|
count: 1
|
|
});
|
|
});
|
|
|
|
it('enforces the artifact limit for direct creation and version writes as well', async () => {
|
|
const { createScript, uploadNewVersion, listVersions, getScriptByName } =
|
|
await import('../../src/lib/server/fleet/scripts.js');
|
|
const content = Buffer.alloc(MAX_ARTIFACT_BYTES + 1);
|
|
expect(() =>
|
|
createScript({ name: 'Too big', targetRelPath: 'large.dll', kind: 'plugin', content })
|
|
).toThrow(/32 MiB/);
|
|
expect(() => uploadNewVersion(scriptId, content)).toThrow(/32 MiB/);
|
|
expect(getScriptByName('Too big')).toBeUndefined();
|
|
expect(listVersions(scriptId)).toHaveLength(1);
|
|
});
|
|
});
|