import {describe, expect, test} from 'bun:test' import { CreateDBInstanceCommand, CreateDBSnapshotCommand, DeleteDBInstanceCommand, DescribeDBInstancesCommand, DescribeDBSnapshotsCommand, DescribeOrderableDBInstanceOptionsCommand, ListTagsForResourceCommand, ModifyDBInstanceCommand, RebootDBInstanceCommand, StartDBInstanceCommand, StopDBInstanceCommand, type RDSClient, } from '@aws-sdk/client-rds' import {ExecuteStatementCommand, type RDSDataClient} from '@aws-sdk/client-rds-data' import {ConflictError, ValidationError} from '../cloud-spi/errors' import {createRdsService} from '../services/rds' import {mapAwsSdkError} from './awsErrors' import {AwsDatabaseAdapter} from './AwsDatabaseAdapter' type RdsCommand = | CreateDBInstanceCommand | CreateDBSnapshotCommand | DeleteDBInstanceCommand | DescribeDBInstancesCommand | DescribeDBSnapshotsCommand | DescribeOrderableDBInstanceOptionsCommand | ListTagsForResourceCommand | ModifyDBInstanceCommand | RebootDBInstanceCommand | StartDBInstanceCommand | StopDBInstanceCommand type Responder = (command: RdsCommand) => unknown | Promise function recordingRds(responder: Responder) { const commands: RdsCommand[] = [] const client = { async send(command: RdsCommand) { commands.push(command) return responder(command) }, } as unknown as RDSClient return {client, commands} } function adapterWith(responder: Responder) { const recording = recordingRds(responder) return { ...recording, adapter: new AwsDatabaseAdapter(createRdsService(recording.client), recording.client), } } function recordingRdsData(responder: (command: ExecuteStatementCommand) => unknown | Promise) { const commands: ExecuteStatementCommand[] = [] const client = { async send(command: ExecuteStatementCommand) { commands.push(command) return responder(command) }, } as unknown as RDSDataClient return {client, commands} } function instanceResponse(command: CreateDBInstanceCommand) { return { DBInstance: { DBInstanceIdentifier: command.input.DBInstanceIdentifier, Engine: command.input.Engine, EngineVersion: command.input.EngineVersion, DBInstanceClass: command.input.DBInstanceClass, AllocatedStorage: command.input.AllocatedStorage, MasterUsername: command.input.MasterUsername, DBName: command.input.DBName, DBInstanceStatus: 'creating', }, } } const validCreateValues = { dbInstanceIdentifier: 'orders-db', engine: 'postgres', masterUserPassword: 'secret-pass', } describe('AwsDatabaseAdapter', () => { test('explores PostgreSQL databases through the RDS Data API without console credentials', async () => { const {client, commands} = recordingRdsData((command) => { expect(command.input.resourceArn).toBe('arn:aws:rds:us-east-1:000000000000:db:orders-db') expect(command.input.database).toBe('orders') expect(command.input.sql).toContain('FROM pg_database') return { columnMetadata: [{name: 'name', typeName: 'varchar'}, {name: 'state', typeName: 'varchar'}, {name: 'isSystem', typeName: 'bool'}], records: [[{stringValue: 'orders'}, {stringValue: 'ONLINE'}, {booleanValue: false}]], } }) const {client: rds, commands: rdsCommands} = recordingRds((command) => { expect(command).toBeInstanceOf(DescribeDBInstancesCommand) return {DBInstances: [{ DBInstanceIdentifier: 'orders-db', DBInstanceArn: 'arn:aws:rds:us-east-1:000000000000:db:orders-db', Engine: 'postgres', DBName: 'orders', VpcSecurityGroups: [], }]} }) const adapter = new AwsDatabaseAdapter(createRdsService(rds), rds, client) await expect(adapter.listSqlDatabases('orders-db', {username: '', password: ''})).resolves.toEqual([{ name: 'orders', state: 'ONLINE', createdAt: null, isSystem: false, }]) expect(rdsCommands).toHaveLength(1) expect(commands).toHaveLength(1) expect(commands[0].input.secretArn).toContain('floci-ui-rds-data') }) test('rejects SQL exploration for unsupported RDS engines', async () => { const {client: rds} = recordingRds(() => ({DBInstances: [{ DBInstanceIdentifier: 'maria-db', Engine: 'mariadb', VpcSecurityGroups: [], }]})) const {client} = recordingRdsData(() => ({})) const adapter = new AwsDatabaseAdapter(createRdsService(rds), rds, client) await expect(adapter.querySql('maria-db', {username: '', password: '', database: 'app'}, 'SELECT 1')) .rejects.toBeInstanceOf(ValidationError) }) test('keeps list search behavior intact', async () => { const {adapter} = adapterWith(async (command) => { expect(command).toBeInstanceOf(DescribeDBInstancesCommand) return { DBInstances: [ {DBInstanceIdentifier: 'orders-db', Engine: 'postgres', VpcSecurityGroups: []}, {DBInstanceIdentifier: 'users-db', Engine: 'mysql', VpcSecurityGroups: []}, ], } }) const resources = await adapter.list({search: ' ORDERS '}) expect(resources.map(({id}) => id)).toEqual(['orders-db']) }) test('keeps get 404 behavior intact', async () => { const {adapter} = adapterWith(async () => { throw Object.assign(new Error('not found'), {$metadata: {httpStatusCode: 404}}) }) await expect(adapter.get('missing-db')).resolves.toBeNull() }) test('creates an instance with required values and provider defaults', async () => { const {adapter, commands} = adapterWith((command) => { expect(command).toBeInstanceOf(CreateDBInstanceCommand) return instanceResponse(command as CreateDBInstanceCommand) }) const resource = await adapter.create({values: { dbInstanceIdentifier: ' orders-db ', engine: 'postgres', masterUserPassword: 'secret-pass', }}) const createCommand = commands[0] as CreateDBInstanceCommand const {MasterUserPassword, ...nonSensitiveInput} = createCommand.input expect(nonSensitiveInput).toEqual({ DBInstanceIdentifier: 'orders-db', Engine: 'postgres', DBInstanceClass: 'db.t3.micro', AllocatedStorage: 20, MasterUsername: 'root', }) expect(MasterUserPassword === validCreateValues.masterUserPassword).toBe(true) expect(JSON.stringify(resource).includes(validCreateValues.masterUserPassword)).toBe(false) expect(resource).toMatchObject({ id: 'orders-db', name: 'orders-db', type: 'db-instance', status: 'creating', engine: 'postgres', instanceClass: 'db.t3.micro', }) }) test('maps all nine create fields including security groups', async () => { const {adapter, commands} = adapterWith((command) => instanceResponse(command as CreateDBInstanceCommand)) await adapter.create({values: { dbInstanceIdentifier: 'orders-db', engine: 'mariadb', masterUserPassword: 'secret-pass', dbInstanceClass: ' db.m6g.large ', allocatedStorage: '100', masterUsername: ' app_user ', dbName: ' orders ', engineVersion: ' 10.11 ', securityGroupIds: 'sg-12345678, sg-87654321', }}) const createCommand = commands[0] as CreateDBInstanceCommand const {MasterUserPassword, ...nonSensitiveInput} = createCommand.input expect(nonSensitiveInput).toEqual({ DBInstanceIdentifier: 'orders-db', Engine: 'mariadb', DBInstanceClass: 'db.m6g.large', AllocatedStorage: 100, MasterUsername: 'app_user', DBName: 'orders', EngineVersion: '10.11', VpcSecurityGroupIds: ['sg-12345678', 'sg-87654321'], }) expect(MasterUserPassword === validCreateValues.masterUserPassword).toBe(true) }) test('accepts array of security group ids', async () => { const {adapter, commands} = adapterWith((command) => instanceResponse(command as CreateDBInstanceCommand)) await adapter.create({values: { ...validCreateValues, securityGroupIds: ['sg-11111111', 'sg-22222222'], }}) const createCommand = commands[0] as CreateDBInstanceCommand expect(createCommand.input.VpcSecurityGroupIds).toEqual(['sg-11111111', 'sg-22222222']) }) test('uses defaults for blank values and omits blank database name and engine version', async () => { const {adapter, commands} = adapterWith((command) => instanceResponse(command as CreateDBInstanceCommand)) await adapter.create({values: { ...validCreateValues, dbInstanceClass: ' ', allocatedStorage: '', masterUsername: ' ', dbName: ' ', }}) const input = (commands[0] as CreateDBInstanceCommand).input expect(input.DBInstanceClass).toBe('db.t3.micro') expect(input.AllocatedStorage).toBe(20) expect(input.MasterUsername).toBe('root') expect(input.DBName).toBeUndefined() expect(input.EngineVersion).toBeUndefined() }) test('rejects invalid instance identifiers before calling RDS', async () => { const {adapter, commands} = adapterWith(async () => ({})) for (const dbInstanceIdentifier of ['', 'Orders-db', '1-orders', 'orders--db', 'orders-', 'a'.repeat(64)]) { await expect(adapter.create({values: { ...validCreateValues, dbInstanceIdentifier, }})).rejects.toBeInstanceOf(ValidationError) } expect(commands).toHaveLength(0) }) test('rejects unsupported engines before calling RDS', async () => { const {adapter, commands} = adapterWith(async () => ({})) for (const engine of ['', 'oracle', 'POSTGRES', 42]) { await expect(adapter.create({values: {...validCreateValues, engine}})).rejects.toBeInstanceOf(ValidationError) } expect(commands).toHaveLength(0) }) test('rejects invalid passwords without exposing them or calling RDS', async () => { const {adapter, commands} = adapterWith(async () => ({})) for (const masterUserPassword of ['', 'short', 'x'.repeat(129), 42]) { let caught: unknown try { await adapter.create({values: {...validCreateValues, masterUserPassword}}) } catch (error) { caught = error } expect(caught).toBeInstanceOf(ValidationError) const exposesPassword = caught instanceof Error && typeof masterUserPassword === 'string' && masterUserPassword.length > 0 && caught.message.includes(masterUserPassword) expect(exposesPassword).toBe(false) } expect(commands).toHaveLength(0) }) test('rejects non-positive, fractional, and non-numeric storage before calling RDS', async () => { const {adapter, commands} = adapterWith(async () => ({})) for (const allocatedStorage of [0, -1, 1.5, '0', '-1', '1.5', 'twenty', true]) { await expect(adapter.create({values: { ...validCreateValues, allocatedStorage, }})).rejects.toBeInstanceOf(ValidationError) } expect(commands).toHaveLength(0) }) test('rejects storage that exceeds the safe integer range before calling RDS', async () => { const {adapter, commands} = adapterWith(async () => ({})) await expect(adapter.create({values: { ...validCreateValues, allocatedStorage: '9'.repeat(400), }})).rejects.toBeInstanceOf(ValidationError) expect(commands).toHaveLength(0) }) test('leaves provider failures for the shared AWS error mapper', async () => { const providerError = Object.assign(new Error('instance already exists'), { name: 'DBInstanceAlreadyExistsFault', $fault: 'client', $metadata: {httpStatusCode: 400}, }) const {adapter} = adapterWith(async () => { throw providerError }) let caught: unknown try { await adapter.create({values: validCreateValues}) } catch (error) { caught = error } expect(caught).toBe(providerError) expect(mapAwsSdkError(caught)).toBeInstanceOf(ConflictError) expect(caught instanceof Error && caught.message.includes(validCreateValues.masterUserPassword)).toBe(false) }) test('deletes without a final snapshot', async () => { const {adapter, commands} = adapterWith(async () => ({})) await adapter.delete('orders-db') const deleteCommand = commands[0] as DeleteDBInstanceCommand expect(deleteCommand).toBeInstanceOf(DeleteDBInstanceCommand) expect(deleteCommand.input).toEqual({ DBInstanceIdentifier: 'orders-db', SkipFinalSnapshot: true, }) }) test('sends RDS lifecycle commands for an instance', async () => { const {adapter, commands} = adapterWith(async () => ({})) await adapter.start('orders-db') await adapter.stop('orders-db') await adapter.reboot('orders-db') expect(commands.map((command) => command.constructor)).toEqual([ StartDBInstanceCommand, StopDBInstanceCommand, RebootDBInstanceCommand, ]) expect(commands.map((command) => command.input)).toEqual([ {DBInstanceIdentifier: 'orders-db'}, {DBInstanceIdentifier: 'orders-db'}, {DBInstanceIdentifier: 'orders-db'}, ]) }) test('lists and normalizes database snapshots', async () => { const {adapter, commands} = adapterWith(async (command) => { expect(command).toBeInstanceOf(DescribeDBSnapshotsCommand) return { DBSnapshots: [{ DBSnapshotIdentifier: 'orders-db-snapshot-1', DBInstanceIdentifier: 'orders-db', DBSnapshotArn: 'arn:aws:rds:eu-west-2:000000000000:snapshot:orders-db-snapshot-1', Status: 'available', Engine: 'postgres', EngineVersion: '16.4', AllocatedStorage: 100, SnapshotType: 'manual', SnapshotCreateTime: new Date('2026-08-14T09:30:00.000Z'), Port: 5432, AvailabilityZone: 'eu-west-2a', VpcId: 'vpc-123', }], } }) const snapshots = await adapter.listDatabaseSnapshots('orders-db') expect((commands[0] as DescribeDBSnapshotsCommand).input).toEqual({ DBInstanceIdentifier: 'orders-db', Marker: undefined, }) expect(snapshots).toEqual([{ id: 'orders-db-snapshot-1', name: 'orders-db-snapshot-1', instanceIdentifier: 'orders-db', status: 'available', engine: 'postgres', version: '16.4', createdAt: '2026-08-14T09:30:00.000Z', metadata: { arn: 'arn:aws:rds:eu-west-2:000000000000:snapshot:orders-db-snapshot-1', allocatedStorage: 100, snapshotType: 'manual', port: 5432, availabilityZone: 'eu-west-2a', vpcId: 'vpc-123', }, }]) }) test('does not swallow snapshot provider failures', async () => { const providerError = Object.assign(new Error('operation unavailable'), { name: 'UnsupportedOperation', $fault: 'client', $metadata: {httpStatusCode: 501}, }) const {adapter} = adapterWith(async () => { throw providerError }) await expect(adapter.listDatabaseSnapshots()).rejects.toBe(providerError) }) test('creates and normalizes a database snapshot', async () => { const {adapter, commands} = adapterWith(async (command) => { expect(command).toBeInstanceOf(CreateDBSnapshotCommand) return { DBSnapshot: { DBSnapshotIdentifier: 'orders-db-snapshot-1', DBInstanceIdentifier: 'orders-db', Status: 'creating', Engine: 'postgres', SnapshotType: 'manual', }, } }) const snapshot = await adapter.createDatabaseSnapshot({ instanceIdentifier: ' orders-db ', snapshotIdentifier: ' orders-db-snapshot-1 ', }) const createCommand = commands[0] as CreateDBSnapshotCommand expect(createCommand.input).toEqual({ DBInstanceIdentifier: 'orders-db', DBSnapshotIdentifier: 'orders-db-snapshot-1', }) expect(snapshot).toMatchObject({ id: 'orders-db-snapshot-1', name: 'orders-db-snapshot-1', instanceIdentifier: 'orders-db', status: 'creating', engine: 'postgres', metadata: {snapshotType: 'manual'}, }) }) test('rejects missing and invalid snapshot identifiers before calling RDS', async () => { const {adapter, commands} = adapterWith(async () => ({})) const invalidInputs = [ {instanceIdentifier: '', snapshotIdentifier: 'orders-db-snapshot-1'}, {instanceIdentifier: 'orders-db', snapshotIdentifier: ''}, {instanceIdentifier: 'orders db', snapshotIdentifier: 'orders-db-snapshot-1'}, {instanceIdentifier: 'orders-db', snapshotIdentifier: '1-orders-snapshot'}, {instanceIdentifier: 'orders-db', snapshotIdentifier: 'orders--snapshot'}, {instanceIdentifier: 'orders-db', snapshotIdentifier: 'orders-snapshot-'}, {instanceIdentifier: 'orders-db', snapshotIdentifier: 'a'.repeat(256)}, ] for (const input of invalidInputs) { await expect(adapter.createDatabaseSnapshot(input)).rejects.toBeInstanceOf(ValidationError) } expect(commands).toHaveLength(0) }) test('lists and deduplicates orderable instance classes', async () => { const {adapter, commands} = adapterWith(async (command) => { expect(command).toBeInstanceOf(DescribeOrderableDBInstanceOptionsCommand) return { OrderableDBInstanceOptions: [ {DBInstanceClass: 'db.t3.micro', Engine: 'postgres'}, {DBInstanceClass: 'db.m8g.large', Engine: 'postgres'}, {DBInstanceClass: 'db.t3.micro', Engine: 'postgres'}, ], } }) const classes = await adapter.listDatabaseOrderableInstanceClasses('postgres') expect((commands[0] as DescribeOrderableDBInstanceOptionsCommand).input).toEqual({ Engine: 'postgres', Marker: undefined, }) expect(classes).toEqual(['db.t3.micro', 'db.m8g.large']) }) test('updates an instance with supported fields and excludes unsupported fields', async () => { const {adapter, commands} = adapterWith(async (command) => { expect(command).toBeInstanceOf(ModifyDBInstanceCommand) return { DBInstance: { DBInstanceIdentifier: 'orders-db', Engine: 'postgres', DBInstanceStatus: 'available', IAMDatabaseAuthenticationEnabled: false, DBSubnetGroup: {DBSubnetGroupName: 'private-db'}, VpcSecurityGroups: [{VpcSecurityGroupId: 'sg-1', Status: 'active'}, {VpcSecurityGroupId: 'sg-2', Status: 'active'}], OptionGroupMemberships: [{OptionGroupName: 'default-postgres', Status: 'in-sync'}], AutoMinorVersionUpgrade: true, }, } }) const resource = await adapter.update('orders-db', {values: { masterUserPassword: 'new-secret', enableIamDatabaseAuthentication: 'false', dbSubnetGroupName: 'private-db', vpcSecurityGroupIds: 'sg-1, sg-2', optionGroupName: 'default-postgres', autoMinorVersionUpgrade: 'true', }}) const modifyCommand = commands[0] as ModifyDBInstanceCommand expect(modifyCommand.input).toEqual({ DBInstanceIdentifier: 'orders-db', MasterUserPassword: 'new-secret', EnableIAMDatabaseAuthentication: false, DBSubnetGroupName: 'private-db', VpcSecurityGroupIds: ['sg-1', 'sg-2'], OptionGroupName: 'default-postgres', AutoMinorVersionUpgrade: true, }) expect(modifyCommand.input).not.toHaveProperty('DBInstanceClass') expect(modifyCommand.input).not.toHaveProperty('AllocatedStorage') expect(modifyCommand.input).not.toHaveProperty('Engine') expect(modifyCommand.input).not.toHaveProperty('EngineVersion') expect(modifyCommand.input).not.toHaveProperty('ApplyImmediately') expect(JSON.stringify(resource)).not.toContain('new-secret') expect(resource.metadata.optionGroupName).toBe('default-postgres') expect(resource.metadata.autoMinorVersionUpgrade).toBe(true) expect(resource.metadata.vpcSecurityGroupIds).toBe('sg-1, sg-2') }) test('accepts boolean literals for boolean update fields', async () => { const {adapter, commands} = adapterWith(async (command) => ({ DBInstance: { DBInstanceIdentifier: 'orders-db', Engine: 'postgres', DBInstanceStatus: 'available', }, })) await adapter.update('orders-db', {values: { enableIamDatabaseAuthentication: true, autoMinorVersionUpgrade: false, }}) const modifyCommand = commands[0] as ModifyDBInstanceCommand expect(modifyCommand.input.EnableIAMDatabaseAuthentication).toBe(true) expect(modifyCommand.input.AutoMinorVersionUpgrade).toBe(false) }) test('omits blank strings from update and trims CSV', async () => { const {adapter, commands} = adapterWith(async () => ({ DBInstance: { DBInstanceIdentifier: 'orders-db', Engine: 'postgres', DBInstanceStatus: 'available', }, })) await adapter.update('orders-db', {values: { masterUserPassword: '', enableIamDatabaseAuthentication: '', dbSubnetGroupName: ' ', vpcSecurityGroupIds: ' sg-100 , sg-200 ', optionGroupName: '', autoMinorVersionUpgrade: '', }}) const modifyCommand = commands[0] as ModifyDBInstanceCommand expect(modifyCommand.input).toEqual({ DBInstanceIdentifier: 'orders-db', VpcSecurityGroupIds: ['sg-100', 'sg-200'], }) }) test('rejects malformed booleans and invalid passwords without echoing password', async () => { const {adapter, commands} = adapterWith(async () => ({})) await expect(adapter.update('orders-db', {values: { enableIamDatabaseAuthentication: 'maybe', }})).rejects.toBeInstanceOf(ValidationError) await expect(adapter.update('orders-db', {values: { masterUserPassword: 'short', }})).rejects.toThrow('8 and 128') try { await adapter.update('orders-db', {values: {masterUserPassword: 'short'}}) } catch (err) { expect(String(err)).not.toContain('short') } expect(commands).toHaveLength(0) }) test('rejects update requests with zero effective supported changes', async () => { const {adapter, commands} = adapterWith(async () => ({})) await expect(adapter.update('orders-db', {values: {}})).rejects.toBeInstanceOf(ValidationError) await expect(adapter.update('orders-db', {values: { masterUserPassword: '', dbSubnetGroupName: '', unsupportedField: 'value', }})).rejects.toBeInstanceOf(ValidationError) expect(commands).toHaveLength(0) }) })