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