Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/fly

src/fly-core.test.ts

110 lines · 3.7 KB · TypeScript
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
	}
})