Files
floci-ui/packages/api/src/routes/clouds.test.ts
T
Zakaria 5eee0b7e9b
CI / checks (pull_request) Successful in 28m25s
feat(api): add Athena workgroups and a query editor to the cloud explorer
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.
2026-10-25 18:10:33 -04:00

2104 lines
97 KiB
TypeScript

import {describe, expect, test} from 'bun:test'
import {Hono} from 'hono'
import {NotImplementedByRuntimeError, RuntimeError, RuntimeUnavailableError, ValidationError} from '../cloud-spi/errors'
import type {
ChildCollection,
ChildItem,
CollectionPage,
DocumentStoreAdapter,
ItemStoreAdapter,
} from '../cloud-spi/childCollections'
import {awsDatabaseSchema, azureDatabaseSchema} from '../cloud-spi/databaseSchema'
import {awsLogsSchema} from '../cloud-spi/logsSchema'
import {azureNoSqlSchema} from '../cloud-spi/noSqlSchema'
import {awsDynamoDbSchema} from '../cloud-spi/dynamodbSchema'
import {awsEksSchema} from '../cloud-spi/eksSchema'
import {awsKmsSchema} from '../cloud-spi/kmsSchema'
import {awsStorageSchema, azureStorageSchema, gcpStorageSchema} from '../cloud-spi/storageSchema'
import {awsSesEmailSchema} from '../cloud-spi/emailSchema'
import {awsUserPoolsSchema} from '../cloud-spi/userPoolsSchema'
import {awsCdnSchema} from '../cloud-spi/cdnSchema'
import {awsAutoScalingSchema} from '../cloud-spi/autoScalingSchema'
import {awsFileSystemsSchema} from '../cloud-spi/fileSystemsSchema'
import {awsDataCatalogSchema} from '../cloud-spi/dataCatalogSchema'
import {awsQuerySchema} from '../cloud-spi/querySchema'
import {awsCertificatesSchema} from '../cloud-spi/certificatesSchema'
import {awsMonitoringSchema} from '../cloud-spi/monitoringSchema'
import {awsAppConfigSchema} from '../cloud-spi/appConfigSchema'
import {awsEventBridgeSchema} from '../cloud-spi/eventbridgeSchema'
import {awsNotificationsSchema} from '../cloud-spi/notificationsSchema'
import type {
AppConfigConfigurationProfile,
AppConfigDeployment,
AppConfigDeploymentStrategy,
AppConfigEnvironment,
AppConfigHostedConfigurationVersion,
CloudProvider,
CloudResource,
CloudServiceAdapter,
CosmosContainer,
CosmosItem,
CosmosQueryResult,
CreateDatabaseSnapshotInput,
CreateResourceInput,
DatabaseSnapshot,
LogsInsightsQueryResult,
} from '../cloud-spi/types'
import {CloudAdapterRegistry} from '../registry/CloudAdapterRegistry'
import {CloudProxyService} from '../service/CloudProxyService'
import type {RuntimeProbe} from '../service/runtimeProbe'
import {createCloudRoutes} from './clouds'
function mockAdapter(cloud: CloudProvider, overrides: Partial<CloudServiceAdapter> = {}): CloudServiceAdapter {
return {
cloud,
service: 'storage',
schema: cloud === 'aws' ? awsStorageSchema : cloud === 'gcp' ? gcpStorageSchema : azureStorageSchema,
list: async () => [],
get: async () => null,
create: async (_input: CreateResourceInput): Promise<CloudResource> => ({
id: 'created',
name: 'created',
cloud,
service: 'storage',
type: cloud === 'azure' ? 'container' : 'bucket',
region: null,
createdAt: null,
metadata: {},
}),
delete: async () => {},
listObjects: async (resourceId: string, prefix = '') => ({
prefix,
objects: [{
key: `${resourceId}/object.txt`,
name: 'object.txt',
type: 'object',
size: 12,
lastModified: null,
metadata: {},
}],
}),
...overrides,
}
}
/**
* Runtime probes make real HTTP calls to the emulator endpoints, so tests inject
* stubs — otherwise status assertions depend on whether a container happens to
* be listening, which passes locally and fails in CI.
*/
function stubProbes(overrides: Partial<Record<CloudProvider, RuntimeProbe>> = {}): Record<CloudProvider, RuntimeProbe> {
const reachable: RuntimeProbe = async () => {}
return {aws: reachable, azure: reachable, gcp: reachable, ...overrides}
}
function unreachable(message: string): RuntimeProbe {
return async () => {
throw new RuntimeUnavailableError(message)
}
}
function appWithRoutes(
adapters: CloudServiceAdapter[] = [mockAdapter('aws'), mockAdapter('azure')],
probes: Record<CloudProvider, RuntimeProbe> = stubProbes(),
) {
const app = new Hono()
const registry = new CloudAdapterRegistry(adapters)
app.route('/api/clouds', createCloudRoutes(new CloudProxyService(registry, probes)))
return app
}
describe('EventBridge rule routes', () => {
test('routes rule lifecycle requests through the Events adapter using the event bus resource id', async () => {
const calls: Array<{operation: string; args: unknown[]}> = []
const adapter = mockAdapter('aws', {
service: 'events',
schema: awsEventBridgeSchema,
listEventBridgeRules: async (...args) => {
calls.push({operation: 'list', args})
return []
},
createEventBridgeRule: async (...args) => {
calls.push({operation: 'create', args})
return {
name: 'hourly', busName: 'orders', busId: 'event-bus:orders', arn: null,
scheduleExpression: 'rate(1 hour)', state: 'ENABLED', targets: [],
}
},
deleteEventBridgeRule: async (...args) => {
calls.push({operation: 'delete', args})
},
})
const app = appWithRoutes([adapter])
const create = await app.request('/api/clouds/aws/services/events/resources/event-bus:orders/rules', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
name: 'hourly', scheduleExpression: 'rate(1 hour)',
targets: [{id: 'worker', arn: 'arn:aws:lambda:us-east-1:000000000000:function:worker'}],
}),
})
expect(create.status).toBe(201)
await app.request('/api/clouds/aws/services/events/resources/event-bus:orders/rules')
const remove = await app.request('/api/clouds/aws/services/events/resources/event-bus:orders/rules/hourly', {method: 'DELETE'})
expect(remove.status).toBe(200)
expect(calls).toEqual([
{operation: 'create', args: ['event-bus:orders', {name: 'hourly', scheduleExpression: 'rate(1 hour)', eventPattern: undefined, state: undefined, description: undefined, targets: [{id: 'worker', arn: 'arn:aws:lambda:us-east-1:000000000000:function:worker'}]}]},
{operation: 'list', args: ['event-bus:orders']},
{operation: 'delete', args: ['event-bus:orders', 'hourly']},
])
})
})
describe('API Gateway CORS routes', () => {
test('dispatches resource-scoped CORS configuration through the API Gateway adapter', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'apigateway',
enableApiGatewayCors: async (apiId, input) => {
calls.push({apiId, input})
return {configuredMethods: ['GET'], proxyMethods: ['POST']}
},
})])
const response = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/cors', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({resourceId: 'resource-456', stage: 'dev', origin: '*', headers: 'Content-Type', methods: ['GET', 'POST']}),
})
expect(response.status).toBe(200)
expect(await response.json()).toEqual({configuredMethods: ['GET'], proxyMethods: ['POST']})
expect(calls).toEqual([{apiId: 'api-123', input: {
resourceId: 'resource-456', stage: 'dev', origin: '*', headers: 'Content-Type', methods: ['GET', 'POST'],
}}])
})
})
describe('API Gateway method test routes', () => {
test('dispatches the AWS TestInvokeMethod input without requiring a stage', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'apigateway',
testApiGatewayMethod: async (apiId, input) => {
calls.push({apiId, input})
return {statusCode: 200, body: '{}', headers: {}, multiValueHeaders: {}, log: 'ok', latency: 4}
},
})])
const response = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/methods/test', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({
resourceId: 'resource-456', httpMethod: 'POST', pathWithQueryString: '/orders?dryRun=true', body: '{}',
headers: {'content-type': 'application/json'}, multiValueHeaders: {'x-tag': ['one', 'two']}, stageVariables: {environment: 'test'},
}),
})
expect(response.status).toBe(200)
expect(await response.json()).toEqual({statusCode: 200, body: '{}', headers: {}, multiValueHeaders: {}, log: 'ok', latency: 4})
expect(calls).toEqual([{apiId: 'api-123', input: {
resourceId: 'resource-456', httpMethod: 'POST', pathWithQueryString: '/orders?dryRun=true', body: '{}',
headers: {'content-type': 'application/json'}, multiValueHeaders: {'x-tag': ['one', 'two']}, stageVariables: {environment: 'test'},
}}])
})
})
describe('API Gateway resource routes', () => {
test('dispatches parent-scoped nested route creation and returns the resource tree', async () => {
const calls: unknown[] = []
const tree = {
resources: [
{resourceId: 'root-123', parentId: null, path: '/', pathPart: null, methods: []},
{resourceId: 'orders-123', parentId: 'root-123', path: '/orders', pathPart: 'orders', methods: []},
],
routes: [],
stages: ['dev'],
}
const app = appWithRoutes([mockAdapter('aws', {
service: 'apigateway',
createApiGatewayLambdaRoute: async (apiId, input) => {
calls.push({apiId, input})
return {resourceId: 'order-123', deploymentId: 'deployment-123', invokeUrl: '/orders/{id}'}
},
listApiGatewayRoutes: async () => tree,
})])
const create = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/routes', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({
path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}',
method: 'PATCH', stage: 'dev', functionName: 'orders-handler',
}),
})
const list = await app.request('/api/clouds/aws/services/apigateway/resources/api-123/routes')
expect(create.status).toBe(201)
expect(await list.json()).toEqual(tree)
expect(calls).toEqual([{apiId: 'api-123', input: {
path: '/orders/{id}', parentResourceId: 'orders-123', pathPart: '{id}',
method: 'PATCH', stage: 'dev', functionName: 'orders-handler',
}}])
})
})
describe('S3 website routes', () => {
test('routes normalized website configuration lifecycle requests through the storage adapter', async () => {
const calls: Array<{operation: string; args: unknown[]}> = []
const adapter = mockAdapter('aws', {
getS3BucketWebsite: async (...args) => {
calls.push({operation: 'get', args})
return {mode: 'index', indexDocument: 'index.html', errorDocument: null}
},
putS3BucketWebsite: async (...args) => { calls.push({operation: 'put', args}) },
deleteS3BucketWebsite: async (...args) => { calls.push({operation: 'delete', args}) },
})
const app = appWithRoutes([adapter])
const get = await app.request('/api/clouds/aws/services/storage/resources/assets/website')
const put = await app.request('/api/clouds/aws/services/storage/resources/assets/website', {
method: 'PUT', headers: {'content-type': 'application/json'},
body: JSON.stringify({mode: 'redirect', hostName: 'www.example.com', protocol: 'https'}),
})
const remove = await app.request('/api/clouds/aws/services/storage/resources/assets/website', {method: 'DELETE'})
expect(await get.json()).toEqual({mode: 'index', indexDocument: 'index.html', errorDocument: null})
expect(put.status).toBe(200)
expect(remove.status).toBe(200)
expect(calls).toEqual([
{operation: 'get', args: ['assets']},
{operation: 'put', args: ['assets', {mode: 'redirect', hostName: 'www.example.com', protocol: 'https'}]},
{operation: 'delete', args: ['assets']},
])
})
})
describe('SNS subscription routes', () => {
test('routes subscription lifecycle requests through the Notifications adapter', async () => {
const calls: Array<{operation: string; args: unknown[]}> = []
const adapter = mockAdapter('aws', {
service: 'notifications',
schema: awsNotificationsSchema,
createSnsSubscription: async (...args) => {
calls.push({operation: 'create', args})
return {arn: 'subscription-1', protocol: 'sqs', endpoint: 'arn:aws:sqs:us-east-1:000000000000:orders', status: 'Confirmed', filterPolicy: null}
},
listSnsSubscriptions: async (...args) => {
calls.push({operation: 'list', args})
return []
},
deleteSnsSubscription: async (...args) => {
calls.push({operation: 'delete', args})
},
})
const app = appWithRoutes([adapter])
const create = await app.request('/api/clouds/aws/services/notifications/resources/orders/subscriptions', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({protocol: 'sqs', endpoint: 'arn:aws:sqs:us-east-1:000000000000:orders'}),
})
expect(create.status).toBe(201)
await app.request('/api/clouds/aws/services/notifications/resources/orders/subscriptions')
const remove = await app.request('/api/clouds/aws/services/notifications/resources/orders/subscriptions/subscription-1', {method: 'DELETE'})
expect(remove.status).toBe(200)
expect(calls).toEqual([
{operation: 'create', args: ['orders', {protocol: 'sqs', endpoint: 'arn:aws:sqs:us-east-1:000000000000:orders'}]},
{operation: 'list', args: ['orders']},
{operation: 'delete', args: ['subscription-1']},
])
})
})
const databaseSnapshot: DatabaseSnapshot = {
id: 'orders-db-snapshot-1',
name: 'orders-db-snapshot-1',
instanceIdentifier: 'orders-db',
status: 'available',
engine: 'postgres',
version: '16.4',
createdAt: '2026-08-14T09:30:00.000Z',
metadata: {snapshotType: 'manual'},
}
describe('database snapshot routes', () => {
test('lists snapshots with the exact optional instance filter', async () => {
let delegatedIdentifier: string | undefined
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
listDatabaseSnapshots: async (instanceIdentifier) => {
delegatedIdentifier = instanceIdentifier
return [databaseSnapshot]
},
})])
const res = await app.request(
'/api/clouds/aws/services/database/snapshots?instanceIdentifier=orders-db',
)
expect(res.status).toBe(200)
expect(await res.json()).toEqual([databaseSnapshot])
expect(delegatedIdentifier).toBe('orders-db')
})
test('creates a snapshot with the raw two-field input', async () => {
let delegatedInput: CreateDatabaseSnapshotInput | undefined
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
createDatabaseSnapshot: async (input) => {
delegatedInput = input
return databaseSnapshot
},
})])
const input = {
instanceIdentifier: 'orders-db',
snapshotIdentifier: 'orders-db-snapshot-1',
}
const res = await app.request('/api/clouds/aws/services/database/snapshots', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify(input),
})
expect(res.status).toBe(201)
expect(await res.json()).toEqual(databaseSnapshot)
expect(delegatedInput).toEqual(input)
})
test('returns 501 when the database adapter lacks either snapshot method', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
})])
const listRes = await app.request('/api/clouds/aws/services/database/snapshots')
const createRes = await app.request('/api/clouds/aws/services/database/snapshots', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
instanceIdentifier: 'orders-db',
snapshotIdentifier: 'orders-db-snapshot-1',
}),
})
expect(listRes.status).toBe(501)
expect((await listRes.json()).code).toBe('operation_not_supported')
expect(createRes.status).toBe(501)
expect((await createRes.json()).code).toBe('operation_not_supported')
})
test('maps adapter snapshot validation errors to 400', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
createDatabaseSnapshot: async () => {
throw new ValidationError('snapshotIdentifier is required')
},
})])
const res = await app.request('/api/clouds/aws/services/database/snapshots', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({instanceIdentifier: 'orders-db', snapshotIdentifier: ''}),
})
expect(res.status).toBe(400)
const body = await res.json()
expect(body).toMatchObject({
code: 'invalid_request',
message: 'snapshotIdentifier is required',
})
})
test('maps AWS UnsupportedOperation snapshot errors through the shared mapper', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
listDatabaseSnapshots: async () => {
throw Object.assign(new Error('RDS snapshots are unavailable'), {
name: 'UnsupportedOperation',
$fault: 'client',
$metadata: {httpStatusCode: 500},
})
},
})])
const res = await app.request('/api/clouds/aws/services/database/snapshots')
expect(res.status).toBe(501)
const body = await res.json()
expect(body.code).toBe('operation_not_implemented')
expect(body.detail).toBe('RDS snapshots are unavailable')
})
test('lists orderable instance classes with engine filter', async () => {
let delegatedEngine: string | undefined
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
listDatabaseOrderableInstanceClasses: async (engine) => {
delegatedEngine = engine
return ['db.t3.micro', 'db.m8g.large']
},
})])
const res = await app.request(
'/api/clouds/aws/services/database/orderable-classes?engine=postgres',
)
expect(res.status).toBe(200)
expect(await res.json()).toEqual(['db.t3.micro', 'db.m8g.large'])
expect(delegatedEngine).toBe('postgres')
})
})
describe('resource lifecycle routes', () => {
test('routes start, stop, and reboot through the generic adapter lifecycle operations', async () => {
const calls: string[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
start: async (id) => { calls.push(`start:${id}`) },
stop: async (id) => { calls.push(`stop:${id}`) },
reboot: async (id) => { calls.push(`reboot:${id}`) },
})])
for (const action of ['start', 'stop', 'reboot']) {
const response = await app.request(`/api/clouds/aws/services/database/resources/orders-db/${action}`, {method: 'POST'})
expect(response.status).toBe(200)
expect(await response.json()).toEqual({ok: true})
}
expect(calls).toEqual(['start:orders-db', 'stop:orders-db', 'reboot:orders-db'])
})
})
describe('cloud schema routes', () => {
test('returns AWS storage schema', async () => {
const res = await appWithRoutes().request('/api/clouds/aws/services/storage/schema')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('aws')
expect(body.service).toBe('storage')
expect(body.fields[0].name).toBe('bucketName')
})
test('returns Azure storage schema', async () => {
const res = await appWithRoutes().request('/api/clouds/azure/services/storage/schema')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('azure')
expect(body.service).toBe('storage')
expect(body.fields[0].name).toBe('containerName')
})
test('returns Azure database schema when the adapter is registered', async () => {
const app = appWithRoutes([mockAdapter('azure', {
service: 'database',
schema: azureDatabaseSchema,
})])
const res = await app.request('/api/clouds/azure/services/database/schema')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('azure')
expect(body.service).toBe('database')
// Cosmos moved to the nosql category; database now covers Azure SQL and PostgreSQL.
expect(body.displayName).toBe('Azure Databases')
})
test('returns Azure Cosmos NoSQL schema when the adapter is registered', async () => {
const app = appWithRoutes([mockAdapter('azure', {
service: 'nosql',
schema: azureNoSqlSchema,
})])
const res = await app.request('/api/clouds/azure/services/nosql/schema')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.service).toBe('nosql')
expect(body.displayName).toBe('Azure Cosmos DB NoSQL')
})
test('returns AWS DynamoDB schema for the nosql service', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'nosql',
schema: awsDynamoDbSchema,
})])
const res = await app.request('/api/clouds/aws/services/nosql/schema')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('aws')
expect(body.service).toBe('nosql')
expect(body.displayName).toBe('DynamoDB')
})
test('returns GCP storage schema when the adapter is registered', async () => {
const app = appWithRoutes([mockAdapter('gcp', {service: 'storage', schema: gcpStorageSchema})])
const res = await app.request('/api/clouds/gcp/services/storage/schema')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('gcp')
expect(body.service).toBe('storage')
expect(body.fields[0].name).toBe('bucketName')
})
// Previously a static schema was served for any known service even with no
// adapter behind it, so the UI rendered a table that then 501'd on every call.
test('does not serve a schema for a service with no registered adapter', async () => {
const app = appWithRoutes([mockAdapter('aws')])
for (const path of [
'/api/clouds/azure/services/k8s/schema',
'/api/clouds/gcp/services/k8s/schema',
'/api/clouds/gcp/services/database/schema',
'/api/clouds/azure/services/compute/schema',
]) {
const res = await app.request(path)
expect(res.status).toBe(404)
expect((await res.json()).error).toBe('Schema not available')
}
})
test('rejects a service slug that is not in the catalog', async () => {
const res = await appWithRoutes().request('/api/clouds/aws/services/queue/schema')
expect(res.status).toBe(404)
expect((await res.json()).error).toBe('Unknown cloud or service')
})
test('returns AWS cloud status', async () => {
const res = await appWithRoutes().request('/api/clouds/aws/status')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('aws')
expect(body.adapterRegistered).toBe(true)
expect(body.runtime).toBe('reachable')
})
test('returns GCP runtime status without a registered adapter', async () => {
const app = appWithRoutes(
[mockAdapter('aws'), mockAdapter('azure')],
stubProbes({gcp: unreachable('Cannot reach Floci-GCP at http://localhost:4588')}),
)
const res = await app.request('/api/clouds/gcp/status')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.cloud).toBe('gcp')
expect(body.adapterRegistered).toBe(false)
expect(body.runtime).toBe('unavailable')
expect(body.endpoint).toBe('http://localhost:4588')
expect(body.error).toContain('Cannot reach Floci-GCP')
})
test('cloud status reflects the runtime probe, not one adapter listing', async () => {
// Previously a cloud whose storage adapter could list reported "reachable"
// no matter what state the runtime was actually in.
const app = appWithRoutes(
[mockAdapter('aws')],
stubProbes({aws: unreachable('Cannot reach Floci core at http://localhost:4566')}),
)
const body = await (await app.request('/api/clouds/aws/status')).json()
expect(body.runtime).toBe('unavailable')
expect(body.adapterRegistered).toBe(true)
})
test('lists storage objects through the cloud adapter', async () => {
const res = await appWithRoutes().request('/api/clouds/aws/services/storage/resources/demo/objects')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.objects).toHaveLength(1)
expect(body.objects[0].name).toBe('object.txt')
})
test('lists Cosmos containers through the cloud NoSQL adapter', async () => {
const app = appWithRoutes([mockAdapter('azure', {
service: 'nosql',
schema: azureNoSqlSchema,
listCosmosContainers: async (databaseId: string): Promise<CosmosContainer[]> => [{
id: 'items',
name: 'items',
databaseId,
partitionKeyPath: '/id',
createdAt: null,
metadata: {},
}],
})])
const res = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers')
const body = await res.json()
expect(res.status).toBe(200)
expect(body[0].databaseId).toBe('appdb')
expect(body[0].name).toBe('items')
})
test('lists DynamoDB records through the generic item adapter', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'nosql',
schema: awsDynamoDbSchema,
items: {
listItems: async (resourceId) => ({items: [{
id: '{"pk":"item-1"}', collectionId: null, timestamp: null,
body: {pk: 'item-1', name: 'First item', table: resourceId}, metadata: {key: {pk: 'item-1'}},
}], nextCursor: null}),
},
})])
const res = await app.request('/api/clouds/aws/services/nosql/resources/orders/items')
const body = await res.json()
expect(res.status).toBe(200)
expect(body.items[0].metadata.key).toEqual({pk: 'item-1'})
expect(body.items[0].body.table).toBe('orders')
})
test('puts and deletes DynamoDB records through the generic item adapter with the complete key', async () => {
const calls: Array<{resourceId: string; document: Record<string, unknown>}> = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'nosql',
schema: awsDynamoDbSchema,
items: {
listItems: async () => ({items: [], nextCursor: null}),
putItem: async (resourceId, document) => {
calls.push({resourceId, document})
return {id: JSON.stringify({pk: document.pk, sk: document.sk}), collectionId: null, timestamp: null, body: document, metadata: {key: {pk: document.pk, sk: document.sk}}}
},
deleteItem: async (resourceId, itemId) => {
calls.push({resourceId, document: {itemId}})
},
},
})])
const res = await app.request('/api/clouds/aws/services/nosql/resources/orders/items', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({pk: 'item-1', sk: '2026-09-29', name: 'First item'}),
})
const body = await res.json()
expect(res.status).toBe(201)
expect(body.metadata.key).toEqual({pk: 'item-1', sk: '2026-09-29'})
expect(calls).toEqual([{resourceId: 'orders', document: {pk: 'item-1', sk: '2026-09-29', name: 'First item'}}])
const deleteRes = await app.request('/api/clouds/aws/services/nosql/resources/orders/items/%7B%22pk%22%3A%22item-1%22%2C%22sk%22%3A%222026-09-29%22%7D', {method: 'DELETE'})
expect(deleteRes.status).toBe(200)
expect(calls).toEqual([
{resourceId: 'orders', document: {pk: 'item-1', sk: '2026-09-29', name: 'First item'}},
{resourceId: 'orders', document: {itemId: '{"pk":"item-1","sk":"2026-09-29"}'}},
])
})
test('reads one flat item, answering 404 when it is gone', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'userpools',
schema: awsUserPoolsSchema,
items: {
listItems: async () => ({items: [], nextCursor: null}),
getItem: async (_poolId, username) => username === 'bob@example.com'
? {id: 'u-1', collectionId: null, timestamp: null, body: {email: username, groups: ['admins']}, metadata: {}}
: null,
},
})])
const found = await app.request('/api/clouds/aws/services/userpools/resources/us-east-1_x/items/bob%40example.com')
const missing = await app.request('/api/clouds/aws/services/userpools/resources/us-east-1_x/items/nobody')
expect(found.status).toBe(200)
expect((await found.json()).body.groups).toEqual(['admins'])
expect(missing.status).toBe(404)
})
test('routes Cognito user actions, groups and test sign-in to the adapter', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'userpools',
schema: awsUserPoolsSchema,
updateUserPoolUser: async (poolId, username, input) => { calls.push(['action', poolId, username, input]) },
setUserPoolUserGroups: async (poolId, username, groups) => { calls.push(['groups', poolId, username, groups]) },
listUserPoolGroups: async () => [{name: 'admins', description: null, precedence: 0, createdAt: null}],
createUserPoolGroup: async (_poolId, input) => ({name: input.name, description: null, precedence: null, createdAt: null}),
deleteUserPoolGroup: async (poolId, groupName) => { calls.push(['deleteGroup', poolId, groupName]) },
testUserPoolSignIn: async () => ({outcome: 'rejected', code: 'NotAuthorizedException', message: 'Incorrect username or password.'}),
})])
const json = (method: string, body: unknown) => ({method, headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})
const base = '/api/clouds/aws/services/userpools/resources/us-east-1_x'
expect((await app.request(`${base}/users/bob%40example.com/actions`, json('POST', {action: 'disable'}))).status).toBe(200)
expect((await app.request(`${base}/users/bob%40example.com/groups`, json('PUT', {groups: ['admins']}))).status).toBe(200)
expect(await (await app.request(`${base}/groups`)).json()).toEqual([{name: 'admins', description: null, precedence: 0, createdAt: null}])
expect((await app.request(`${base}/groups`, json('POST', {name: 'editors'}))).status).toBe(201)
expect((await app.request(`${base}/groups/editors`, {method: 'DELETE'})).status).toBe(200)
const signIn = await app.request(`${base}/sign-in-test`, json('POST', {clientId: 'c', username: 'bob', password: 'x'}))
expect(signIn.status).toBe(200)
expect((await signIn.json()).outcome).toBe('rejected')
expect(calls).toEqual([
['action', 'us-east-1_x', 'bob@example.com', {action: 'disable'}],
['groups', 'us-east-1_x', 'bob@example.com', ['admins']],
['deleteGroup', 'us-east-1_x', 'editors'],
])
})
test('reads and updates an app client\'s OAuth settings', async () => {
const settings = {clientId: 'c1', clientName: 'web', hasSecret: false, explicitAuthFlows: [], callbackUrls: [], logoutUrls: [], oauthFlows: [], oauthScopes: [], identityProviders: ['COGNITO']}
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'userpools',
schema: awsUserPoolsSchema,
describeUserPoolClient: async () => settings,
updateUserPoolClientOAuth: async (poolId, clientId, input) => {
calls.push([poolId, clientId, input])
return {...settings, ...input}
},
})])
const base = '/api/clouds/aws/services/userpools/resources/us-east-1_x/clients/c1'
const input = {callbackUrls: ['http://localhost:4600/cb'], logoutUrls: [], oauthFlows: ['code'], oauthScopes: ['openid'], identityProviders: ['COGNITO']}
expect(await (await app.request(base)).json()).toEqual(settings)
const updated = await app.request(`${base}/oauth`, {method: 'PUT', headers: {'content-type': 'application/json'}, body: JSON.stringify(input)})
expect(updated.status).toBe(200)
expect((await updated.json()).oauthFlows).toEqual(['code'])
expect(calls).toEqual([['us-east-1_x', 'c1', input]])
})
test('routes CloudWatch metrics, history, alarm state and actions to the adapter', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'monitoring',
schema: awsMonitoringSchema,
listMetrics: async (query) => { calls.push(['metrics', query]); return [{namespace: 'App', metricName: 'Errors', dimensions: []}] },
getMetricSeries: async (query) => { calls.push(['series', query.metricName, query.rangeMinutes]); return {label: 'Errors', statistic: 'Sum', periodSeconds: 60, points: []} },
describeAlarmHistory: async (name) => [{timestamp: null, type: 'StateUpdate', summary: `${name} changed`}],
setAlarmState: async (name, state, reason) => { calls.push(['state', name, state, reason]) },
setAlarmActionsEnabled: async (name, enabled) => { calls.push(['actions', name, enabled]) },
})])
const json = (method: string, body: unknown) => ({method, headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})
const base = '/api/clouds/aws/services/monitoring'
expect(await (await app.request(`${base}/metrics?namespace=App`)).json()).toEqual([{namespace: 'App', metricName: 'Errors', dimensions: []}])
expect((await (await app.request(`${base}/metric-data`, json('POST', {namespace: 'App', metricName: 'Errors', rangeMinutes: 60}))).json()).label).toBe('Errors')
expect((await (await app.request(`${base}/resources/high-errors/history`)).json())[0].summary).toBe('high-errors changed')
expect((await app.request(`${base}/resources/high-errors/state`, json('PUT', {state: 'ALARM', reason: 'test'}))).status).toBe(200)
expect((await app.request(`${base}/resources/high-errors/actions`, json('PUT', {enabled: false}))).status).toBe(200)
expect(calls).toEqual([
['metrics', {namespace: 'App', search: undefined}],
['series', 'Errors', 60],
['state', 'high-errors', 'ALARM', 'test'],
['actions', 'high-errors', false],
])
})
test('enables a distribution and invalidates its paths through the CloudFront adapter', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'cdn',
schema: awsCdnSchema,
setDistributionEnabled: async (id, enabled) => {
calls.push(['enabled', id, enabled])
return {id, name: 'd.cloudfront.net', cloud: 'aws', service: 'cdn', type: 'distribution', region: null, createdAt: null, metadata: {enabled}}
},
createDistributionInvalidation: async (id, paths) => {
calls.push(['invalidate', id, paths])
return {id: 'I1', status: 'Completed', createdAt: null, paths}
},
})])
const json = (method: string, body: unknown) => ({method, headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})
const base = '/api/clouds/aws/services/cdn/resources/E123'
const enabled = await app.request(`${base}/enabled`, json('PUT', {enabled: false}))
expect(enabled.status).toBe(200)
expect((await enabled.json()).metadata.enabled).toBe(false)
const invalidated = await app.request(`${base}/invalidations`, json('POST', {paths: ['/*']}))
expect(invalidated.status).toBe(201)
expect(calls).toEqual([['enabled', 'E123', false], ['invalidate', 'E123', ['/*']]])
})
test('downloads, exports and revokes a certificate through the ACM adapter', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'certificates',
schema: awsCertificatesSchema,
getCertificatePem: async (id) => {
calls.push(['pem', id])
return {certificate: 'CERT', certificateChain: null}
},
exportCertificate: async (id, passphrase) => {
calls.push(['export', id, passphrase])
return {certificate: 'CERT', certificateChain: null, privateKey: 'KEY'}
},
revokeCertificate: async (id, reason) => {
calls.push(['revoke', id, reason])
return {id, name: 'a.test', cloud: 'aws', service: 'certificates', type: 'certificate', region: null, createdAt: null, status: 'REVOKED', metadata: {}}
},
})])
const json = (body: unknown) => ({method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})
const base = '/api/clouds/aws/services/certificates/resources/c1'
expect(await (await app.request(`${base}/pem`)).json()).toEqual({certificate: 'CERT', certificateChain: null})
expect((await (await app.request(`${base}/export`, json({passphrase: 'secret'}))).json()).privateKey).toBe('KEY')
const revoked = await app.request(`${base}/revoke`, json({}))
expect((await revoked.json()).status).toBe('REVOKED')
expect(calls).toEqual([['pem', 'c1'], ['export', 'c1', 'secret'], ['revoke', 'c1', 'UNSPECIFIED']])
})
test('changes capacity and terminates an instance through the Auto Scaling adapter', async () => {
const calls: unknown[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'autoscaling',
schema: awsAutoScalingSchema,
setAutoScalingCapacity: async (id, input) => {
calls.push(['capacity', id, input])
return {id, name: id, cloud: 'aws', service: 'autoscaling', type: 'autoScalingGroup', region: null, createdAt: null, metadata: {desiredCapacity: 3}}
},
terminateAutoScalingInstance: async (id, instanceId, replace) => {
calls.push(['terminate', id, instanceId, replace])
},
})])
const json = (method: string, body: unknown) => ({method, headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})
const base = '/api/clouds/aws/services/autoscaling/resources/web-asg'
const updated = await app.request(`${base}/capacity`, json('PUT', {desiredCapacity: 3}))
expect((await updated.json()).metadata.desiredCapacity).toBe(3)
expect((await app.request(`${base}/instances/i-abc/terminate`, json('POST', {replace: true}))).status).toBe(200)
expect(calls).toEqual([['capacity', 'web-asg', {desiredCapacity: 3}], ['terminate', 'web-asg', 'i-abc', true]])
})
test('runs, reads, lists and cancels queries through the Athena adapter', async () => {
const calls: unknown[] = []
const execution = {id: 'q1', sql: 'SELECT 1', database: 'shop', workGroup: 'analysts', state: 'SUCCEEDED', stateReason: null, statementType: 'DML', submittedAt: null, completedAt: null, runtimeMs: 5, dataScannedBytes: 0, outputLocation: 's3://r/q1.csv'}
const app = appWithRoutes([mockAdapter('aws', {
service: 'query',
schema: awsQuerySchema,
startQuery: async (id, input) => { calls.push(['start', id, input.sql, input.database]); return execution },
getQueryResult: async (id, queryId) => { calls.push(['result', id, queryId]); return {execution, columns: [{name: '_col0', type: 'integer'}], rows: [['1']], truncated: false} },
stopQuery: async (id, queryId) => { calls.push(['stop', id, queryId]) },
listQueries: async (id) => { calls.push(['list', id]); return [execution] },
describeQueryCatalog: async (database) => { calls.push(['catalog', database]); return {databases: ['shop'], database: 'shop', tables: []} },
})])
const base = '/api/clouds/aws/services/query/resources/analysts/queries'
expect((await app.request(base, {method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({sql: 'SELECT 1', database: 'shop'})})).status).toBe(201)
expect((await (await app.request(`${base}/q1`)).json()).rows).toEqual([['1']])
expect((await app.request(base)).status).toBe(200)
expect((await app.request(`${base}/q1/stop`, {method: 'POST'})).status).toBe(200)
expect((await (await app.request('/api/clouds/aws/services/query/catalog?database=shop')).json()).databases).toEqual(['shop'])
expect(calls).toEqual([['start', 'analysts', 'SELECT 1', 'shop'], ['result', 'analysts', 'q1'], ['list', 'analysts'], ['stop', 'analysts', 'q1'], ['catalog', 'shop']])
})
test('creates and deletes catalog tables through the Glue adapter', async () => {
const calls: unknown[] = []
const database = {id: 'shop', name: 'shop', cloud: 'aws' as const, service: 'datacatalog' as const, type: 'glueDatabase', region: null, createdAt: null, metadata: {}}
const app = appWithRoutes([mockAdapter('aws', {
service: 'datacatalog',
schema: awsDataCatalogSchema,
createCatalogTable: async (id, input) => { calls.push(['create', id, input.name, input.format]); return database },
deleteCatalogTable: async (id, tableName) => { calls.push(['delete', id, tableName]); return database },
})])
const base = '/api/clouds/aws/services/datacatalog/resources/shop/tables'
const created = await app.request(base, {method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({name: 'orders', location: 's3://data/orders/', format: 'csv', columns: [{name: 'id', type: 'int'}]})})
expect(created.status).toBe(201)
expect((await app.request(`${base}/orders`, {method: 'DELETE'})).status).toBe(200)
expect(calls).toEqual([['create', 'shop', 'orders', 'csv'], ['delete', 'shop', 'orders']])
})
test('manages mount targets, access points and function mounts through the EFS adapter', async () => {
const calls: unknown[] = []
const fileSystem = {id: 'fs-1', name: 'shared', cloud: 'aws' as const, service: 'filesystems' as const, type: 'fileSystem', region: null, createdAt: null, metadata: {}}
const app = appWithRoutes([mockAdapter('aws', {
service: 'filesystems',
schema: awsFileSystemsSchema,
createMountTarget: async (id, subnetId) => { calls.push(['mount', id, subnetId]); return fileSystem },
deleteMountTarget: async (id, mountTargetId) => { calls.push(['unmount', id, mountTargetId]); return fileSystem },
createAccessPoint: async (id, input) => { calls.push(['ap', id, input.path]); return fileSystem },
deleteAccessPoint: async (id, accessPointId) => { calls.push(['delete-ap', id, accessPointId]); return fileSystem },
attachFileSystemToFunction: async (id, input) => { calls.push(['attach', id, input.functionName, input.mountPath]); return fileSystem },
detachFileSystemFromFunction: async (id, functionName) => { calls.push(['detach', id, functionName]); return fileSystem },
})])
const json = (method: string, body: unknown) => ({method, headers: {'content-type': 'application/json'}, body: JSON.stringify(body)})
const base = '/api/clouds/aws/services/filesystems/resources/fs-1'
expect((await app.request(`${base}/mount-targets`, json('POST', {subnetId: 'subnet-a'}))).status).toBe(201)
expect((await app.request(`${base}/mount-targets/fsmt-1`, {method: 'DELETE'})).status).toBe(200)
expect((await app.request(`${base}/access-points`, json('POST', {path: '/app', uid: 1000, gid: 1000, permissions: '755'}))).status).toBe(201)
expect((await app.request(`${base}/access-points/fsap-1`, {method: 'DELETE'})).status).toBe(200)
expect((await app.request(`${base}/functions`, json('POST', {accessPointId: 'fsap-1', functionName: 'writer', mountPath: '/mnt/share'}))).status).toBe(200)
expect((await app.request(`${base}/functions/writer`, {method: 'DELETE'})).status).toBe(200)
expect(calls).toEqual([
['mount', 'fs-1', 'subnet-a'], ['unmount', 'fs-1', 'fsmt-1'], ['ap', 'fs-1', '/app'], ['delete-ap', 'fs-1', 'fsap-1'],
['attach', 'fs-1', 'writer', '/mnt/share'], ['detach', 'fs-1', 'writer'],
])
})
test('clears the SES mailbox through the email adapter', async () => {
let cleared = false
const app = appWithRoutes([mockAdapter('aws', {
service: 'email',
schema: awsSesEmailSchema,
clearEmailInbox: async () => {
cleared = true
},
})])
const res = await app.request('/api/clouds/aws/services/email/inbox', {method: 'DELETE'})
expect(res.status).toBe(200)
expect(await res.json()).toEqual({ok: true})
expect(cleared).toBeTrue()
})
test('creates and deletes Cosmos databases through the cloud NoSQL adapter', async () => {
const deleted: string[] = []
const app = appWithRoutes([mockAdapter('azure', {
service: 'nosql',
schema: azureNoSqlSchema,
create: async (input: CreateResourceInput): Promise<CloudResource> => ({
id: String(input.values.databaseName),
name: String(input.values.databaseName),
cloud: 'azure',
service: 'nosql',
type: 'cosmos-database',
region: null,
createdAt: null,
metadata: {},
}),
delete: async (id: string) => {
deleted.push(id)
},
})])
const createRes = await app.request('/api/clouds/azure/services/nosql/resources', {
method: 'POST',
body: JSON.stringify({databaseName: 'appdb'}),
})
const created = await createRes.json()
const deleteRes = await app.request('/api/clouds/azure/services/nosql/resources/appdb', {method: 'DELETE'})
expect(createRes.status).toBe(201)
expect(created.type).toBe('cosmos-database')
expect(created.name).toBe('appdb')
expect(deleteRes.status).toBe(200)
expect(deleted).toEqual(['appdb'])
})
test('creates and deletes Cosmos containers through the cloud NoSQL adapter', async () => {
const deleted: Array<{databaseId: string; containerId: string}> = []
const app = appWithRoutes([mockAdapter('azure', {
service: 'nosql',
schema: azureNoSqlSchema,
createCosmosContainer: async (databaseId: string, input: CreateResourceInput): Promise<CosmosContainer> => ({
id: String(input.values.containerName),
name: String(input.values.containerName),
databaseId,
partitionKeyPath: String(input.values.partitionKeyPath),
createdAt: null,
metadata: {},
}),
deleteCosmosContainer: async (databaseId: string, containerId: string) => {
deleted.push({databaseId, containerId})
},
})])
const createRes = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers', {
method: 'POST',
body: JSON.stringify({containerName: 'items', partitionKeyPath: '/category'}),
})
const created = await createRes.json()
const deleteRes = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers/items', {method: 'DELETE'})
expect(createRes.status).toBe(201)
expect(created.databaseId).toBe('appdb')
expect(created.partitionKeyPath).toBe('/category')
expect(deleteRes.status).toBe(200)
expect(deleted).toEqual([{databaseId: 'appdb', containerId: 'items'}])
})
test('upserts, deletes, and queries Cosmos items through the cloud NoSQL adapter', async () => {
const deleted: Array<{databaseId: string; containerId: string; itemId: string; partitionKey?: string | null}> = []
const app = appWithRoutes([mockAdapter('azure', {
service: 'nosql',
schema: azureNoSqlSchema,
upsertCosmosItem: async (databaseId: string, containerId: string, document: Record<string, unknown>): Promise<CosmosItem> => ({
id: String(document.id),
databaseId,
containerId,
partitionKey: String(document.category),
etag: 'etag',
timestamp: null,
document,
}),
deleteCosmosItem: async (databaseId: string, containerId: string, itemId: string, partitionKey?: string | null) => {
deleted.push({databaseId, containerId, itemId, partitionKey})
},
queryCosmosItems: async (_databaseId: string, _containerId: string, query: string): Promise<CosmosQueryResult> => ({
items: [{id: 'item-1', query}],
count: 1,
}),
})])
const upsertRes = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers/items/items', {
method: 'POST',
body: JSON.stringify({id: 'item-1', category: 'demo'}),
})
const upserted = await upsertRes.json()
const queryRes = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers/items/query', {
method: 'POST',
body: JSON.stringify({query: 'SELECT * FROM c'}),
})
const queryBody = await queryRes.json()
const deleteRes = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers/items/items/item-1?partitionKey=demo', {method: 'DELETE'})
expect(upsertRes.status).toBe(201)
expect(upserted.partitionKey).toBe('demo')
expect(queryRes.status).toBe(200)
expect(queryBody.count).toBe(1)
expect(deleteRes.status).toBe(200)
expect(deleted).toEqual([{databaseId: 'appdb', containerId: 'items', itemId: 'item-1', partitionKey: 'demo'}])
})
test('runs a Logs Insights query through the route', async () => {
let received: {logGroupName?: string; queryString?: string; startTime?: number; endTime?: number; limit?: number} = {}
const app = appWithRoutes([mockAdapter('aws', {
service: 'logs',
schema: awsLogsSchema,
queryLogs: async (logGroupName, input): Promise<LogsInsightsQueryResult> => {
received = {logGroupName, ...input}
return {queryId: 'q-1', status: 'Complete', rows: [{'@message': 'hello'}]}
},
})])
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/query', {
method: 'POST',
body: JSON.stringify({queryString: 'fields @message', startTime: 1000, endTime: 2000}),
})
const body = await res.json()
expect(res.status).toBe(200)
expect(body).toEqual({queryId: 'q-1', status: 'Complete', rows: [{'@message': 'hello'}]})
expect(received).toEqual({logGroupName: '/floci/probe', queryString: 'fields @message', startTime: 1000, endTime: 2000, limit: undefined})
})
test('rejects a Logs Insights query with a non-string queryString before calling the adapter', async () => {
let called = false
const app = appWithRoutes([mockAdapter('aws', {
service: 'logs',
schema: awsLogsSchema,
queryLogs: async (): Promise<LogsInsightsQueryResult> => {
called = true
return {queryId: 'q-1', status: 'Complete', rows: []}
},
})])
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/query', {
method: 'POST',
body: JSON.stringify({queryString: 123, startTime: 1000, endTime: 2000}),
})
expect(res.status).toBe(400)
expect(called).toBe(false)
})
test('rejects a Logs Insights query with a missing startTime before calling the adapter', async () => {
const app = appWithRoutes([mockAdapter('aws', {
service: 'logs',
schema: awsLogsSchema,
queryLogs: async (): Promise<LogsInsightsQueryResult> => ({queryId: 'q-1', status: 'Complete', rows: []}),
})])
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/query', {
method: 'POST',
body: JSON.stringify({queryString: 'fields @message', endTime: 2000}),
})
expect(res.status).toBe(400)
})
test('filters log events with pagination through the route', async () => {
let received: Record<string, unknown> = {}
const app = appWithRoutes([mockAdapter('aws', {
service: 'logs',
schema: awsLogsSchema,
filterLogEvents: async (logGroupName, input, page) => {
received = {logGroupName, input, page}
return {items: [{id: 'event-1', collectionId: 'stream-a', timestamp: null, body: {message: 'ERROR'}, metadata: {}}], nextCursor: 'next'}
},
})])
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/filter?cursor=previous&limit=25', {
method: 'POST',
body: JSON.stringify({filterPattern: 'ERROR', logStreamNames: ['stream-a'], startTime: 1000, endTime: 2000}),
})
expect(res.status).toBe(200)
expect(await res.json()).toMatchObject({nextCursor: 'next'})
expect(received).toEqual({
logGroupName: '/floci/probe',
input: {filterPattern: 'ERROR', logStreamNames: ['stream-a'], startTime: 1000, endTime: 2000},
page: {cursor: 'previous', limit: 25},
})
})
test('rejects invalid log event filters before calling the adapter', async () => {
let called = false
const app = appWithRoutes([mockAdapter('aws', {
service: 'logs',
schema: awsLogsSchema,
filterLogEvents: async () => {
called = true
return {items: [], nextCursor: null}
},
})])
const res = await app.request('/api/clouds/aws/services/logs/resources/%2Ffloci%2Fprobe/filter', {
method: 'POST',
body: JSON.stringify({logStreamNames: 'stream-a'}),
})
expect(res.status).toBe(400)
expect(called).toBe(false)
})
test('normalizes runtime unavailable errors', async () => {
const app = appWithRoutes([
mockAdapter('aws', {
list: async () => {
throw new RuntimeUnavailableError('Cannot reach Floci-AZ at http://localhost:4577: connection refused')
},
}),
])
const res = await app.request('/api/clouds/aws/services/storage/resources')
const body = await res.json()
expect(res.status).toBe(503)
expect(body.code).toBe('runtime_unavailable')
expect(body.message).toBe('Runtime unavailable')
expect(body.detail).toContain('connection refused')
})
test('normalizes not implemented runtime errors', async () => {
const app = appWithRoutes([
mockAdapter('azure', {
create: async () => {
throw new NotImplementedByRuntimeError('Azure Blob request failed: HTTP 501')
},
}),
])
const res = await app.request('/api/clouds/azure/services/storage/resources', {
method: 'POST',
body: JSON.stringify({containerName: 'demo'}),
})
const body = await res.json()
expect(res.status).toBe(501)
expect(body.code).toBe('operation_not_implemented')
expect(body.message).toBe('Operation is not implemented by the selected runtime')
})
test('reports a missing adapter as unsupported rather than a runtime failure', async () => {
const res = await appWithRoutes([mockAdapter('aws')]).request('/api/clouds/azure/services/storage/resources')
const body = await res.json()
expect(res.status).toBe(501)
expect(body.code).toBe('operation_not_supported')
expect(body.detail).toContain('No adapter registered for azure/storage')
})
test('maps a validation error to 400 with the adapter message intact', async () => {
const app = appWithRoutes([
mockAdapter('aws', {
create: async () => {
throw new ValidationError('bucketName is required')
},
}),
])
const res = await app.request('/api/clouds/aws/services/storage/resources', {
method: 'POST',
body: JSON.stringify({}),
})
const body = await res.json()
expect(res.status).toBe(400)
expect(body.code).toBe('invalid_request')
expect(body.message).toBe('bucketName is required')
})
// Before typed errors these AWS SDK failures all collapsed into a blanket 502.
const sdkCases: Array<{name: string; status: number; code: string}> = [
{name: 'BucketAlreadyOwnedByYou', status: 409, code: 'resource_conflict'},
{name: 'AccessDenied', status: 403, code: 'access_denied'},
{name: 'ValidationException', status: 400, code: 'invalid_request'},
{name: 'ThrottlingException', status: 429, code: 'rate_limited'},
{name: 'NoSuchBucket', status: 404, code: 'resource_not_found'},
]
for (const sdkCase of sdkCases) {
test(`maps the AWS SDK ${sdkCase.name} error to ${sdkCase.status}`, async () => {
const app = appWithRoutes([
mockAdapter('aws', {
create: async () => {
const err = new Error(`${sdkCase.name} raised by the runtime`)
err.name = sdkCase.name
Object.assign(err, {$metadata: {httpStatusCode: 500}, $fault: 'client'})
throw err
},
}),
])
const res = await app.request('/api/clouds/aws/services/storage/resources', {
method: 'POST',
body: JSON.stringify({name: 'demo'}),
})
const body = await res.json()
expect(res.status).toBe(sdkCase.status)
expect(body.code).toBe(sdkCase.code)
expect(body.detail ?? body.message).toContain(sdkCase.name)
})
}
})
describe('KMS crypto routes', () => {
test('decodes plaintext and encodes the ciphertext response', async () => {
let delegatedId = ''
let delegatedPlaintext: Uint8Array | undefined
let delegatedContext: Record<string, string> | undefined
const app = appWithRoutes([mockAdapter('aws', {
service: 'kms',
schema: awsKmsSchema,
encrypt: async (id, input) => {
delegatedId = id
delegatedPlaintext = input.plaintext
delegatedContext = input.encryptionContext
return {
ciphertextBlob: new Uint8Array([1, 2, 3]),
keyId: id,
encryptionAlgorithm: input.encryptionAlgorithm,
}
},
})])
const res = await app.request('/api/clouds/aws/services/kms/resources/key-1/encrypt', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
plaintextBase64: Buffer.from('hello').toString('base64'),
encryptionAlgorithm: 'SYMMETRIC_DEFAULT',
encryptionContext: {purpose: 'test'},
}),
})
const body = await res.json()
expect(res.status).toBe(200)
expect(delegatedId).toBe('key-1')
expect(new TextDecoder().decode(delegatedPlaintext)).toBe('hello')
expect(delegatedContext).toEqual({purpose: 'test'})
expect(body).toEqual({
ciphertextBlobBase64: 'AQID',
keyId: 'key-1',
encryptionAlgorithm: 'SYMMETRIC_DEFAULT',
})
expect(res.headers.get('cache-control')).toBe('no-store')
})
test('decodes ciphertext and encodes the plaintext response', async () => {
let delegatedCiphertext: Uint8Array | undefined
const app = appWithRoutes([mockAdapter('aws', {
service: 'kms',
schema: awsKmsSchema,
decrypt: async (id, input) => {
delegatedCiphertext = input.ciphertextBlob
return {
plaintext: new TextEncoder().encode('round trip'),
keyId: id,
encryptionAlgorithm: input.encryptionAlgorithm,
}
},
})])
const res = await app.request('/api/clouds/aws/services/kms/resources/key-1/decrypt', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
ciphertextBlobBase64: 'AQID',
encryptionAlgorithm: 'SYMMETRIC_DEFAULT',
}),
})
const body = await res.json()
expect(res.status).toBe(200)
expect(delegatedCiphertext).toEqual(new Uint8Array([1, 2, 3]))
expect(Buffer.from(body.plaintextBase64, 'base64').toString('utf8')).toBe('round trip')
expect(res.headers.get('cache-control')).toBe('no-store')
})
test('rejects non-canonical base64 before calling the adapter', async () => {
let calls = 0
const app = appWithRoutes([mockAdapter('aws', {
service: 'kms',
schema: awsKmsSchema,
encrypt: async () => {
calls += 1
throw new Error('must not be called')
},
})])
const res = await app.request('/api/clouds/aws/services/kms/resources/key-1/encrypt', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
plaintextBase64: 'not base64',
encryptionAlgorithm: 'SYMMETRIC_DEFAULT',
}),
})
const body = await res.json()
expect(res.status).toBe(400)
expect(body.code).toBe('invalid_request')
expect(body.message).toContain('canonical base64')
expect(calls).toBe(0)
expect(res.headers.get('cache-control')).toBe('no-store')
})
test('rejects malformed JSON as a caller error', async () => {
const app = appWithRoutes([mockAdapter('aws', {service: 'kms', schema: awsKmsSchema})])
const res = await app.request('/api/clouds/aws/services/kms/resources/key-1/decrypt', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: '{',
})
const body = await res.json()
expect(res.status).toBe(400)
expect(body.message).toBe('Request body must be valid JSON')
expect(res.headers.get('cache-control')).toBe('no-store')
})
test('maps provider contract failures to 502 without echoing request data', async () => {
const secretMarker = 'plaintext-that-must-not-be-returned'
const app = appWithRoutes([mockAdapter('aws', {
service: 'kms',
schema: awsKmsSchema,
encrypt: async () => {
throw new RuntimeError('KMS did not return ciphertext')
},
})])
const res = await app.request('/api/clouds/aws/services/kms/resources/key-1/encrypt', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
plaintextBase64: Buffer.from(secretMarker).toString('base64'),
encryptionAlgorithm: 'SYMMETRIC_DEFAULT',
}),
})
const responseText = await res.text()
expect(res.status).toBe(502)
expect(responseText).not.toContain(secretMarker)
expect(responseText).not.toContain(Buffer.from(secretMarker).toString('base64'))
expect(res.headers.get('cache-control')).toBe('no-store')
})
test('sanitizes provider validation text for crypto requests', async () => {
const secretMarker = 'provider-echoed-sensitive-value'
const app = appWithRoutes([mockAdapter('aws', {
service: 'kms',
schema: awsKmsSchema,
encrypt: async () => {
throw Object.assign(new Error(secretMarker), {
name: 'ValidationException',
$fault: 'client',
$metadata: {httpStatusCode: 400},
})
},
})])
const res = await app.request('/api/clouds/aws/services/kms/resources/key-1/encrypt', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({
plaintextBase64: 'AQ==',
encryptionAlgorithm: 'SYMMETRIC_DEFAULT',
}),
})
const responseText = await res.text()
expect(res.status).toBe(400)
expect(responseText).not.toContain(secretMarker)
expect(responseText).toContain('KMS rejected the request')
expect(res.headers.get('cache-control')).toBe('no-store')
})
})
describe('service descriptors', () => {
test('every descriptor carries nav metadata for every cloud', async () => {
for (const cloud of ['aws', 'azure', 'gcp']) {
const res = await appWithRoutes().request(`/api/clouds/${cloud}/services`)
const body = await res.json()
expect(res.status).toBe(200)
expect(body.length).toBeGreaterThan(0)
for (const descriptor of body) {
expect(descriptor.cloud).toBe(cloud)
expect(typeof descriptor.route).toBe('string')
expect(descriptor.route.length).toBeGreaterThan(0)
expect(typeof descriptor.iconKey).toBe('string')
expect(typeof descriptor.group).toBe('string')
expect(typeof descriptor.order).toBe('number')
expect(descriptor.displayName.length).toBeGreaterThan(0)
}
}
})
test('every unavailable service explains itself', async () => {
for (const cloud of ['aws', 'azure', 'gcp']) {
const body = await (await appWithRoutes().request(`/api/clouds/${cloud}/services`)).json()
const unexplained = body.filter(
(d: {availability: string; reason?: string}) => d.availability === 'coming_soon' && !d.reason,
)
expect(unexplained).toEqual([])
}
})
test('availability follows adapter registration rather than a hardcoded list', async () => {
const withK8s = await (await appWithRoutes([
mockAdapter('gcp', {service: 'k8s'}),
]).request('/api/clouds/gcp/services')).json()
const withoutK8s = await (await appWithRoutes([mockAdapter('aws')]).request('/api/clouds/gcp/services')).json()
const find = (body: Array<{service: string; reason?: string}>, service: string) =>
body.find((d) => d.service === service)
expect(find(withK8s, 'k8s')).toMatchObject({availability: 'available'})
expect(find(withoutK8s, 'k8s')).toMatchObject({availability: 'coming_soon'})
expect(find(withoutK8s, 'k8s')?.reason).toContain('GCP')
})
test('an adapter can report coming_soon when its runtime does not implement it', async () => {
// floci-az answers 501 for /functions, so a registered adapter must still
// be able to tell the truth about the runtime behind it.
const app = appWithRoutes([
mockAdapter('azure', {
service: 'serverless',
descriptorOverride: () => ({
availability: 'coming_soon',
reason: 'The Floci-AZ runtime returns 501 NotImplemented for the Azure Functions endpoint.',
}),
}),
])
const body = await (await app.request('/api/clouds/azure/services')).json()
const serverless = body.find((d: {service: string}) => d.service === 'serverless')
expect(serverless.availability).toBe('coming_soon')
expect(serverless.reason).toContain('501')
})
test('routes AWS secrets to its bespoke page and GCP to the generic explorer', async () => {
// Availability now comes from adapter registration like every other service,
// but the absolute route is kept so the card still links to the standalone
// page instead of Cloud Explorer. Without an adapter it reads coming_soon.
const withAdapter = appWithRoutes([
mockAdapter('aws'),
mockAdapter('aws', {service: 'secrets'}),
mockAdapter('gcp', {service: 'secrets'}),
])
const aws = await (await withAdapter.request('/api/clouds/aws/services')).json()
const bare = await (await appWithRoutes().request('/api/clouds/aws/services')).json()
const gcp = await (await withAdapter.request('/api/clouds/gcp/services')).json()
const secretsFor = (body: Array<{service: string}>) => body.find((d) => d.service === 'secrets')
expect(secretsFor(aws)).toMatchObject({availability: 'available', route: '/secretsmanager'})
expect(secretsFor(bare)).toMatchObject({availability: 'coming_soon'})
expect(secretsFor(gcp)).toMatchObject({availability: 'available', route: 'secrets'})
})
test('marks a service listable only when an adapter serves the generic list', async () => {
// MSK is available through its own pre-SPI page; Console Home must not count it
// through /resources, which has no adapter behind it and answers 501.
const body = await (await appWithRoutes([mockAdapter('aws')]).request('/api/clouds/aws/services')).json() as Array<{service: string; availability: string; listable: boolean}>
const of = (service: string) => body.find((descriptor) => descriptor.service === service)
expect(of('storage')).toMatchObject({availability: 'available', listable: true})
expect(of('msk')).toMatchObject({availability: 'available', listable: false})
expect(of('msk-connect')).toMatchObject({availability: 'available', listable: false})
expect(of('serverless')).toMatchObject({availability: 'coming_soon', listable: false})
})
})
describe('AppConfig nested routes', () => {
test('routes AppConfig operations through the adapter', async () => {
const calls: string[] = []
const environment: AppConfigEnvironment = {
id: 'env-1',
applicationId: 'app-1',
name: 'dev',
description: null,
state: 'READY_FOR_DEPLOYMENT',
}
const profile: AppConfigConfigurationProfile = {
id: 'profile-1',
applicationId: 'app-1',
name: 'settings',
description: null,
locationUri: 'hosted',
type: 'AWS.Freeform',
}
const version: AppConfigHostedConfigurationVersion = {
id: 'profile-1:1',
applicationId: 'app-1',
configurationProfileId: 'profile-1',
versionNumber: 1,
description: null,
contentType: 'application/json',
content: '{"enabled":true}',
}
const strategy: AppConfigDeploymentStrategy = {
id: 'strategy-1',
name: 'immediate',
description: null,
deploymentDurationInMinutes: 0,
growthType: 'LINEAR',
growthFactor: 100,
finalBakeTimeInMinutes: 0,
replicateTo: 'NONE',
}
const deployment: AppConfigDeployment = {
applicationId: 'app-1',
environmentId: 'env-1',
deploymentNumber: 1,
configurationProfileId: 'profile-1',
configurationVersion: '1',
deploymentStrategyId: 'strategy-1',
state: 'COMPLETE',
percentageComplete: 100,
startedAt: null,
completedAt: null,
description: null,
}
const app = appWithRoutes([mockAdapter('aws', {
service: 'configuration',
schema: awsAppConfigSchema,
listAppConfigEnvironments: async (applicationId) => {
calls.push(`list-environments:${applicationId}`)
return [environment]
},
createAppConfigEnvironment: async (applicationId, input) => {
calls.push(`create-environment:${applicationId}:${input.values.name}`)
return environment
},
deleteAppConfigEnvironment: async (applicationId, environmentId) => {
calls.push(`delete-environment:${applicationId}:${environmentId}`)
},
listAppConfigConfigurationProfiles: async (applicationId) => {
calls.push(`list-profiles:${applicationId}`)
return [profile]
},
createAppConfigConfigurationProfile: async (applicationId, input) => {
calls.push(`create-profile:${applicationId}:${input.values.name}`)
return profile
},
deleteAppConfigConfigurationProfile: async (applicationId, profileId) => {
calls.push(`delete-profile:${applicationId}:${profileId}`)
},
listAppConfigHostedConfigurationVersions: async (applicationId, profileId) => {
calls.push(`list-versions:${applicationId}:${profileId}`)
return [version]
},
getAppConfigHostedConfigurationVersion: async (applicationId, profileId, versionNumber) => {
calls.push(`get-version:${applicationId}:${profileId}:${versionNumber}`)
return version
},
createAppConfigHostedConfigurationVersion: async (applicationId, profileId, input) => {
calls.push(`create-version:${applicationId}:${profileId}:${input.values.contentType}`)
return version
},
deleteAppConfigHostedConfigurationVersion: async (applicationId, profileId, versionNumber) => {
calls.push(`delete-version:${applicationId}:${profileId}:${versionNumber}`)
},
listAppConfigDeploymentStrategies: async () => {
calls.push('list-strategies')
return [strategy]
},
createAppConfigDeploymentStrategy: async (input) => {
calls.push(`create-strategy:${input.values.name}`)
return strategy
},
deleteAppConfigDeploymentStrategy: async (strategyId) => {
calls.push(`delete-strategy:${strategyId}`)
},
startAppConfigDeployment: async (applicationId, environmentId, input) => {
calls.push(`start-deployment:${applicationId}:${environmentId}:${input.values.configurationVersion}`)
return deployment
},
getAppConfigDeployment: async (applicationId, environmentId, deploymentNumber) => {
calls.push(`get-deployment:${applicationId}:${environmentId}:${deploymentNumber}`)
return deployment
},
})])
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/environments')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/environments', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({name: 'dev'}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/environments/env-1', {method: 'DELETE'})).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({name: 'settings'}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles/profile-1', {method: 'DELETE'})).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles/profile-1/hosted-configuration-versions')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles/profile-1/hosted-configuration-versions', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({content: '{}', contentType: 'application/json'}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles/profile-1/hosted-configuration-versions/1')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/configuration-profiles/profile-1/hosted-configuration-versions/1', {method: 'DELETE'})).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/deployment-strategies')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/deployment-strategies', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({name: 'immediate'}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/configuration/deployment-strategies/strategy-1', {method: 'DELETE'})).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/environments/env-1/deployments', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({configurationVersion: '1'}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/configuration/resources/app-1/environments/env-1/deployments/1')).status).toBe(200)
expect(calls).toEqual([
'list-environments:app-1',
'create-environment:app-1:dev',
'delete-environment:app-1:env-1',
'list-profiles:app-1',
'create-profile:app-1:settings',
'delete-profile:app-1:profile-1',
'list-versions:app-1:profile-1',
'create-version:app-1:profile-1:application/json',
'get-version:app-1:profile-1:1',
'delete-version:app-1:profile-1:1',
'list-strategies',
'create-strategy:immediate',
'delete-strategy:strategy-1',
'start-deployment:app-1:env-1:1',
'get-deployment:app-1:env-1:1',
])
})
})
describe('per-service status', () => {
test('reports a reachable service with a latency measurement', async () => {
const res = await appWithRoutes().request('/api/clouds/aws/services/storage/status')
const body = await res.json()
expect(res.status).toBe(200)
expect(body).toMatchObject({
cloud: 'aws',
service: 'storage',
adapterRegistered: true,
runtime: 'reachable',
error: null,
errorCode: null,
})
expect(body.latencyMs).toBeGreaterThanOrEqual(0)
})
test('reports coming_soon for a service with no adapter', async () => {
const res = await appWithRoutes([mockAdapter('aws')]).request('/api/clouds/gcp/services/storage/status')
const body = await res.json()
expect(body).toMatchObject({runtime: 'coming_soon', adapterRegistered: false, latencyMs: null})
})
test('distinguishes a runtime that does not implement a service from one that is down', async () => {
// This is the whole point of errorCode: floci-az serves blob storage but
// answers 501 for functions, and the UI must not call that "offline".
const app = appWithRoutes([
mockAdapter('azure', {
service: 'serverless',
list: async () => {
throw new NotImplementedByRuntimeError('HTTP 501 /functions')
},
}),
mockAdapter('gcp', {
list: async () => {
throw new RuntimeUnavailableError('Cannot reach Floci-GCP')
},
}),
])
const notImplemented = await (await app.request('/api/clouds/azure/services/serverless/status')).json()
expect(notImplemented.runtime).toBe('unavailable')
expect(notImplemented.errorCode).toBe('operation_not_implemented')
const down = await (await app.request('/api/clouds/gcp/services/storage/status')).json()
expect(down.errorCode).toBe('runtime_unavailable')
})
test('prefers an adapter health() override to list()', async () => {
let listCalls = 0
let healthCalls = 0
const app = appWithRoutes([
mockAdapter('aws', {
list: async () => {
listCalls += 1
return []
},
health: async () => {
healthCalls += 1
},
}),
])
await app.request('/api/clouds/aws/services/storage/status')
expect(healthCalls).toBe(1)
expect(listCalls).toBe(0)
})
test('omits per-service detail from the cloud status by default', async () => {
const body = await (await appWithRoutes().request('/api/clouds/aws/status')).json()
expect(body.services).toBeUndefined()
})
test('includes per-service detail only when asked', async () => {
const body = await (await appWithRoutes().request('/api/clouds/aws/status?services=all')).json()
expect(Array.isArray(body.services)).toBe(true)
expect(body.services[0]).toMatchObject({cloud: 'aws', service: 'storage'})
})
test('caches probes so a polling sidebar does not fan out per request', async () => {
let probes = 0
const app = appWithRoutes([
mockAdapter('aws', {
list: async () => {
probes += 1
return []
},
}),
])
for (let i = 0; i < 5; i += 1) {
await app.request('/api/clouds/aws/services/storage/status')
}
expect(probes).toBe(1)
})
test('rejects an unknown service slug', async () => {
const res = await appWithRoutes().request('/api/clouds/aws/services/queue/status')
expect(res.status).toBe(404)
})
test('routes nested EKS nodegroup and Fargate actions through the cloud adapter', async () => {
const calls: string[] = []
const app = appWithRoutes([mockAdapter('aws', {
service: 'k8s',
schema: awsEksSchema,
listKubernetesNodegroups: async (clusterId) => [{
id: 'workers', name: 'workers', clusterId, arn: null, status: 'ACTIVE', version: null,
releaseVersion: null, createdAt: null, modifiedAt: null, capacityType: null,
instanceTypes: ['t3.medium'], subnets: ['subnet-a'], nodeRole: null,
scalingConfig: null, labels: {}, tags: {},
}],
createKubernetesNodegroup: async (clusterId, input) => {
calls.push(`create-nodegroup:${clusterId}:${input.name}`)
return {
id: input.name, name: input.name, clusterId, arn: null, status: null, version: null,
releaseVersion: null, createdAt: null, modifiedAt: null, capacityType: null,
instanceTypes: [], subnets: input.subnets, nodeRole: input.nodeRole,
scalingConfig: null, labels: {}, tags: {},
}
},
deleteKubernetesNodegroup: async (clusterId, nodegroupId) => {
calls.push(`delete-nodegroup:${clusterId}:${nodegroupId}`)
},
listKubernetesFargateProfiles: async (clusterId) => [{
id: 'default', name: 'default', clusterId, arn: null, status: 'ACTIVE', createdAt: null,
podExecutionRoleArn: null, subnets: [], selectors: [], tags: {},
}],
createKubernetesFargateProfile: async (clusterId, input) => {
calls.push(`create-fargate:${clusterId}:${input.name}`)
return {
id: input.name, name: input.name, clusterId, arn: null, status: null, createdAt: null,
podExecutionRoleArn: input.podExecutionRoleArn, subnets: input.subnets ?? [], selectors: [], tags: {},
}
},
deleteKubernetesFargateProfile: async (clusterId, profileId) => {
calls.push(`delete-fargate:${clusterId}:${profileId}`)
},
})])
expect((await app.request('/api/clouds/aws/services/k8s/resources/demo/nodegroups')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/k8s/resources/demo/nodegroups', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({name: 'workers', nodeRole: 'role', subnets: ['subnet-a']}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/k8s/resources/demo/nodegroups/workers', {method: 'DELETE'})).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/k8s/resources/demo/fargate-profiles')).status).toBe(200)
expect((await app.request('/api/clouds/aws/services/k8s/resources/demo/fargate-profiles', {
method: 'POST', headers: {'content-type': 'application/json'},
body: JSON.stringify({name: 'default', podExecutionRoleArn: 'role', selectors: [{namespace: 'default'}]}),
})).status).toBe(201)
expect((await app.request('/api/clouds/aws/services/k8s/resources/demo/fargate-profiles/default', {method: 'DELETE'})).status).toBe(200)
expect(calls).toEqual([
'create-nodegroup:demo:workers',
'delete-nodegroup:demo:workers',
'create-fargate:demo:default',
'delete-fargate:demo:default',
])
})
test('routes PATCH to the adapter update method', async () => {
let updatedId = ''
let updatedValues: Record<string, unknown> = {}
const app = appWithRoutes([
mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
update: async (id, input) => {
updatedId = id
updatedValues = input.values
return {
id,
name: id,
cloud: 'aws',
service: 'database',
type: 'db-instance',
region: 'us-east-1',
createdAt: null,
metadata: input.values,
}
},
}),
])
const res = await app.request('/api/clouds/aws/services/database/resources/orders-db', {
method: 'PATCH',
headers: {'content-type': 'application/json'},
body: JSON.stringify({autoMinorVersionUpgrade: 'true'}),
})
expect(res.status).toBe(200)
expect(updatedId).toBe('orders-db')
expect(updatedValues).toEqual({autoMinorVersionUpgrade: 'true'})
})
test('returns 501 when the adapter does not implement update', async () => {
const app = appWithRoutes([mockAdapter('aws', {service: 'database', schema: awsDatabaseSchema})])
const res = await app.request('/api/clouds/aws/services/database/resources/orders-db', {
method: 'PATCH',
headers: {'content-type': 'application/json'},
body: JSON.stringify({autoMinorVersionUpgrade: 'true'}),
})
expect(res.status).toBe(501)
})
test('maps adapter ValidationError on PATCH to 400', async () => {
const app = appWithRoutes([
mockAdapter('aws', {
service: 'database',
schema: awsDatabaseSchema,
update: async () => {
throw new ValidationError('Invalid password')
},
}),
])
const res = await app.request('/api/clouds/aws/services/database/resources/orders-db', {
method: 'PATCH',
headers: {'content-type': 'application/json'},
body: JSON.stringify({masterUserPassword: 'short'}),
})
expect(res.status).toBe(400)
const body = await res.json()
expect(body.message).toBe('Invalid password')
})
})
const stubCollections: CollectionPage<ChildCollection> = {
items: [{id: 'stream-a', name: 'stream-a', parentId: 'group-1', createdAt: null, metadata: {}}],
nextCursor: 'cursor-2',
}
const stubItems: CollectionPage<ChildItem> = {
items: [{id: 'event-1', collectionId: 'stream-a', timestamp: null, body: {message: 'hello'}, metadata: {}}],
nextCursor: null,
}
const documentsStub: DocumentStoreAdapter = {
listCollections: async () => stubCollections,
createCollection: async (resourceId, input) => ({
id: String(input.values.name),
name: String(input.values.name),
parentId: resourceId,
createdAt: null,
metadata: {},
}),
deleteCollection: async () => {},
listItems: async () => stubItems,
}
const itemsStub: ItemStoreAdapter = {
listItems: async () => stubItems,
}
describe('child collection routes', () => {
test('lists collections and returns the cursor', async () => {
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/collections')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({
items: [{id: 'stream-a', name: 'stream-a', parentId: 'group-1', createdAt: null, metadata: {}}],
nextCursor: 'cursor-2',
})
})
test('creates a collection', async () => {
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/collections', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({name: 'stream-b'}),
})
expect(res.status).toBe(201)
expect(await res.json()).toMatchObject({id: 'stream-b', parentId: 'group-1'})
})
test('lists items under a collection', async () => {
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/collections/stream-a/items')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({items: stubItems.items, nextCursor: null})
})
test('lists flat items', async () => {
const app = appWithRoutes([mockAdapter('aws', {items: itemsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/table-1/items')
expect(res.status).toBe(200)
expect(await res.json()).toEqual({items: stubItems.items, nextCursor: null})
})
test('passes a structured flat-item query and pagination through the existing query route', async () => {
let seen: unknown
const app = appWithRoutes([mockAdapter('aws', {items: {
...itemsStub,
queryItems: async (resourceId, query, page) => {
seen = {resourceId, query, page}
return {...stubItems, count: 1, scannedCount: 4, consumedCapacity: 0.5}
},
}})])
const res = await app.request('/api/clouds/aws/services/storage/resources/table-1/query?cursor=opaque&limit=25', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({query: {operation: 'scan', filter: {attribute: 'status', operator: 'EQ', type: 'S', value: 'OPEN'}}}),
})
expect(res.status).toBe(200)
expect(seen).toEqual({
resourceId: 'table-1',
query: {operation: 'scan', filter: {attribute: 'status', operator: 'EQ', type: 'S', value: 'OPEN'}},
page: {cursor: 'opaque', limit: 25},
})
expect(await res.json()).toMatchObject({count: 1, scannedCount: 4, consumedCapacity: 0.5})
})
// A documents-only adapter has no flat shape, and vice versa.
test('501s when the adapter has the other shape', async () => {
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/items')
expect(res.status).toBe(501)
})
test('501s when the adapter implements no child store', async () => {
const app = appWithRoutes([mockAdapter('aws')])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/collections')
expect(res.status).toBe(501)
})
test('501s for a write the store does not implement', async () => {
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/collections/stream-a/items', {
method: 'POST',
headers: {'content-type': 'application/json'},
body: JSON.stringify({message: 'nope'}),
})
expect(res.status).toBe(501)
})
test('rejects an out-of-range limit', async () => {
const app = appWithRoutes([mockAdapter('aws', {documents: documentsStub})])
const res = await app.request('/api/clouds/aws/services/storage/resources/group-1/collections?limit=5000')
expect(res.status).toBe(400)
})
test('passes cursor and limit through to the adapter', async () => {
let seen: unknown
const app = appWithRoutes([
mockAdapter('aws', {
documents: {
...documentsStub,
listCollections: async (_resourceId, page) => {
seen = page
return stubCollections
},
},
}),
])
await app.request('/api/clouds/aws/services/storage/resources/group-1/collections?cursor=abc&limit=25')
expect(seen).toEqual({cursor: 'abc', limit: 25})
})
// Registration-order regression: the literal `nosql` segment must keep
// beating the new `:service` param, or the Cosmos panel silently breaks.
test('the existing Cosmos container routes still resolve', async () => {
const app = appWithRoutes([
mockAdapter('azure', {
service: 'nosql',
schema: azureNoSqlSchema,
listCosmosContainers: async (): Promise<CosmosContainer[]> => [{
id: 'items',
name: 'items',
databaseId: 'appdb',
partitionKeyPath: '/pk',
createdAt: null,
metadata: {},
}],
}),
])
const res = await app.request('/api/clouds/azure/services/nosql/resources/appdb/containers')
expect(res.status).toBe(200)
expect(await res.json()).toMatchObject([{id: 'items', databaseId: 'appdb'}])
})
})