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

1970 lines
87 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 {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('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'})
})
})
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'}])
})
})