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' 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} } const ARN = 'arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/web/77aa388f69144785' const balancer = { LoadBalancerArn: ARN, LoadBalancerName: 'web', DNSName: 'web-77aa388f69144785.elb.localhost.floci.io', // The runtime nests this: provisioning State: {Code: 'provisioning'}, Type: 'application', Scheme: 'internet-facing', VpcId: 'vpc-default', CreatedTime: new Date('2026-07-28T17:04:35.028Z'), AvailabilityZones: [ {ZoneName: 'us-east-1b', SubnetId: 'subnet-default-b'}, {ZoneName: 'us-east-1c', SubnetId: 'subnet-default-c'}, ], SecurityGroups: ['sg-1'], } function runtimeStub() { return stubElb((command) => { if (command instanceof CreateLoadBalancerCommand) return {LoadBalancers: [balancer]} if (command instanceof DescribeLoadBalancersCommand) return {LoadBalancers: [balancer]} return {} }) } 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, stubEc2()) expect(adapter.cloud).toBe('aws') expect(adapter.service).toBe('loadbalancing') expect(adapter.schema().displayName).toBe('AWS Elastic Load Balancing') }) test('lists load balancers and reads the nested state code', async () => { const {client, sent} = runtimeStub() const [resource] = await new AwsLoadBalancingAdapter(client, stubEc2()).list() expect(sent[0]).toBeInstanceOf(DescribeLoadBalancersCommand) expect(resource).toMatchObject({ // The ARN is the id: Describe and Delete both take one. id: ARN, name: 'web', cloud: 'aws', service: 'loadbalancing', type: 'load-balancer', region: 'us-east-1', // State is nested, not a flat field — reading State directly yields // an object and the column would render blank. status: 'provisioning', createdAt: '2026-07-28T17:04:35.028Z', }) expect(resource?.metadata).toMatchObject({ dnsName: 'web-77aa388f69144785.elb.localhost.floci.io', lbType: 'application', scheme: 'internet-facing', vpcId: 'vpc-default', subnets: ['subnet-default-b', 'subnet-default-c'], availabilityZones: ['us-east-1b', 'us-east-1c'], }) }) test('follows the pagination marker', async () => { let call = 0 const {client, sent} = stubElb((command) => { if (!(command instanceof DescribeLoadBalancersCommand)) return {} call += 1 if (call === 1) return {LoadBalancers: [balancer], NextMarker: 'page-2'} return {LoadBalancers: [{...balancer, LoadBalancerName: 'api', LoadBalancerArn: `${ARN}-2`}]} }) 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') }) test('treats an empty NextMarker as the end of the list', async () => { // The runtime returns , which the SDK surfaces as // an empty string; looping on truthiness alone would spin forever. let call = 0 const {client} = stubElb((command) => { if (!(command instanceof DescribeLoadBalancersCommand)) return {} call += 1 return {LoadBalancers: [balancer], NextMarker: ''} }) 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, stubEc2()) await expect(adapter.list({search: 'we'})).resolves.toHaveLength(1) await expect(adapter.list({search: 'loadbalancer/app'})).resolves.toHaveLength(1) await expect(adapter.list({search: 'nope'})).resolves.toHaveLength(0) }) test('inspects a load balancer by ARN', async () => { const {client, sent} = runtimeStub() const resource = await new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN) expect((sent[0] as DescribeLoadBalancersCommand).input.LoadBalancerArns).toEqual([ARN]) expect(resource?.id).toBe(ARN) }) test('returns null on LoadBalancerNotFound even though the runtime answers 400', async () => { // ELB reports a missing load balancer as LoadBalancerNotFound with HTTP 400, // not 404, so matching on the status would rethrow instead of returning null. const {client} = stubElb(() => { throw Object.assign(new Error('One or more load balancers not found.'), { name: 'LoadBalancerNotFound', $metadata: {httpStatusCode: 400}, }) }) await expect(new AwsLoadBalancingAdapter(client, stubEc2()).get(ARN)).resolves.toBeNull() }) test('also returns null for the SDK-modelled exception name', async () => { // The wire is LoadBalancerNotFound but the SDK calls the error // LoadBalancerNotFoundException. Matching only the wire code passed every // stubbed test here and still returned 400 against the live runtime. const {client} = stubElb(() => { throw Object.assign(new Error('One or more load balancers not found.'), { name: 'LoadBalancerNotFoundException', $metadata: {httpStatusCode: 400}, }) }) 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, stubEc2()).get(ARN)).resolves.toBeNull() }) test('rethrows a failure that is not a missing load balancer', async () => { const {client} = stubElb(() => { throw Object.assign(new Error('AccessDenied'), { name: 'AccessDeniedException', $metadata: {httpStatusCode: 403}, }) }) 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, 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) }) 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, stubEc2()) await expect(adapter.create({values: {...validValues, subnets: 'subnet-only'}})).rejects.toThrow( new ValidationError('subnets must list at least two subnet ids'), ) }) test('requires the fields the schema marks required', async () => { 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( new ValidationError('subnets is required'), ) }) test('rejects values the runtime would refuse', async () => { 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, scheme: 'sideways'}})).rejects.toThrow(ValidationError) }) test('deletes a load balancer by ARN', async () => { const {client, sent} = runtimeStub() 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) }) })