5eee0b7e9b
CI / checks (pull_request) Successful in 28m25s
Create and delete Athena workgroups (with a result folder, optionally enforced), and run SQL in one: pick a database from the Glue Data Catalog, preview a table, run with Ctrl+Enter, cancel a running query, read up to 1,000 rows and download the full result CSV from S3. Recent queries in the workgroup can be reopened. TheCloud runs the SQL for real over the files a Glue table points at. The Glue page links to the editor.
394 lines
18 KiB
TypeScript
394 lines
18 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 {AutoScalingActionName, CloudProvider, CloudServiceAdapter, CloudServiceType, CertificateActionName, DatabaseActionName, DataCatalogActionName, DistributionActionName, FileSystemActionName, HttpApiActionName, QueryActionName, 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_HTTP_API_ACTION: Record<HttpApiActionName, keyof CloudServiceAdapter> = {
|
|
describe: 'describeHttpApi',
|
|
createRoute: 'createHttpApiRoute',
|
|
deleteRoute: 'deleteHttpApiRoute',
|
|
setRouteAuthorizer: 'setHttpApiRouteAuthorizer',
|
|
createStage: 'createHttpApiStage',
|
|
deleteStage: 'deleteHttpApiStage',
|
|
deploy: 'deployHttpApi',
|
|
createAuthorizer: 'createHttpApiAuthorizer',
|
|
deleteAuthorizer: 'deleteHttpApiAuthorizer',
|
|
updateCors: 'updateHttpApiCors',
|
|
sendRequest: 'testApiGatewayRoute',
|
|
testWebSocket: 'testWebSocketApi',
|
|
}
|
|
|
|
const METHOD_FOR_DISTRIBUTION_ACTION: Record<DistributionActionName, keyof CloudServiceAdapter> = {
|
|
setEnabled: 'setDistributionEnabled',
|
|
createInvalidation: 'createDistributionInvalidation',
|
|
}
|
|
|
|
const METHOD_FOR_CERTIFICATE_ACTION: Record<CertificateActionName, keyof CloudServiceAdapter> = {
|
|
download: 'getCertificatePem',
|
|
export: 'exportCertificate',
|
|
revoke: 'revokeCertificate',
|
|
}
|
|
|
|
const METHOD_FOR_QUERY_ACTION: Record<QueryActionName, keyof CloudServiceAdapter> = {
|
|
run: 'startQuery',
|
|
results: 'getQueryResult',
|
|
stop: 'stopQuery',
|
|
history: 'listQueries',
|
|
catalog: 'describeQueryCatalog',
|
|
}
|
|
|
|
const METHOD_FOR_DATA_CATALOG_ACTION: Record<DataCatalogActionName, keyof CloudServiceAdapter> = {
|
|
createTable: 'createCatalogTable',
|
|
deleteTable: 'deleteCatalogTable',
|
|
}
|
|
|
|
const METHOD_FOR_FILE_SYSTEM_ACTION: Record<FileSystemActionName, keyof CloudServiceAdapter> = {
|
|
createMountTarget: 'createMountTarget',
|
|
deleteMountTarget: 'deleteMountTarget',
|
|
createAccessPoint: 'createAccessPoint',
|
|
deleteAccessPoint: 'deleteAccessPoint',
|
|
attachFunction: 'attachFileSystemToFunction',
|
|
detachFunction: 'detachFileSystemFromFunction',
|
|
}
|
|
|
|
const METHOD_FOR_AUTO_SCALING_ACTION: Record<AutoScalingActionName, keyof CloudServiceAdapter> = {
|
|
setCapacity: 'setAutoScalingCapacity',
|
|
terminateInstance: 'terminateAutoScalingInstance',
|
|
}
|
|
|
|
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 httpApiClaims = (adapter.schema().capabilities?.httpApiActions ?? []).filter((capability) => capability.status === 'available')
|
|
for (const capability of httpApiClaims) {
|
|
const method = METHOD_FOR_HTTP_API_ACTION[capability.name]
|
|
expect(
|
|
typeof adapter[method],
|
|
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
|
|
).toBe('function')
|
|
}
|
|
|
|
const certificateClaims = (adapter.schema().capabilities?.certificateActions ?? []).filter((capability) => capability.status === 'available')
|
|
for (const capability of certificateClaims) {
|
|
const method = METHOD_FOR_CERTIFICATE_ACTION[capability.name]
|
|
expect(
|
|
typeof adapter[method],
|
|
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
|
|
).toBe('function')
|
|
}
|
|
|
|
const autoScalingClaims = (adapter.schema().capabilities?.autoScalingActions ?? []).filter((capability) => capability.status === 'available')
|
|
for (const capability of autoScalingClaims) {
|
|
const method = METHOD_FOR_AUTO_SCALING_ACTION[capability.name]
|
|
expect(
|
|
typeof adapter[method],
|
|
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
|
|
).toBe('function')
|
|
}
|
|
|
|
const queryClaims = (adapter.schema().capabilities?.queryActions ?? []).filter((capability) => capability.status === 'available')
|
|
for (const capability of queryClaims) {
|
|
const method = METHOD_FOR_QUERY_ACTION[capability.name]
|
|
expect(
|
|
typeof adapter[method],
|
|
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
|
|
).toBe('function')
|
|
}
|
|
|
|
const dataCatalogClaims = (adapter.schema().capabilities?.dataCatalogActions ?? []).filter((capability) => capability.status === 'available')
|
|
for (const capability of dataCatalogClaims) {
|
|
const method = METHOD_FOR_DATA_CATALOG_ACTION[capability.name]
|
|
expect(
|
|
typeof adapter[method],
|
|
`${label} advertises ${capability.name} as available but has no ${String(method)}()`,
|
|
).toBe('function')
|
|
}
|
|
|
|
const fileSystemClaims = (adapter.schema().capabilities?.fileSystemActions ?? []).filter((capability) => capability.status === 'available')
|
|
for (const capability of fileSystemClaims) {
|
|
const method = METHOD_FOR_FILE_SYSTEM_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?.autoScalingActions ?? []),
|
|
...(adapter.schema().capabilities?.fileSystemActions ?? []),
|
|
...(adapter.schema().capabilities?.dataCatalogActions ?? []),
|
|
...(adapter.schema().capabilities?.queryActions ?? []),
|
|
...(adapter.schema().capabilities?.certificateActions ?? []),
|
|
...(adapter.schema().capabilities?.monitoringActions ?? []),
|
|
...(adapter.schema().capabilities?.httpApiActions ?? []),
|
|
...(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')
|
|
})
|
|
})
|