Files
floci-ui/packages/api/src/cloudProxy.test.ts
T
Zakaria 91ce11274d
CI / checks (pull_request) Successful in 25m47s
chore: show TheCloud instead of Floci in console text and messages
Labels, empty states, error messages and defaults the console shows now name
TheCloud or its runtimes. Identifiers that must match the emulator (FLOCI_*
settings, the X-Floci-CSRF header, emulator-returned host names) are unchanged.
2026-10-24 14:30:44 -04:00

268 lines
12 KiB
TypeScript

import {describe, expect, test} from 'bun:test'
import {createCloudAdapterRegistry} from './cloudProxy'
import {checkChildCapabilities} from './cloud-spi/childCapabilities'
import {isServiceType} from './cloud-spi/serviceCatalog'
import type {CloudProvider, CloudServiceAdapter, CloudServiceType, DatabaseActionName, DistributionActionName, KubernetesActionName, ResourceActionName, UserPoolActionName} from './cloud-spi/types'
/**
* Guards the schema-to-implementation contract for every registered adapter.
*
* A schema is a promise to the frontend: it decides which controls render and
* which are greyed out. Three separate bugs came from breaking that promise —
* networking advertised create/delete as available while the adapter threw,
* serverless shipped an invoke panel against an adapter with no invoke, and the
* Azure serverless adapter was advertised as available against a runtime that
* answers 501. These tests make the contract mechanical instead of a convention.
*/
const CLOUDS: CloudProvider[] = ['aws', 'azure', 'gcp']
/** Adapter method that must exist for a capability to claim `available`. */
const METHOD_FOR_ACTION: Record<ResourceActionName, keyof CloudServiceAdapter> = {
list: 'list',
create: 'create',
update: 'update',
delete: 'delete',
inspect: 'get',
encrypt: 'encrypt',
decrypt: 'decrypt',
invoke: 'invoke',
start: 'start',
stop: 'stop',
reboot: 'reboot',
updateTags: 'updateTags',
}
const METHOD_FOR_DATABASE_ACTION = {
listSnapshots: 'listDatabaseSnapshots',
createSnapshot: 'createDatabaseSnapshot',
} satisfies Record<DatabaseActionName, keyof CloudServiceAdapter>
const METHOD_FOR_KUBERNETES_ACTION: Record<KubernetesActionName, keyof CloudServiceAdapter> = {
listNodegroups: 'listKubernetesNodegroups',
createNodegroup: 'createKubernetesNodegroup',
deleteNodegroup: 'deleteKubernetesNodegroup',
listFargateProfiles: 'listKubernetesFargateProfiles',
createFargateProfile: 'createKubernetesFargateProfile',
deleteFargateProfile: 'deleteKubernetesFargateProfile',
}
const METHOD_FOR_DISTRIBUTION_ACTION: Record<DistributionActionName, keyof CloudServiceAdapter> = {
setEnabled: 'setDistributionEnabled',
createInvalidation: 'createDistributionInvalidation',
}
const METHOD_FOR_USER_POOL_ACTION: Record<UserPoolActionName, keyof CloudServiceAdapter> = {
updateUser: 'updateUserPoolUser',
setUserGroups: 'setUserPoolUserGroups',
listGroups: 'listUserPoolGroups',
createGroup: 'createUserPoolGroup',
deleteGroup: 'deleteUserPoolGroup',
testSignIn: 'testUserPoolSignIn',
describeClient: 'describeUserPoolClient',
updateClientOAuth: 'updateUserPoolClientOAuth',
}
const registry = createCloudAdapterRegistry()
function registeredAdapters(): Array<{cloud: CloudProvider; adapter: CloudServiceAdapter}> {
return CLOUDS.flatMap((cloud) =>
registry.servicesFor(cloud).map((service) => ({cloud, adapter: adapterFor(cloud, service)})),
)
}
function adapterFor(cloud: CloudProvider, service: CloudServiceType): CloudServiceAdapter {
const adapter = registry.get(cloud, service)
if (!adapter) throw new Error(`expected an adapter for ${cloud}/${service}`)
return adapter
}
describe('registered adapters honour their schema', () => {
const adapters = registeredAdapters()
test('at least one adapter is registered for every cloud', () => {
for (const cloud of CLOUDS) {
expect(adapters.filter((entry) => entry.cloud === cloud).length).toBeGreaterThan(0)
}
})
for (const {cloud, adapter} of adapters) {
const label = `${cloud}/${adapter.service}`
test(`${label} schema identifies itself consistently`, () => {
const schema = adapter.schema()
expect(schema.cloud).toBe(adapter.cloud)
expect(schema.service).toBe(adapter.service)
expect(isServiceType(schema.service)).toBe(true)
expect(schema.displayName.length).toBeGreaterThan(0)
})
test(`${label} implements every action it advertises`, () => {
for (const action of adapter.schema().actions) {
const method = METHOD_FOR_ACTION[action]
expect(typeof adapter[method]).toBe('function')
}
})
test(`${label} implements every capability it marks available`, () => {
const capabilities = adapter.schema().capabilities?.resourceActions ?? []
const claimed = capabilities.filter((capability) => capability.status === 'available')
for (const capability of claimed) {
const method = METHOD_FOR_ACTION[capability.name]
expect(
typeof adapter[method],
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
).toBe('function')
}
const databaseCapabilities = adapter.schema().capabilities?.databaseActions ?? []
const databaseClaims = databaseCapabilities.filter(
(capability) => capability.status === 'available' || (capability.status === 'partial' && capability.enabled),
)
for (const capability of databaseClaims) {
const method = METHOD_FOR_DATABASE_ACTION[capability.name]
expect(
typeof adapter[method],
`${label} advertises ${capability.name} as ${capability.status} but has no ${String(method)}()`,
).toBe('function')
}
const kubernetesCapabilities = adapter.schema().capabilities?.kubernetesActions ?? []
const kubernetesClaims = kubernetesCapabilities.filter((capability) => capability.status === 'available')
for (const capability of kubernetesClaims) {
const method = METHOD_FOR_KUBERNETES_ACTION[capability.name]
expect(
typeof adapter[method],
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
).toBe('function')
}
const userPoolClaims = (adapter.schema().capabilities?.userPoolActions ?? []).filter((capability) => capability.status === 'available')
for (const capability of userPoolClaims) {
const method = METHOD_FOR_USER_POOL_ACTION[capability.name]
expect(
typeof adapter[method],
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
).toBe('function')
}
const distributionClaims = (adapter.schema().capabilities?.distributionActions ?? []).filter((capability) => capability.status === 'available')
for (const capability of distributionClaims) {
const method = METHOD_FOR_DISTRIBUTION_ACTION[capability.name]
expect(
typeof adapter[method],
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
).toBe('function')
}
})
test(`${label} child collections match their advertised capabilities`, () => {
expect(checkChildCapabilities(adapter), `${label} child capability mismatch`).toEqual([])
})
test(`${label} explains every capability it does not fully support`, () => {
const capabilities = [
...(adapter.schema().capabilities?.resourceActions ?? []),
...(adapter.schema().capabilities?.objectActions ?? []),
...(adapter.schema().capabilities?.databaseActions ?? []),
...(adapter.schema().capabilities?.kubernetesActions ?? []),
...(adapter.schema().capabilities?.userPoolActions ?? []),
...(adapter.schema().capabilities?.distributionActions ?? []),
...(adapter.schema().capabilities?.collectionActions ?? []),
...(adapter.schema().capabilities?.itemActions ?? []),
]
for (const capability of capabilities) {
if (capability.status === 'available') continue
expect(
capability.reason?.length ?? 0,
`${label} capability ${capability.name} is ${capability.status} with no reason`,
).toBeGreaterThan(0)
}
})
test(`${label} declares object capabilities only when it can serve them`, () => {
const objectActions = adapter.schema().capabilities?.objectActions ?? []
if (objectActions.length === 0) return
// An object-action block means the resource is a container of objects.
expect(typeof adapter.listObjects).toBe('function')
})
test(`${label} columns resolve to a path`, () => {
for (const column of adapter.schema().columns) {
expect((column.path ?? column.name).length).toBeGreaterThan(0)
}
})
}
})
describe('adapter capability advertisements match the runtime reality', () => {
test('AWS database advertises instance and snapshot capabilities', () => {
const schema = adapterFor('aws', 'database').schema()
expect(schema.actions).toEqual(['list', 'create', 'update', 'delete', 'inspect'])
expect(schema.capabilities?.resourceActions?.map(({name, enabled, status, runtimeRequired}) => ({
name,
enabled,
status,
runtimeRequired,
}))).toEqual([
{name: 'list', enabled: true, status: 'available', runtimeRequired: true},
{name: 'create', enabled: true, status: 'available', runtimeRequired: true},
{name: 'update', enabled: true, status: 'available', runtimeRequired: true},
{name: 'delete', enabled: true, status: 'available', runtimeRequired: true},
{name: 'inspect', enabled: true, status: 'available', runtimeRequired: true},
{name: 'start', enabled: true, status: 'available', runtimeRequired: true},
{name: 'stop', enabled: true, status: 'available', runtimeRequired: true},
{name: 'reboot', enabled: true, status: 'available', runtimeRequired: true},
])
expect(schema.capabilities?.resourceActions?.find(({name}) => name === 'create')?.label).toBe('Create DB instance')
expect(schema.capabilities?.resourceActions?.find(({name}) => name === 'update')?.label).toBe('Update DB instance')
expect(schema.capabilities?.databaseActions).toEqual([
{
name: 'listSnapshots',
label: 'List DB snapshots',
enabled: true,
status: 'available',
runtimeRequired: true,
},
{
name: 'createSnapshot',
label: 'Create DB snapshot',
enabled: true,
status: 'partial',
reason: 'The Cloud Proxy operation is available, but the current runtime does not implement CreateDBSnapshot.',
runtimeRequired: true,
},
])
})
test('AWS serverless advertises invoke and implements it', () => {
const adapter = adapterFor('aws', 'serverless')
const invoke = adapter.schema().capabilities?.resourceActions?.find((c) => c.name === 'invoke')
expect(invoke?.status).toBe('available')
expect(typeof adapter.invoke).toBe('function')
})
test('AWS networking does not advertise create or delete as available', () => {
const adapter = adapterFor('aws', 'networking')
const actions = adapter.schema().capabilities?.resourceActions ?? []
for (const name of ['create', 'delete'] as const) {
const capability = actions.find((c) => c.name === name)
expect(capability?.status).not.toBe('available')
expect(capability?.reason).toBeTruthy()
}
})
test('Azure serverless reports its runtime gap through descriptorOverride', () => {
const override = adapterFor('azure', 'serverless').descriptorOverride?.()
expect(override?.availability).toBe('coming_soon')
expect(override?.reason).toContain('501')
})
})