143 lines
6.0 KiB
TypeScript
143 lines
6.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { newFixture, type Fixture } from '../helpers/fixture.js';
|
|
import { prepareNativeHost } from '../helpers/native-host.js';
|
|
|
|
vi.mock('../../src/lib/server/pipeline/worker.js', () => ({ kick: vi.fn(async () => {}) }));
|
|
|
|
let fixture: Fixture;
|
|
let prepared: Awaited<ReturnType<typeof prepareNativeHost>>;
|
|
const custom = 'registry.example/untrusted:latest';
|
|
const launch = {
|
|
game: 't5' as const,
|
|
gameMode: 'zombie' as const,
|
|
mapSlug: 'theater',
|
|
plutoKey: 'saved-key'
|
|
};
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules();
|
|
fixture = await newFixture([]);
|
|
prepared = await prepareNativeHost();
|
|
});
|
|
afterEach(async () => {
|
|
await fixture.cleanup();
|
|
});
|
|
|
|
async function cache(images: string[]) {
|
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
|
const host = hosts.getHost(prepared.host.id)!;
|
|
hosts.heartbeat(host.id, { ...host.inventory, runtimeImages: images });
|
|
}
|
|
|
|
describe('runtime image launch policy', () => {
|
|
it.each([false, true])(
|
|
'rejects an uncached custom image at POST /api/servers (automatic: %s)',
|
|
async (automatic) => {
|
|
const { POST } = await import('../../src/routes/api/servers/+server.js');
|
|
const response = await POST({
|
|
request: new Request('https://controller.test/api/servers', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
...launch,
|
|
image: custom,
|
|
...(automatic ? {} : { hostId: prepared.host.id })
|
|
})
|
|
})
|
|
});
|
|
expect(response.status).toBe(400);
|
|
expect(await response.json()).toMatchObject({
|
|
error: expect.stringContaining('prepared by an admin')
|
|
});
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
for (const table of ['native_creation_intents', 'server_deployments', 'server_keys']) {
|
|
expect(db().prepare(`SELECT * FROM ${table}`).all()).toEqual([]);
|
|
}
|
|
}
|
|
);
|
|
|
|
it('allows the configured default before it is cached', async () => {
|
|
await cache([]);
|
|
const { queueNativeCreation } = await import('../../src/lib/server/provisioning/native.js');
|
|
expect(queueNativeCreation(launch).ok).toBe(true);
|
|
});
|
|
|
|
it('accepts prepared images only on hosts that have them', async () => {
|
|
await cache([custom]);
|
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
|
const other = hosts.createHost({ name: 'Unprepared host' });
|
|
const { nativeLaunchSelection, nativeSelection } =
|
|
await import('../../src/lib/server/provisioning/native.js');
|
|
expect(nativeLaunchSelection({ hostId: prepared.host.id, image: custom }, 't5').image).toBe(
|
|
custom
|
|
);
|
|
expect(nativeSelection({ image: custom }, 't5').hostId).toBe(prepared.host.id);
|
|
expect(() => nativeLaunchSelection({ hostId: other.id, image: custom }, 't5')).toThrow(
|
|
/prepared by an admin/
|
|
);
|
|
hosts.updateHost(prepared.host.id, { mode: 'disabled' });
|
|
expect(() => nativeLaunchSelection({ image: custom }, 't5')).toThrow(
|
|
/prepared by an admin/
|
|
);
|
|
});
|
|
|
|
it('requires exact image references and does not treat a queued admin pull as cached', async () => {
|
|
const hosts = await import('../../src/lib/server/hosts/index.js');
|
|
hosts.queueHostOperation(prepared.host.id, 'runtime.pull', { image: custom });
|
|
const { nativeLaunchSelection } =
|
|
await import('../../src/lib/server/provisioning/native.js');
|
|
expect(() => nativeLaunchSelection({ image: custom }, 't5')).toThrow(
|
|
/prepared by an admin/
|
|
);
|
|
await cache([custom + '-other']);
|
|
expect(() => nativeLaunchSelection({ image: custom }, 't5')).toThrow(
|
|
/prepared by an admin/
|
|
);
|
|
});
|
|
|
|
it('rechecks cached images when submitting a saved single-server preview', async () => {
|
|
await cache([custom]);
|
|
const { previewNativeCreation, queueNativeCreation } =
|
|
await import('../../src/lib/server/provisioning/native.js');
|
|
const preview = previewNativeCreation({ ...launch, image: custom });
|
|
await cache([]);
|
|
expect(queueNativeCreation({ ...launch, previewId: preview.id })).toMatchObject({
|
|
ok: false,
|
|
message: expect.stringContaining('prepared by an admin')
|
|
});
|
|
});
|
|
|
|
it('rejects uncached collection images and rechecks saved previews before writing any jobs', async () => {
|
|
const { saveBlueprint } = await import('../../src/lib/server/fleet/launch-recipes.js');
|
|
const { previewCollection, queueCollection } =
|
|
await import('../../src/lib/server/fleet/collections.js');
|
|
const blueprint = saveBlueprint({
|
|
name: 'Image policy',
|
|
entries: [{ game: 't5', mapSlug: 'theater', count: 2 }]
|
|
});
|
|
expect(() => previewCollection(blueprint.id, 'fill', { image: custom })).toThrow(
|
|
/prepared by an admin/
|
|
);
|
|
await cache([custom]);
|
|
const preview = previewCollection(blueprint.id, 'fill', { image: custom });
|
|
const keys = Object.fromEntries(
|
|
preview.servers.map((server) => [server.name!, 'key-' + server.name])
|
|
);
|
|
await cache([]);
|
|
expect(() => queueCollection({ previewId: preview.id, keys })).toThrow(
|
|
/prepared by an admin/
|
|
);
|
|
const { db } = await import('../../src/lib/server/db/index.js');
|
|
for (const table of [
|
|
'server_launch_collections',
|
|
'native_creation_intents',
|
|
'server_deployments',
|
|
'server_keys'
|
|
]) {
|
|
expect(db().prepare(`SELECT * FROM ${table}`).all()).toEqual([]);
|
|
}
|
|
await cache([custom]);
|
|
expect(queueCollection({ previewId: preview.id, keys }).ok).toBe(true);
|
|
});
|
|
});
|