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>; 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); }); });