Files
floci-ui/packages/api/src/adapter-aws/AwsCloudFormationAdapter.test.ts
T
2026-09-30 17:43:42 -04:00

262 lines
11 KiB
TypeScript

import {describe, expect, test} from 'bun:test'
import {
CreateStackCommand,
CreateChangeSetCommand,
DescribeChangeSetCommand,
DeleteStackCommand,
DescribeStackEventsCommand,
DescribeStacksCommand,
GetTemplateCommand,
ListStackResourcesCommand,
ListStacksCommand,
UpdateStackCommand,
type CloudFormationClient,
} from '@aws-sdk/client-cloudformation'
import {AwsCloudFormationAdapter} from './AwsCloudFormationAdapter'
function fakeClient(handler: (command: unknown) => unknown): CloudFormationClient {
return {
send: async (command: unknown) => handler(command),
} as unknown as CloudFormationClient
}
const ordersStackSummary = {
StackId: 'arn:aws:cloudformation:us-east-1:000000000000:stack/orders-stack/abc123',
StackName: 'orders-stack',
CreationTime: new Date('2026-01-02T03:04:05.000Z'),
StackStatus: 'CREATE_COMPLETE',
}
const ordersStack = {
StackId: 'arn:aws:cloudformation:us-east-1:000000000000:stack/orders-stack/abc123',
StackName: 'orders-stack',
CreationTime: new Date('2026-01-02T03:04:05.000Z'),
LastUpdatedTime: new Date('2026-01-02T03:04:06.000Z'),
StackStatus: 'CREATE_COMPLETE',
Capabilities: [],
Outputs: [{OutputKey: 'BucketName', OutputValue: 'orders-bucket', Description: 'The bucket name'}],
Tags: [{Key: 'env', Value: 'dev'}],
EnableTerminationProtection: false,
}
const bucketResource = {
LogicalResourceId: 'OrdersBucket',
PhysicalResourceId: 'orders-bucket',
ResourceType: 'AWS::S3::Bucket',
ResourceStatus: 'CREATE_COMPLETE',
}
describe('AwsCloudFormationAdapter', () => {
test('lists every page and maps stacks to normalized resources', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
if (!(command instanceof ListStacksCommand)) throw new Error('Unexpected command')
if (!command.input.NextToken) return {StackSummaries: [ordersStackSummary], NextToken: 'next'}
return {StackSummaries: [{...ordersStackSummary, StackName: 'accounts-stack'}]}
}))
const result = await adapter.list()
expect(result).toHaveLength(2)
expect(result[0]).toMatchObject({
id: 'orders-stack',
name: 'orders-stack',
cloud: 'aws',
service: 'iac',
type: 'stack',
region: null,
createdAt: '2026-01-02T03:04:05.000Z',
status: 'CREATE_COMPLETE',
})
expect(result[0].metadata).toMatchObject({
stackId: ordersStackSummary.StackId,
})
})
test('filters stacks by search term', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient(() => ({
StackSummaries: [ordersStackSummary, {...ordersStackSummary, StackName: 'accounts-stack'}],
})))
const result = await adapter.list({search: 'ORDERS'})
expect(result.map((resource) => resource.name)).toEqual(['orders-stack'])
})
test('gets and maps one stack, including its resources', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
if (command instanceof DescribeStacksCommand) return {Stacks: [ordersStack]}
if (command instanceof ListStackResourcesCommand) return {StackResourceSummaries: [bucketResource]}
if (command instanceof DescribeStackEventsCommand) return {StackEvents: [{LogicalResourceId: 'OrdersBucket', ResourceStatus: 'CREATE_COMPLETE', Timestamp: new Date('2026-01-02T03:04:07.000Z')}]}
if (command instanceof GetTemplateCommand) return {TemplateBody: '{"Resources":{}}'}
throw new Error('Unexpected command')
}))
const result = await adapter.get('orders-stack')
expect(result?.id).toBe('orders-stack')
expect(result?.status).toBe('CREATE_COMPLETE')
expect(result?.metadata.outputs).toEqual([{key: 'BucketName', value: 'orders-bucket', description: 'The bucket name'}])
expect(result?.metadata.tags).toEqual([{key: 'env', value: 'dev'}])
expect(result?.metadata.resources).toEqual([{
logicalId: 'OrdersBucket',
physicalId: 'orders-bucket',
resourceType: 'AWS::S3::Bucket',
status: 'CREATE_COMPLETE',
}])
expect(result?.metadata.templateBody).toBe('{"Resources":{}}')
})
test('paginates ListStackResources and combines resources from every page', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
if (command instanceof DescribeStacksCommand) return {Stacks: [ordersStack]}
if (command instanceof ListStackResourcesCommand) {
if (!command.input.NextToken) {
return {StackResourceSummaries: [bucketResource], NextToken: 'next-page'}
}
return {StackResourceSummaries: [{...bucketResource, LogicalResourceId: 'OrdersQueue', PhysicalResourceId: 'orders-queue', ResourceType: 'AWS::SQS::Queue'}]}
}
if (command instanceof DescribeStackEventsCommand) return {StackEvents: []}
if (command instanceof GetTemplateCommand) return {TemplateBody: '{}'}
throw new Error('Unexpected command')
}))
const result = await adapter.get('orders-stack')
expect(result?.metadata.resources).toEqual([
{
logicalId: 'OrdersBucket',
physicalId: 'orders-bucket',
resourceType: 'AWS::S3::Bucket',
status: 'CREATE_COMPLETE',
},
{
logicalId: 'OrdersQueue',
physicalId: 'orders-queue',
resourceType: 'AWS::SQS::Queue',
status: 'CREATE_COMPLETE',
},
])
})
test('returns null when CloudFormation reports a missing stack', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient(() => {
throw Object.assign(new Error('Stack with id missing-stack does not exist'), {name: 'ValidationError'})
}))
await expect(adapter.get('missing-stack')).resolves.toBeNull()
})
test('creates a stack from a stack name and template body', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
expect(command).toBeInstanceOf(CreateStackCommand)
expect((command as CreateStackCommand).input).toEqual({
StackName: 'orders-stack',
TemplateBody: '{"Resources":{}}',
})
return {StackId: ordersStack.StackId}
}))
const result = await adapter.create({values: {stackName: 'orders-stack', templateBody: '{"Resources":{}}'}})
expect(result.id).toBe('orders-stack')
expect(result.status).toBe('CREATE_IN_PROGRESS')
expect(result.metadata.stackId).toBe(ordersStack.StackId)
})
test('passes parameters and capabilities to CloudFormation', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
if (command instanceof CreateStackCommand) {
expect(command.input).toMatchObject({
Parameters: [{ParameterKey: 'Environment', ParameterValue: 'test'}],
Capabilities: ['CAPABILITY_NAMED_IAM'],
})
return {StackId: ordersStack.StackId}
}
if (command instanceof CreateChangeSetCommand) {
expect(command.input).toMatchObject({
Parameters: [{ParameterKey: 'Environment', ParameterValue: 'test'}],
Capabilities: ['CAPABILITY_NAMED_IAM'],
})
return {Id: 'change-set-id'}
}
if (command instanceof DescribeChangeSetCommand) return {ChangeSetName: 'planned', Status: 'CREATE_COMPLETE', ExecutionStatus: 'AVAILABLE'}
throw new Error('Unexpected command')
}))
await adapter.create({values: {stackName: 'orders-stack', templateBody: '{}', parameters: '{"Environment":"test"}', capabilities: 'CAPABILITY_NAMED_IAM'}})
await adapter.createCloudFormationChangeSet('orders-stack', {name: 'planned', templateBody: '{}', parameters: {Environment: 'test'}, capabilities: ['CAPABILITY_NAMED_IAM']})
})
test('creates a stack from an S3 template URL', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
expect(command).toBeInstanceOf(CreateStackCommand)
expect((command as CreateStackCommand).input).toEqual({
StackName: 'orders-stack',
TemplateURL: 'https://templates.s3.amazonaws.com/orders.yaml',
})
return {StackId: ordersStack.StackId}
}))
await expect(adapter.create({values: {stackName: 'orders-stack', templateUrl: 'https://templates.s3.amazonaws.com/orders.yaml'}})).resolves.toMatchObject({id: 'orders-stack'})
})
test('rejects a missing stack name before calling CloudFormation', async () => {
let called = false
const adapter = new AwsCloudFormationAdapter(fakeClient(() => {
called = true
return {}
}))
await expect(adapter.create({values: {templateBody: '{}'}})).rejects.toThrow('stackName is required')
expect(called).toBeFalse()
})
test('rejects missing template input before calling CloudFormation', async () => {
let called = false
const adapter = new AwsCloudFormationAdapter(fakeClient(() => {
called = true
return {}
}))
await expect(adapter.create({values: {stackName: 'orders-stack'}})).rejects.toThrow('templateBody or templateUrl is required')
expect(called).toBeFalse()
})
test('deletes the requested stack', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
expect(command).toBeInstanceOf(DeleteStackCommand)
expect((command as DeleteStackCommand).input.StackName).toBe('orders-stack')
return {}
}))
await adapter.delete('orders-stack')
})
test('updates a stack template then reads back the stack workspace', async () => {
const adapter = new AwsCloudFormationAdapter(fakeClient((command) => {
if (command instanceof UpdateStackCommand) {
expect(command.input).toEqual({StackName: 'orders-stack', TemplateBody: '{"Resources":{"Queue":{"Type":"AWS::SQS::Queue"}}}'})
return {StackId: ordersStack.StackId}
}
if (command instanceof DescribeStacksCommand) return {Stacks: [ordersStack]}
if (command instanceof ListStackResourcesCommand) return {StackResourceSummaries: []}
if (command instanceof DescribeStackEventsCommand) return {StackEvents: []}
if (command instanceof GetTemplateCommand) return {TemplateBody: '{}'}
throw new Error('Unexpected command')
}))
await expect(adapter.update('orders-stack', {values: {templateBody: '{"Resources":{"Queue":{"Type":"AWS::SQS::Queue"}}}'}})).resolves.toMatchObject({id: 'orders-stack'})
})
test('returns the AWS CloudFormation schema', () => {
const adapter = new AwsCloudFormationAdapter(fakeClient(() => ({})))
expect(adapter.schema()).toMatchObject({
cloud: 'aws',
service: 'iac',
displayName: 'CloudFormation',
actions: ['list', 'create', 'update', 'delete', 'inspect'],
})
})
})