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 { return { cloud, service: 'storage', schema: cloud === 'aws' ? awsStorageSchema : cloud === 'gcp' ? gcpStorageSchema : azureStorageSchema, list: async () => [], get: async () => null, create: async (_input: CreateResourceInput): Promise => ({ 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 { 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 = 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 => [{ 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}> = [] 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 => ({ 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 => ({ 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): Promise => ({ 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 => ({ 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 => { 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 => { 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 => ({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 = {} 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 | 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 = {} 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 = { items: [{id: 'stream-a', name: 'stream-a', parentId: 'group-1', createdAt: null, metadata: {}}], nextCursor: 'cursor-2', } const stubItems: CollectionPage = { 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 => [{ 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'}]) }) })