import {describe, expect, test} from 'bun:test' import { CreateCacheClusterCommand, CreateReplicationGroupCommand, DeleteCacheClusterCommand, DeleteReplicationGroupCommand, DescribeCacheClustersCommand, DescribeReplicationGroupsCommand, type ElastiCacheClient, } from '@aws-sdk/client-elasticache' import {ValidationError} from '../cloud-spi/errors' import {AwsElastiCacheAdapter} from './AwsElastiCacheAdapter' type ElastiCacheCommand = | CreateCacheClusterCommand | CreateReplicationGroupCommand | DeleteCacheClusterCommand | DeleteReplicationGroupCommand | DescribeCacheClustersCommand | DescribeReplicationGroupsCommand function recordingClient(responder: (command: ElastiCacheCommand) => unknown | Promise) { const commands: ElastiCacheCommand[] = [] const client = { async send(command: ElastiCacheCommand) { commands.push(command) return responder(command) }, } as unknown as ElastiCacheClient return {client, commands} } describe('AwsElastiCacheAdapter', () => { test('lists replication groups and cache clusters with native resource metadata', async () => { const {client} = recordingClient((command) => { if (command instanceof DescribeReplicationGroupsCommand) return {ReplicationGroups: [{ ReplicationGroupId: 'orders', ARN: 'arn:aws:elasticache:us-east-1:000000000000:replicationgroup:orders', Status: 'available', Engine: 'valkey', CacheNodeType: 'cache.t3.micro', ClusterEnabled: true, ConfigurationEndpoint: {Address: 'orders.cfg.local', Port: 6379}, NodeGroups: [{NodeGroupId: '0001', PrimaryEndpoint: {Address: 'orders-001.local', Port: 6379}}], }]} return {CacheClusters: [{ CacheClusterId: 'sessions', ARN: 'arn:aws:elasticache:us-east-1:000000000000:cluster:sessions', CacheClusterStatus: 'available', Engine: 'memcached', EngineVersion: '1.6.22', CacheNodeType: 'cache.t3.micro', CacheClusterCreateTime: new Date('2026-10-05T10:00:00.000Z'), NumCacheNodes: 1, CacheNodes: [{CacheNodeId: '0001', Endpoint: {Address: 'sessions.local', Port: 11211}}], }]} }) const resources = await new AwsElastiCacheAdapter(client).list() expect(resources).toEqual([ expect.objectContaining({id: 'orders', type: 'replication-group', engine: 'valkey', status: 'available', region: 'us-east-1'}), expect.objectContaining({id: 'sessions', type: 'cache-cluster', engine: 'memcached', version: '1.6.22', createdAt: '2026-10-05T10:00:00.000Z'}), ]) expect(resources[0]?.metadata).toMatchObject({endpoint: 'orders.cfg.local', clusterEnabled: true, nodeGroups: [{NodeGroupId: '0001'}]}) expect(resources[1]?.metadata).toMatchObject({endpoint: 'sessions.local', numCacheNodes: 1}) }) test('creates a cluster-mode replication group with its AWS topology input', async () => { const {client, commands} = recordingClient((command) => { expect(command).toBeInstanceOf(CreateReplicationGroupCommand) return {ReplicationGroup: { ReplicationGroupId: 'orders', ReplicationGroupDescription: 'Order cache', Engine: 'redis', CacheNodeType: 'cache.t3.micro', Status: 'creating', }} }) const resource = await new AwsElastiCacheAdapter(client).create({values: { kind: 'replication-group', id: ' orders ', description: ' Order cache ', engine: 'redis', nodeType: 'cache.t3.micro', port: '6379', clusterMode: 'enabled', numNodeGroups: '2', replicasPerNodeGroup: '1', }}) expect((commands[0] as CreateReplicationGroupCommand).input).toEqual({ ReplicationGroupId: 'orders', ReplicationGroupDescription: 'Order cache', Engine: 'redis', CacheNodeType: 'cache.t3.micro', Port: 6379, NumNodeGroups: 2, ReplicasPerNodeGroup: 1, }) expect(resource).toMatchObject({id: 'orders', type: 'replication-group', status: 'creating'}) }) test('creates a cache cluster with the cache-cluster AWS contract', async () => { const {client, commands} = recordingClient((command) => { expect(command).toBeInstanceOf(CreateCacheClusterCommand) return {CacheCluster: {CacheClusterId: 'sessions', Engine: 'memcached', CacheNodeType: 'cache.t3.micro', CacheClusterStatus: 'creating'}} }) await new AwsElastiCacheAdapter(client).create({values: { kind: 'cache-cluster', id: 'sessions', engine: 'memcached', nodeType: 'cache.t3.micro', numCacheNodes: '2', }}) expect((commands[0] as CreateCacheClusterCommand).input).toEqual({ CacheClusterId: 'sessions', Engine: 'memcached', CacheNodeType: 'cache.t3.micro', NumCacheNodes: 2, }) }) test('rejects invalid resource combinations before sending a command', async () => { const {client, commands} = recordingClient(() => ({})) const adapter = new AwsElastiCacheAdapter(client) await expect(adapter.create({values: { kind: 'replication-group', id: 'orders', description: 'Orders', engine: 'memcached', nodeType: 'cache.t3.micro', }})).rejects.toBeInstanceOf(ValidationError) await expect(adapter.create({values: { kind: 'cache-cluster', id: 'orders--cache', engine: 'redis', nodeType: 'cache.t3.micro', }})).rejects.toBeInstanceOf(ValidationError) expect(commands).toHaveLength(0) }) test('deletes replication groups and cache clusters through their respective commands', async () => { const {client, commands} = recordingClient((command) => { if (command instanceof DescribeReplicationGroupsCommand && command.input.ReplicationGroupId === 'orders') { return {ReplicationGroups: [{ReplicationGroupId: 'orders'}]} } if (command instanceof DescribeReplicationGroupsCommand) { throw Object.assign(new Error('not found'), {name: 'ReplicationGroupNotFoundFault'}) } if (command instanceof DescribeCacheClustersCommand) return {CacheClusters: [{CacheClusterId: 'sessions'}]} return {} }) const adapter = new AwsElastiCacheAdapter(client) await adapter.delete('orders') await adapter.delete('sessions') expect(commands.filter((command) => command instanceof DeleteReplicationGroupCommand)[0]?.input).toEqual({ ReplicationGroupId: 'orders', RetainPrimaryCluster: false, }) expect(commands.filter((command) => command instanceof DeleteCacheClusterCommand)[0]?.input).toEqual({CacheClusterId: 'sessions'}) }) })