203 lines
8.5 KiB
TypeScript
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}]})
|
|
})
|
|
})
|