Files
floci-ui/packages/api/src/adapter-aws/AwsTargetGroupAdapter.test.ts
T
2026-09-26 16:14:24 -04:00

203 lines
8.5 KiB
TypeScript

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<string, unknown>
function stubElb(handler: (command: object) => SendResult | Promise<SendResult>) {
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<SendResult> = () => ({})) {
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}]})
})
})