feat: fully functioning ALB
This commit is contained in:
@@ -1,10 +1,15 @@
|
||||
import {describe, expect, test} from 'bun:test'
|
||||
import {
|
||||
CreateListenerCommand,
|
||||
CreateLoadBalancerCommand,
|
||||
DeleteListenerCommand,
|
||||
DeleteLoadBalancerCommand,
|
||||
DescribeListenersCommand,
|
||||
DescribeLoadBalancersCommand,
|
||||
SetSecurityGroupsCommand,
|
||||
type ElasticLoadBalancingV2Client,
|
||||
} from '@aws-sdk/client-elastic-load-balancing-v2'
|
||||
import {DescribeSecurityGroupsCommand, DescribeSubnetsCommand, DescribeVpcsCommand, type EC2Client} from '@aws-sdk/client-ec2'
|
||||
import {AwsLoadBalancingAdapter} from './AwsLoadBalancingAdapter'
|
||||
import {ValidationError} from '../cloud-spi/errors'
|
||||
|
||||
@@ -48,11 +53,26 @@ function runtimeStub() {
|
||||
})
|
||||
}
|
||||
|
||||
const validValues = {name: 'web', subnets: 'subnet-default-b, subnet-default-c'}
|
||||
function stubEc2() {
|
||||
return {
|
||||
async send(command: object) {
|
||||
if (command instanceof DescribeSubnetsCommand) return {Subnets: [
|
||||
{SubnetId: 'subnet-default-b', VpcId: 'vpc-default', AvailabilityZone: 'us-east-1b', CidrBlock: '10.0.1.0/24'},
|
||||
{SubnetId: 'subnet-default-c', VpcId: 'vpc-default', AvailabilityZone: 'us-east-1c', CidrBlock: '10.0.2.0/24'},
|
||||
]}
|
||||
if (command instanceof DescribeVpcsCommand) return {Vpcs: [{VpcId: 'vpc-default', CidrBlock: '10.0.0.0/16'}]}
|
||||
if (command instanceof DescribeSecurityGroupsCommand) return {SecurityGroups: [{GroupId: 'sg-1', VpcId: 'vpc-default', GroupName: 'web'}]}
|
||||
return {}
|
||||
},
|
||||
} as unknown as EC2Client
|
||||
}
|
||||
|
||||
const validValues = {name: 'web', subnets: 'subnet-default-b, subnet-default-c', securityGroups: 'sg-1'}
|
||||
const LISTENER_ARN = 'arn:aws:elasticloadbalancing:us-east-1:000000000000:listener/app/web/77aa388f69144785/aabbccddeeff0011'
|
||||
|
||||
describe('AwsLoadBalancingAdapter', () => {
|
||||
test('identifies itself as the AWS load balancing adapter', () => {
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client)
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client, stubEc2())
|
||||
|
||||
expect(adapter.cloud).toBe('aws')
|
||||
expect(adapter.service).toBe('loadbalancing')
|
||||
@@ -61,7 +81,7 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
|
||||
test('lists load balancers and reads the nested state code', async () => {
|
||||
const {client, sent} = runtimeStub()
|
||||
const [resource] = await new AwsLoadBalancingAdapter(client).list()
|
||||
const [resource] = await new AwsLoadBalancingAdapter(client, stubEc2()).list()
|
||||
|
||||
expect(sent[0]).toBeInstanceOf(DescribeLoadBalancersCommand)
|
||||
expect(resource).toMatchObject({
|
||||
@@ -96,7 +116,7 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
return {LoadBalancers: [{...balancer, LoadBalancerName: 'api', LoadBalancerArn: `${ARN}-2`}]}
|
||||
})
|
||||
|
||||
const resources = await new AwsLoadBalancingAdapter(client).list()
|
||||
const resources = await new AwsLoadBalancingAdapter(client, stubEc2()).list()
|
||||
|
||||
expect(resources.map((r) => r.name)).toEqual(['web', 'api'])
|
||||
expect((sent[1] as DescribeLoadBalancersCommand).input.Marker).toBe('page-2')
|
||||
@@ -112,12 +132,12 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
return {LoadBalancers: [balancer], NextMarker: ''}
|
||||
})
|
||||
|
||||
await expect(new AwsLoadBalancingAdapter(client).list()).resolves.toHaveLength(1)
|
||||
await expect(new AwsLoadBalancingAdapter(client, stubEc2()).list()).resolves.toHaveLength(1)
|
||||
expect(call).toBe(1)
|
||||
})
|
||||
|
||||
test('filters by name and ARN', async () => {
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client)
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client, stubEc2())
|
||||
|
||||
await expect(adapter.list({search: 'we'})).resolves.toHaveLength(1)
|
||||
await expect(adapter.list({search: 'loadbalancer/app'})).resolves.toHaveLength(1)
|
||||
@@ -126,7 +146,7 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
|
||||
test('inspects a load balancer by ARN', async () => {
|
||||
const {client, sent} = runtimeStub()
|
||||
const resource = await new AwsLoadBalancingAdapter(client).get(ARN)
|
||||
const resource = await new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN)
|
||||
|
||||
expect((sent[0] as DescribeLoadBalancersCommand).input.LoadBalancerArns).toEqual([ARN])
|
||||
expect(resource?.id).toBe(ARN)
|
||||
@@ -141,7 +161,7 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
$metadata: {httpStatusCode: 400},
|
||||
})
|
||||
})
|
||||
await expect(new AwsLoadBalancingAdapter(client).get(ARN)).resolves.toBeNull()
|
||||
await expect(new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('also returns null for the SDK-modelled exception name', async () => {
|
||||
@@ -154,12 +174,12 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
$metadata: {httpStatusCode: 400},
|
||||
})
|
||||
})
|
||||
await expect(new AwsLoadBalancingAdapter(client).get(ARN)).resolves.toBeNull()
|
||||
await expect(new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('returns null when the runtime answers with no load balancers', async () => {
|
||||
const {client} = stubElb(() => ({LoadBalancers: []}))
|
||||
await expect(new AwsLoadBalancingAdapter(client).get(ARN)).resolves.toBeNull()
|
||||
await expect(new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN)).resolves.toBeNull()
|
||||
})
|
||||
|
||||
test('rethrows a failure that is not a missing load balancer', async () => {
|
||||
@@ -169,17 +189,18 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
$metadata: {httpStatusCode: 403},
|
||||
})
|
||||
})
|
||||
await expect(new AwsLoadBalancingAdapter(client).get(ARN)).rejects.toThrow('AccessDenied')
|
||||
await expect(new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN)).rejects.toThrow('AccessDenied')
|
||||
})
|
||||
|
||||
test('creates a load balancer, splitting the comma separated subnets', async () => {
|
||||
const {client, sent} = runtimeStub()
|
||||
const resource = await new AwsLoadBalancingAdapter(client).create({values: validValues})
|
||||
const resource = await new AwsLoadBalancingAdapter(client, stubEc2()).create({values: validValues})
|
||||
|
||||
const command = sent[0] as CreateLoadBalancerCommand
|
||||
expect(command).toBeInstanceOf(CreateLoadBalancerCommand)
|
||||
expect(command.input.Name).toBe('web')
|
||||
expect(command.input.Subnets).toEqual(['subnet-default-b', 'subnet-default-c'])
|
||||
expect(command.input.SecurityGroups).toEqual(['sg-1'])
|
||||
expect(command.input.Type).toBe('application')
|
||||
expect(command.input.Scheme).toBe('internet-facing')
|
||||
expect(resource.id).toBe(ARN)
|
||||
@@ -188,7 +209,7 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
test('requires at least two subnet ids', async () => {
|
||||
// Only the count is enforced. The AZ requirement is ELB's, and checking it
|
||||
// would mean calling EC2 from this adapter, so the message must not claim it.
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client)
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client, stubEc2())
|
||||
|
||||
await expect(adapter.create({values: {...validValues, subnets: 'subnet-only'}})).rejects.toThrow(
|
||||
new ValidationError('subnets must list at least two subnet ids'),
|
||||
@@ -196,7 +217,7 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
})
|
||||
|
||||
test('requires the fields the schema marks required', async () => {
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client)
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client, stubEc2())
|
||||
|
||||
await expect(adapter.create({values: {}})).rejects.toThrow(new ValidationError('name is required'))
|
||||
await expect(adapter.create({values: {name: 'web'}})).rejects.toThrow(
|
||||
@@ -205,19 +226,55 @@ describe('AwsLoadBalancingAdapter', () => {
|
||||
})
|
||||
|
||||
test('rejects values the runtime would refuse', async () => {
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client)
|
||||
const adapter = new AwsLoadBalancingAdapter(runtimeStub().client, stubEc2())
|
||||
|
||||
for (const name of ['-web', 'web-', 'has spaces', 'a'.repeat(33)]) {
|
||||
await expect(adapter.create({values: {...validValues, name}}), name).rejects.toThrow(ValidationError)
|
||||
}
|
||||
await expect(adapter.create({values: {...validValues, type: 'gateway'}})).rejects.toThrow(ValidationError)
|
||||
await expect(adapter.create({values: {...validValues, scheme: 'sideways'}})).rejects.toThrow(ValidationError)
|
||||
})
|
||||
|
||||
test('deletes a load balancer by ARN', async () => {
|
||||
const {client, sent} = runtimeStub()
|
||||
await new AwsLoadBalancingAdapter(client).delete(ARN)
|
||||
await new AwsLoadBalancingAdapter(client, stubEc2()).delete(ARN)
|
||||
|
||||
expect((sent[0] as DeleteLoadBalancerCommand).input.LoadBalancerArn).toBe(ARN)
|
||||
})
|
||||
|
||||
test('returns VPC dependencies from the account-scoped EC2 client', async () => {
|
||||
const dependencies = await new AwsLoadBalancingAdapter(runtimeStub().client, stubEc2()).dependencies()
|
||||
|
||||
expect(dependencies.vpcs).toEqual([{id: 'vpc-default', cidrBlock: '10.0.0.0/16'}])
|
||||
expect(dependencies.subnets).toHaveLength(2)
|
||||
expect(dependencies.securityGroups).toEqual([{id: 'sg-1', vpcId: 'vpc-default', name: 'web', description: ''}])
|
||||
})
|
||||
|
||||
test('sets security groups and returns the refreshed load balancer', async () => {
|
||||
const {client, sent} = runtimeStub()
|
||||
await new AwsLoadBalancingAdapter(client, stubEc2()).update(ARN, {values: {securityGroups: 'sg-1'}})
|
||||
|
||||
expect(sent[0]).toBeInstanceOf(SetSecurityGroupsCommand)
|
||||
expect((sent[0] as SetSecurityGroupsCommand).input.SecurityGroups).toEqual(['sg-1'])
|
||||
})
|
||||
|
||||
test('manages listeners through the generic flat-item extension', async () => {
|
||||
const {client, sent} = stubElb((command) => {
|
||||
if (command instanceof DescribeListenersCommand) {
|
||||
return {Listeners: [{ListenerArn: LISTENER_ARN, Protocol: 'HTTP', Port: 80, DefaultActions: [{Type: 'forward', TargetGroupArn: 'target-group-arn'}]}]}
|
||||
}
|
||||
if (command instanceof CreateListenerCommand) {
|
||||
return {Listeners: [{ListenerArn: LISTENER_ARN, Protocol: 'HTTP', Port: 8080, DefaultActions: [{Type: 'forward', TargetGroupArn: 'target-group-arn'}]}]}
|
||||
}
|
||||
return {}
|
||||
})
|
||||
const items = new AwsLoadBalancingAdapter(client, stubEc2()).items
|
||||
|
||||
await expect(items.listItems(ARN)).resolves.toMatchObject({items: [{id: LISTENER_ARN, body: {protocol: 'HTTP', port: 80, targetGroupArn: 'target-group-arn'}}]})
|
||||
await expect(items.putItem!(ARN, {port: '8080', targetGroupArn: 'target-group-arn'})).resolves.toMatchObject({id: LISTENER_ARN})
|
||||
await items.deleteItem!(ARN, LISTENER_ARN)
|
||||
|
||||
expect((sent[0] as DescribeListenersCommand).input.LoadBalancerArn).toBe(ARN)
|
||||
expect((sent[1] as CreateListenerCommand).input).toMatchObject({LoadBalancerArn: ARN, Protocol: 'HTTP', Port: 8080, DefaultActions: [{Type: 'forward', TargetGroupArn: 'target-group-arn'}]})
|
||||
expect((sent[2] as DeleteListenerCommand).input.ListenerArn).toBe(LISTENER_ARN)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user