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'], }) }) })