Files
floci-ui/packages/api/src/cloudProxy.test.ts
T
2026-10-24 22:48:41 -04:00

286 lines
13 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, MonitoringActionName, 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_MONITORING_ACTION: Record<MonitoringActionName, keyof CloudServiceAdapter> = {
listMetrics: 'listMetrics',
getMetricData: 'getMetricSeries',
alarmHistory: 'describeAlarmHistory',
setAlarmState: 'setAlarmState',
setAlarmActions: 'setAlarmActionsEnabled',
}
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 monitoringClaims = (adapter.schema().capabilities?.monitoringActions ?? []).filter((capability) => capability.status === 'available')
for (const capability of monitoringClaims) {
const method = METHOD_FOR_MONITORING_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?.monitoringActions ?? []),
...(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')
})
})