623 lines
24 KiB
TypeScript
623 lines
24 KiB
TypeScript
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<unknown>
|
|
|
|
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<unknown>) {
|
|
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)
|
|
})
|
|
})
|