Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AWS RDS Data API #39

Merged
merged 2 commits into from
Dec 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion assert/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import nodeAssert from 'node:assert'

const debug = (...message: any) => { process.env.ASSERT_DEBUG && console.log(...message) }

interface ExtraAssertions {
objectContains<T extends Record<string, any>>(
actual: Record<string, any>,
Expand All @@ -14,6 +16,8 @@ const assertions: ExtraAssertions = {
expected: T,
message?: string | Error,
): asserts actual is Partial<T> {
debug('[ASSERT] objectContains', actual, expected)

if (typeof actual !== 'object' || typeof expected !== 'object') {
throw new Error('Both actual and expected values must be objects');
}
Expand All @@ -23,7 +27,7 @@ const assertions: ExtraAssertions = {
for (const key of expectedKeys) {
if (key in actual) {
if (typeof actual[key] !== typeof expected[key]) {
throw new Error(`Type mismatch for key '${String(key)}'. Expected ${typeof expected[key]} but got ${typeof actual[key]}. ${message}`);
throw new Error(`Type mismatch for key '${String(key)}'. Expected ${typeof expected[key]} but got ${typeof actual[key]}. ${message ?? ''}`);
}

if (typeof expected[key] === 'object' && expected[key] !== null) {
Expand Down
1 change: 1 addition & 0 deletions dtc-aws-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
"dependencies": {
"@aws-sdk/client-dynamodb": "^3.645.0",
"@aws-sdk/client-eventbridge": "^3.645.0",
"@aws-sdk/client-rds-data": "^3.645.0",
"@aws-sdk/client-lambda": "^3.645.0",
"@aws-sdk/client-sns": "^3.645.0",
"@aws-sdk/lib-dynamodb": "^3.645.0",
Expand Down
4 changes: 2 additions & 2 deletions dtc-aws-plugin/src/lambda-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ export const arrange = async (args: unknown) => {

export const act = async (args: unknown) => {
response = undefined

if (!is(args, LambdaCall)) {
const mismatch = diff(args, LambdaCall)
info(`Lambda plugin declared but test declaration didn't match the act. Invalid ${mismatch[0]}\n`)
Expand All @@ -44,7 +44,7 @@ export const act = async (args: unknown) => {
}

export const assert = async (args: unknown) => {
if (!is(args, {lambda: LambdaCall})) {
if (!is(args, {lambda: record(String, unknown)})) {
return
}

Expand Down
71 changes: 71 additions & 0 deletions dtc-aws-plugin/src/rds-data-plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import {RDSData, SqlParameter} from '@aws-sdk/client-rds-data'
import extraAssert from '@cgauge/assert'
import {info} from '@cgauge/dtc'
import {is, unknown, diff, optional, TypeFromSchema, record, intersection} from '@cgauge/type-guard'

const RDSDataCall = {
sql: String,
parameters: optional([unknown]),
resourceArn: String,
secretArn: String,
database: String,
}
type RDSDataCall = TypeFromSchema<typeof RDSDataCall>
type RDSDataCallSql = RDSDataCall & {parameters?: SqlParameter[]}

const RDSDataCallResponse = intersection(RDSDataCall, {
response: record(String, unknown),
})

const rdsData = new RDSData({})

export const executeStatement = async (params: RDSDataCallSql): Promise<any> => {
const response = await rdsData.executeStatement({
...params,
formatRecordsAs: 'JSON',
})

if (response.formattedRecords) {
return JSON.parse(response.formattedRecords)
}
}

export const arrange = async (args: unknown) => {
if (!is(args, {rds: [RDSDataCall]})) {
return
}

await Promise.all(args.rds.map((v) => executeStatement(v as RDSDataCallSql)))
}

export const act = async (args: unknown) => {
if (!is(args, RDSDataCall)) {
const mismatch = diff(args, RDSDataCall)
info(`Lambda plugin declared but test declaration didn't match the act. Invalid ${mismatch[0]}\n`)
return
}

await executeStatement(args as RDSDataCallSql)
}

export const assert = async (args: unknown) => {
if (!is(args, {rds: [RDSDataCallResponse]})) {
return
}

await Promise.all(
args.rds.map(async (v) => {
const response = await executeStatement(v as RDSDataCallSql)

extraAssert.objectContains(v.response, response)
}),
)
}

export const clean = async (args: unknown) => {
if (!is(args, {rds: [RDSDataCall]})) {
return
}

await Promise.all(args.rds.map((v) => executeStatement(v as RDSDataCallSql)))
}
69 changes: 69 additions & 0 deletions dtc-aws-plugin/test/rds-data-plugin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {test, afterEach} from 'node:test'
import {act, arrange, assert, clean} from '../src/rds-data-plugin'
import * as network from '@cgauge/nock-aws'
import nock from 'nock'

nock.disableNetConnect()

afterEach(() => {network.checkForPendingMocks()})

test('It does not arrange if type does not match', () => arrange({}))

test('It does not act if type does not match', () => act({}))

test('It does not assert if type does not match', () => assert({}))

test('It does not clean if type does not match', () => clean({}))

test('It executes a statement in RDS during arrange', async () => {
const statement = {
sql: 'SELECT now()',
resourceArn: 'arn:resource',
secretArn: 'arn:secret',
database: 'database-name',
}

network.rdsData(statement)

await arrange({rds: [statement]})
})

test('It executes a statement in RDS during act', async () => {
const statement = {
sql: 'SELECT now()',
resourceArn: 'arn:resource',
secretArn: 'arn:secret',
database: 'database-name',
}

network.rdsData(statement)

await act(statement)
})

test('It executes a statement in RDS during assert', async () => {
const statement = {
sql: 'SELECT now()',
resourceArn: 'arn:resource',
secretArn: 'arn:secret',
database: 'database-name',
response: [{a: 1}]
}

network.rdsData(statement, {$metadata: {}, formattedRecords: '[{"a": 1}]'})

await assert({rds: [statement]})
})

test('It executes a statement in RDS during clean', async () => {
const statement = {
sql: 'SELECT now()',
resourceArn: 'arn:resource',
secretArn: 'arn:secret',
database: 'database-name',
}

network.rdsData(statement)

await clean({rds: [statement]})
})
4 changes: 4 additions & 0 deletions dtc/src/plugins/http-mock-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import nodeAssert from 'node:assert/strict'
import extraAssert from '@cgauge/assert'
import nock from 'nock'
import {is, optional, record, union, unknown} from '@cgauge/type-guard'
import { debug } from '../utils'

const MockHttp = {
url: String,
Expand All @@ -15,6 +16,8 @@ const MockHttp = {
}

export const partialBodyCheck = (expected: string | Record<string, unknown>) => (body: Record<string, unknown>) => {
debug(`[HTTP_MOCK] Body check:\n ${JSON.stringify(body, null, 2)}\n ${JSON.stringify(expected, null, 2)}`)

if (typeof expected === 'string') {
nodeAssert.equal(body, expected)
return true
Expand All @@ -26,6 +29,7 @@ export const partialBodyCheck = (expected: string | Record<string, unknown>) =>

export const arrange = async (args: unknown) => {
if (!is(args, {http: [MockHttp]})) {
debug('[HTTP_MOCK] Arrange does not match')
return
}

Expand Down
4 changes: 2 additions & 2 deletions nock-aws/src/dynamodb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@ export const streamDynamodb = (request: any, response?: any): void => {
nock(streamUrl).post('/', partialBodyCheck(request)).reply(200, response)
}

export const failDynamodb = (request: any): void => {
export const dynamodbFailWith = (request: any): void => {
nock(url).post('/', partialBodyCheck(request)).reply(400)
}

export const failDynamodbEmpty = (): void => {
export const dynamodbFail = (): void => {
nock(url).post('/').reply(400)
}
2 changes: 1 addition & 1 deletion nock-aws/src/eventbridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const eventBridge = (entries: unknown[]): void => {
.reply(200, {FailedEntryCount: 0})
}

export const failEventBridge = (): void => {
export const eventBridgeFail = (): void => {
nock(url).post('/').reply(200, {FailedEntryCount: 1})
}
4 changes: 4 additions & 0 deletions nock-aws/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import nodeAssert from 'node:assert/strict'
import extraAssert from '@cgauge/assert'
import nock from 'nock'

const debug = (...message: any) => { process.env.NOCK_AWS_DEBUG && console.log(...message) }

export const checkForPendingMocks = (): void => {
if (!nock.isDone()) {
const error = nock.pendingMocks()
Expand All @@ -12,6 +14,8 @@ export const checkForPendingMocks = (): void => {
}

export const partialBodyCheck = (expected: string | Record<string, unknown>) => (body: Record<string, unknown>) => {
debug('[NOCK_AWS] Body check', body, expected)

if (typeof expected === 'string') {
nodeAssert.equal(body, expected)
return true
Expand Down
4 changes: 2 additions & 2 deletions nock-aws/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
export * from './helpers.js'
export * from './dynamodb.js'
export * from './eventbridge.js'
export * from './dynamodb.js'
export * from './helpers.js'
export * from './lambda.js'
export * from './rds-data.js'
export * from './ses.js'
export * from './sns.js'
export * from './translate.js'
19 changes: 18 additions & 1 deletion nock-aws/src/lambda.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,23 @@ type LambdaRequest = {
InvokeArgs?: any
}

export const lambda = (request: LambdaRequest, response?: any): void => {
nock(url)
.post(
`/2014-11-13/functions/${request.FunctionName || ''}/invoke`,
partialBodyCheck({
...JSON.parse(request.InvokeArgs?.toString() || ''),
}),
)
.reply(200, response)
}

export const lambdaFail = (request: LambdaRequest): void => {
nock(url)
.post(`/2014-11-13/functions/${request.FunctionName || ''}/invoke`)
.reply(500)
}

export const lambdaAsync = (request: LambdaRequest, response?: any): void => {
nock(url)
.post(
Expand All @@ -30,7 +47,7 @@ export const lambdaAsync = (request: LambdaRequest, response?: any): void => {
.reply(200, response ?? lambdaAsyncResponse)
}

export const failLambdaAsync = (request: LambdaRequest): void => {
export const lambdaAsyncFail = (request: LambdaRequest): void => {
nock(url)
.post(`/2014-11-13/functions/${request.FunctionName || ''}/invoke-async`)
.reply(500)
Expand Down
25 changes: 25 additions & 0 deletions nock-aws/src/rds-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import nock from 'nock'
import {partialBodyCheck} from './helpers'
import {ExecuteStatementCommandInput, ExecuteStatementCommandOutput} from '@aws-sdk/client-rds-data'

const region = process.env.AWS_REGION || 'eu-west-1'
const url = `https://rds-data.${region}.amazonaws.com`

export const rdsData = (request: ExecuteStatementCommandInput, response?: ExecuteStatementCommandOutput): void => {
nock(url)
.post(`/Execute`, partialBodyCheck(request as unknown as Record<string, unknown>))
.reply(200, {
formattedRecords: '{}',
...response,
})
}

export const rdsDataFailWith = (request: ExecuteStatementCommandInput): void => {
nock(url)
.post('/Execute', partialBodyCheck(request as unknown as Record<string, unknown>))
.reply(400)
}

export const rdsDataFail = (): void => {
nock(url).post(`/Execute`).reply(500)
}
4 changes: 2 additions & 2 deletions nock-aws/src/ses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@ export const ses = (request: SesRequest, response?: any): void => {
.reply(200, response ?? sendEmailResponse)
}

export const failSes = (): void => {
export const sesFail = (): void => {
nock(url).post('/').reply(500)
}

export const failSesWith = (request: SesRequest): void => {
export const sesFailWith = (request: SesRequest): void => {
const params = {
Action: 'SendEmail',
'Destination.ToAddresses.member.1': request.Destination?.ToAddresses?.[0],
Expand Down
8 changes: 8 additions & 0 deletions nock-aws/src/sns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,11 @@ const snsResponse = `
export const sns = (request: any, response?: any): void => {
nock(url).post('/', partialBodyCheck(request)).reply(200, response ?? snsResponse)
}

export const snsFailWith = (request: any): void => {
nock(url).post('/', partialBodyCheck(request)).reply(400)
}

export const snsFail = (): void => {
nock(url).post(`/Execute`).reply(500)
}
Loading