import assert from 'node:assert/strict' import { test } from 'node:test' import { mutationPreview, requireConfirm, resolveApiTokenSecretName, resolveOrganizationSlug, secretSetupUrl, } from './fly-core.ts' import { destroyMachine, startMachine, stopMachine } from './machines.ts' test('resolveApiTokenSecretName uses flyApiToken and account labels', () => { assert.equal(resolveApiTokenSecretName({}), 'flyApiToken') assert.equal(resolveApiTokenSecretName({ account: 'work' }), 'flyApiToken-work') assert.equal(resolveApiTokenSecretName({ secretName: 'flyApiToken-staging' }), 'flyApiToken-staging') assert.throws(() => resolveApiTokenSecretName({ secretName: 'otherToken' }), /flyApiToken/) }) test('secretSetupUrl stays on the hosted secrets page', () => { assert.match(secretSetupUrl(), /https:\/\/kody\.codes\/account\/secrets\/new\?name=flyApiToken/) assert.match(secretSetupUrl('flyApiToken-work'), /name=flyApiToken-work/) assert.match(secretSetupUrl(), /allowedHosts=api\.machines\.dev,api\.fly\.io/) }) test('requireConfirm blocks live mutations', () => { assert.throws(() => requireConfirm({}, 'destroy machine x'), /confirm: true/) requireConfirm({ confirm: true }, 'destroy machine x') }) test('mutationPreview dryRun skips confirm', () => { const preview = mutationPreview( { dryRun: true }, { action: 'destroy machine x', method: 'DELETE', path: '/apps/my-app/machines/x', requireConfirm: true, }, ) assert.deepEqual(preview, { dryRun: true, action: 'destroy machine x', method: 'DELETE', path: '/apps/my-app/machines/x', query: undefined, body: undefined, }) }) test('start/stop/destroy dryRun do not require confirm or a network call', async () => { const start = await startMachine({ appName: 'my-app', machineId: 'abc', dryRun: true }) const stop = await stopMachine({ appName: 'my-app', machineId: 'abc', dryRun: true, signal: 'SIGTERM' }) const destroy = await destroyMachine({ appName: 'my-app', machineId: 'abc', dryRun: true }) assert.equal(start.dryRun, true) assert.equal(start.method, 'POST') assert.equal(start.path, '/apps/my-app/machines/abc/start') assert.equal(stop.dryRun, true) assert.equal(stop.path, '/apps/my-app/machines/abc/stop') assert.equal(stop.body?.signal, 'SIGTERM') assert.equal(destroy.dryRun, true) assert.equal(destroy.method, 'DELETE') assert.equal(destroy.path, '/apps/my-app/machines/abc') }) test('destroyMachine without confirm or dryRun throws', async () => { await assert.rejects(destroyMachine({ appName: 'my-app', machineId: 'abc' }), /confirm: true/) }) test('resolveOrganizationSlug never defaults to personal', async () => { const originalFetch = globalThis.fetch globalThis.fetch = (async () => { return new Response( JSON.stringify({ data: { organizations: { nodes: [ { id: '1', name: 'Acme', slug: 'acme' }, { id: '2', name: 'Widgets', slug: 'widgets' }, ], }, }, }), { status: 200, headers: { 'content-type': 'application/json' } }, ) }) as typeof fetch try { await assert.rejects(resolveOrganizationSlug({}), /Pass organizationSlug/) const single = await resolveOrganizationSlug({ organizationSlug: 'widgets' }) assert.equal(single, 'widgets') } finally { globalThis.fetch = originalFetch } }) test('resolveOrganizationSlug uses the only visible org', async () => { const originalFetch = globalThis.fetch globalThis.fetch = (async () => { return new Response( JSON.stringify({ data: { organizations: { nodes: [{ id: '1', name: 'Solo', slug: 'solo-org' }] } }, }), { status: 200, headers: { 'content-type': 'application/json' } }, ) }) as typeof fetch try { assert.equal(await resolveOrganizationSlug({}), 'solo-org') } finally { globalThis.fetch = originalFetch } })