import {describe, expect, test} from 'bun:test' import { CreateTargetGroupCommand, DeregisterTargetsCommand, DescribeTargetHealthCommand, DeleteTargetGroupCommand, DescribeTargetGroupsCommand, RegisterTargetsCommand, type ElasticLoadBalancingV2Client, } from '@aws-sdk/client-elastic-load-balancing-v2' import {DescribeInstancesCommand, type EC2Client} from '@aws-sdk/client-ec2' import {AwsTargetGroupAdapter} from './AwsTargetGroupAdapter' import {ValidationError} from '../cloud-spi/errors' type SendResult = Record function stubElb(handler: (command: object) => SendResult | Promise) { const sent: object[] = [] const client = { async send(command: object) { sent.push(command) return handler(command) }, } as unknown as ElasticLoadBalancingV2Client return {client, sent} } function stubEc2(handler: (command: object) => SendResult | Promise = () => ({})) { return {async send(command: object) { return handler(command) }} as unknown as EC2Client } const ARN = 'arn:aws:elasticloadbalancing:us-east-1:000000000000:targetgroup/api/77aa388f69144785' const targetGroup = { TargetGroupArn: ARN, TargetGroupName: 'api', Protocol: 'HTTP', ProtocolVersion: 'HTTP1', Port: 8080, TargetType: 'ip', VpcId: 'vpc-default', LoadBalancerArns: ['arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/web/123'], HealthCheckProtocol: 'HTTP', HealthCheckPath: '/health', } function runtimeStub() { return stubElb((command) => { if (command instanceof CreateTargetGroupCommand) return {TargetGroups: [targetGroup]} if (command instanceof DescribeTargetGroupsCommand) return {TargetGroups: [targetGroup]} return {} }) } describe('AwsTargetGroupAdapter', () => { test('identifies itself as the AWS target groups adapter', () => { const adapter = new AwsTargetGroupAdapter(runtimeStub().client, stubEc2()) expect(adapter.cloud).toBe('aws') expect(adapter.service).toBe('targetgroups') expect(adapter.schema().displayName).toBe('AWS ELB Target Groups') }) test('lists target groups with their table metadata', async () => { const {client, sent} = runtimeStub() const [resource] = await new AwsTargetGroupAdapter(client, stubEc2()).list() expect(sent[0]).toBeInstanceOf(DescribeTargetGroupsCommand) expect(resource).toMatchObject({ id: ARN, name: 'api', cloud: 'aws', service: 'targetgroups', type: 'target-group', region: 'us-east-1', createdAt: null, }) expect(resource?.metadata).toMatchObject({ protocol: 'HTTP', port: 8080, targetType: 'ip', vpcId: 'vpc-default', loadBalancerArns: ['arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/web/123'], }) }) test('follows a non-empty pagination marker and stops at an empty marker', async () => { let call = 0 const {client, sent} = stubElb((command) => { if (!(command instanceof DescribeTargetGroupsCommand)) return {} call += 1 if (call === 1) return {TargetGroups: [targetGroup], NextMarker: 'page-2'} return {TargetGroups: [{...targetGroup, TargetGroupName: 'worker', TargetGroupArn: `${ARN}-2`}], NextMarker: ''} }) const resources = await new AwsTargetGroupAdapter(client, stubEc2()).list() expect(resources.map((resource) => resource.name)).toEqual(['api', 'worker']) expect((sent[1] as DescribeTargetGroupsCommand).input.Marker).toBe('page-2') }) test('filters by name and ARN', async () => { const adapter = new AwsTargetGroupAdapter(runtimeStub().client, stubEc2()) await expect(adapter.list({search: 'ap'})).resolves.toHaveLength(1) await expect(adapter.list({search: 'targetgroup/api'})).resolves.toHaveLength(1) await expect(adapter.list({search: 'nope'})).resolves.toHaveLength(0) }) test('inspects by ARN and returns null for a missing target group', async () => { const {client, sent} = runtimeStub() const adapter = new AwsTargetGroupAdapter(client, stubEc2()) await expect(adapter.get(ARN)).resolves.toMatchObject({id: ARN}) expect((sent[0] as DescribeTargetGroupsCommand).input.TargetGroupArns).toEqual([ARN]) const missing = new AwsTargetGroupAdapter(stubElb(() => { throw Object.assign(new Error('One or more target groups not found.'), {name: 'TargetGroupNotFound', $metadata: {httpStatusCode: 400}}) }).client, stubEc2()) await expect(missing.get(ARN)).resolves.toBeNull() }) test('creates with required and supported optional SDK fields', async () => { const {client, sent} = runtimeStub() const resource = await new AwsTargetGroupAdapter(client, stubEc2()).create({ values: { name: 'api', protocol: 'HTTP', port: '8080', targetType: 'ip', vpcId: 'vpc-default', healthCheckProtocol: 'HTTP', healthCheckPath: '/health', }, }) const command = sent[0] as CreateTargetGroupCommand expect(command.input).toMatchObject({ Name: 'api', Protocol: 'HTTP', Port: 8080, TargetType: 'ip', VpcId: 'vpc-default', HealthCheckProtocol: 'HTTP', HealthCheckPath: '/health', }) expect(resource.id).toBe(ARN) }) test('requires valid name, protocol, and port', async () => { const adapter = new AwsTargetGroupAdapter(runtimeStub().client, stubEc2()) await expect(adapter.create({values: {}})).rejects.toThrow(new ValidationError('name is required')) await expect(adapter.create({values: {name: 'api'}})).rejects.toThrow(new ValidationError('protocol is required')) await expect(adapter.create({values: {name: 'api', protocol: 'HTTP'}})).rejects.toThrow( new ValidationError('port is required'), ) await expect(adapter.create({values: {name: '-api', protocol: 'HTTP', port: 80}})).rejects.toThrow(ValidationError) await expect(adapter.create({values: {name: 'api', protocol: 'HTTP', port: '70000'}})).rejects.toThrow( new ValidationError('port must be an integer from 1 to 65535'), ) }) test('deletes by target group ARN', async () => { const {client, sent} = runtimeStub() await new AwsTargetGroupAdapter(client, stubEc2()).delete(ARN) expect((sent[0] as DeleteTargetGroupCommand).input.TargetGroupArn).toBe(ARN) }) test('lists running account instances and target health through the AWS SDK', async () => { const {client, sent} = stubElb((command) => { if (command instanceof DescribeTargetHealthCommand) return { TargetHealthDescriptions: [{ Target: {Id: 'i-123', Port: 8080}, TargetHealth: {State: 'healthy', Reason: 'Success', Description: 'Health checks passed'}, }], } return {} }) const adapter = new AwsTargetGroupAdapter(client, stubEc2((command) => { if (command instanceof DescribeInstancesCommand) return { Reservations: [{Instances: [{InstanceId: 'i-123', PrivateIpAddress: '10.0.0.5', Tags: [{Key: 'Name', Value: 'api'}]}]}], } return {} })) await expect(adapter.dependencies()).resolves.toEqual({instances: [{id: 'i-123', name: 'api', privateIp: '10.0.0.5'}]}) await expect(adapter.items.listItems(ARN)).resolves.toMatchObject({items: [{id: 'i-123:8080', body: {state: 'healthy', reason: 'Success'}}]}) expect((sent[0] as DescribeTargetHealthCommand).input.TargetGroupArn).toBe(ARN) }) test('registers and deregisters targets with the selected port', async () => { const {client, sent} = stubElb(() => ({})) const store = new AwsTargetGroupAdapter(client, stubEc2()).items await store.putItem?.(ARN, {id: 'i-123', port: '8080'}) await store.deleteItem?.(ARN, 'i-123:8080') expect((sent[0] as RegisterTargetsCommand).input).toEqual({TargetGroupArn: ARN, Targets: [{Id: 'i-123', Port: 8080}]}) expect((sent[1] as DeregisterTargetsCommand).input).toEqual({TargetGroupArn: ARN, Targets: [{Id: 'i-123', Port: 8080}]}) }) })