Skip to content
Open
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
50 changes: 6 additions & 44 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Deploy on LocalStack
name: Deploy on LocalStack

on:
push:
Expand All @@ -10,7 +10,7 @@ on:
branches:
- main
schedule:
# At 00:00 on Sunday.
# "At 00:00 on Sunday."
- cron: "0 0 * * 0"
workflow_dispatch:

Expand All @@ -27,62 +27,24 @@ jobs:
with:
node-version: 22

- name: Install CDK
run: |
npm install -g aws-cdk-local aws-cdk
cdklocal --version

- name: Install dependencies
run: |
yarn
npm install

- name: Start LocalStack
env:
LOCALSTACK_AUTH_TOKEN: ${{ secrets.LOCALSTACK_AUTH_TOKEN }}
run: |
pip install localstack awscli-local[ver1]
pip install terraform-local
docker pull localstack/localstack-pro:latest
# Start LocalStack in the background
EXTRA_CORS_ALLOWED_ORIGINS=* DEBUG=1 LOCALSTACK_AUTH_TOKEN=$LOCALSTACK_AUTH_TOKEN localstack start -d
# Wait 30 seconds for the LocalStack container to become ready before timing out
echo "Waiting for LocalStack startup..."
localstack wait -t 15
localstack wait -t 30
echo "Startup complete"

- name: Deploy using CDK
run: |
cdklocal bootstrap aws://000000000000/us-east-1
cdklocal deploy --require-approval never --outputs-file cdk_outputs.json

- name: List the resources
run: |
awslocal lambda list-functions
awslocal sqs list-queues
awslocal dynamodb list-tables

- name: Send Messages
run: |
front_queue_url=$(awslocal sqs list-queues | jq -r .QueueUrls[0])
awslocal sqs send-message-batch --queue-url $front_queue_url --entries file://test/testMessagesFirst.json
awslocal sqs send-message-batch --queue-url $front_queue_url --entries file://test/testMessagesSecond.json
awslocal sqs send-message-batch --queue-url $front_queue_url --entries file://test/testMessagesThird.json

awslocal sqs send-message-batch --queue-url $front_queue_url --entries file://test/cornerCase1.json
awslocal sqs send-message-batch --queue-url $front_queue_url --entries file://test/cornerCase2.json


- name: Run tests
- name: Run integration tests
run: |
yarn test
endpoint_url=$(cat cdk_outputs.json | jq -r .FriendMicroservicesStack[])
result_player_1=$(curl -X GET "$endpoint_url"friends/player1 | jq -r .[0].friend_id)
result_player_2=$(curl -X GET "$endpoint_url"friends/player2 | jq -r .[0].friend_id)
if [[ $result_player_1 != "player2" ]] || [[ $result_player_2 != "player1" ]]; then
echo "unexpected response"
echo $result_player_1 $result_player_2
exit 1
fi
./run-integ-tests.sh

- name: Send a Slack notification
if: failure() || github.event_name != 'pull_request'
Expand Down
13 changes: 13 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,18 @@ jspm_packages/
# TypeScript cache
*.tsbuildinfo

# Sources
lambda/**/*.js
lambda/**/*.d.ts
integ-tests/**/*.js
integ-tests/**/*.d.ts
bin/**/*.js
bin/**/*.d.ts
lib/**/*.js
lib/**/*.d.ts
models/**/*.js
models/**/*.d.ts

# Optional npm cache directory
.npm

Expand Down Expand Up @@ -108,3 +120,4 @@ cdk.context.json
.cdk.staging/
cdk.out/
*.tabl.json
*.snapshot
165 changes: 165 additions & 0 deletions integ-tests/integ.friend-microservices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/// !cdk-integ FriendMicroservicesIntegStack
import "source-map-support/register";
import { IntegTest, ExpectedResult } from "@aws-cdk/integ-tests-alpha";
import { App, Duration } from "aws-cdk-lib";
import { FriendMicroservicesStack } from "../lib/friend-microservices-stack";

const app = new App();

// Stack under test
// NOTE: Do NOT set explicit `env` - integ-runner provides CDK_DEFAULT_ACCOUNT
// and CDK_DEFAULT_REGION, and the assertion stack must share the same environment.
const stackUnderTest = new FriendMicroservicesStack(
app,
"FriendMicroservicesIntegStack"
);

// Create integration test
const integ = new IntegTest(app, "FriendMicroservicesIntegTest", {
testCases: [stackUnderTest],
cdkCommandOptions: {
destroy: {
args: {
force: true,
},
},
},
regions: ["us-east-1"],
});

// ============================================================================
// Test 1: Send a friend request via SQS and verify the DynamoDB record
// ============================================================================

// Send a "Request" action to the SQS queue
const sendFriendRequest = integ.assertions.awsApiCall("SQS", "sendMessage", {
QueueUrl: stackUnderTest.queueUrl,
MessageBody: JSON.stringify({
player_id: "player-integ-1",
friend_id: "player-integ-2",
friend_action: "Request",
}),
});

// Wait for the Lambda to process and write to DynamoDB
// The frontHandler reads from SQS and writes a "Requested" record
const verifyDynamoDbRecord = integ.assertions
.awsApiCall("DynamoDB", "getItem", {
TableName: stackUnderTest.tableName,
Key: {
player_id: { S: "player-integ-1" },
friend_id: { S: "player-integ-2" },
},
})
.expect(
ExpectedResult.objectLike({
Item: {
player_id: { S: "player-integ-1" },
friend_id: { S: "player-integ-2" },
state: { S: "Requested" },
},
})
)
.waitForAssertions({
totalTimeout: Duration.minutes(2),
interval: Duration.seconds(10),
});

// Chain: send request → verify DynamoDB
sendFriendRequest.next(verifyDynamoDbRecord);

// ============================================================================
// Test 2: Verify the requestStateHandler created the reverse "Pending" record
// (DynamoDB Stream → requestStateHandler creates player-integ-2 → player-integ-1 as Pending)
// ============================================================================

const verifyPendingRecord = integ.assertions
.awsApiCall("DynamoDB", "getItem", {
TableName: stackUnderTest.tableName,
Key: {
player_id: { S: "player-integ-2" },
friend_id: { S: "player-integ-1" },
},
})
.expect(
ExpectedResult.objectLike({
Item: {
player_id: { S: "player-integ-2" },
friend_id: { S: "player-integ-1" },
state: { S: "Pending" },
},
})
)
.waitForAssertions({
totalTimeout: Duration.minutes(2),
interval: Duration.seconds(10),
});

// Chain: verify DynamoDB requested record → verify pending record
verifyDynamoDbRecord.next(verifyPendingRecord);

// ============================================================================
// Test 3: Accept the friend request and verify both records become "Friends"
// ============================================================================

// Send an "Accept" action for player-integ-2 accepting player-integ-1
const sendAccept = integ.assertions.awsApiCall("SQS", "sendMessage", {
QueueUrl: stackUnderTest.queueUrl,
MessageBody: JSON.stringify({
player_id: "player-integ-2",
friend_id: "player-integ-1",
friend_action: "Accept",
}),
});

// Verify player-integ-2's record is now "Friends"
const verifyAcceptedRecord = integ.assertions
.awsApiCall("DynamoDB", "getItem", {
TableName: stackUnderTest.tableName,
Key: {
player_id: { S: "player-integ-2" },
friend_id: { S: "player-integ-1" },
},
})
.expect(
ExpectedResult.objectLike({
Item: {
player_id: { S: "player-integ-2" },
friend_id: { S: "player-integ-1" },
state: { S: "Friends" },
},
})
)
.waitForAssertions({
totalTimeout: Duration.minutes(2),
interval: Duration.seconds(10),
});

// Verify the acceptStateHandler also updated player-integ-1's record to "Friends"
const verifyReverseAccepted = integ.assertions
.awsApiCall("DynamoDB", "getItem", {
TableName: stackUnderTest.tableName,
Key: {
player_id: { S: "player-integ-1" },
friend_id: { S: "player-integ-2" },
},
})
.expect(
ExpectedResult.objectLike({
Item: {
player_id: { S: "player-integ-1" },
friend_id: { S: "player-integ-2" },
state: { S: "Friends" },
},
})
)
.waitForAssertions({
totalTimeout: Duration.minutes(2),
interval: Duration.seconds(10),
});

// Chain: verify pending → send accept → verify accepted → verify reverse accepted
verifyPendingRecord
.next(sendAccept)
.next(verifyAcceptedRecord)
.next(verifyReverseAccepted);
3 changes: 3 additions & 0 deletions integ.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"parallelRegions": ["us-east-1"]
}
8 changes: 0 additions & 8 deletions jest.config.js

This file was deleted.

23 changes: 21 additions & 2 deletions lib/friend-microservices-stack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,21 @@ const friendPk = keyMap.get(Friend)!.get(Keys.PK)!;
const friendSk = keyMap.get(Friend)!.get(Keys.SK)!;

export class FriendMicroservicesStack extends Stack {
public readonly queueUrl: string;
public readonly tableName: string;
public readonly apiUrl: string;

constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const functionProp: NodejsFunctionProps = {
runtime: Runtime.NODEJS_18_X,
runtime: Runtime.NODEJS_22_X,
timeout: cdk.Duration.seconds(10000),
memorySize: 1024,
bundling: {
// aws-sdk v2 is NOT in the Node 22 Lambda runtime, must be bundled
externalModules: [],
},
};

const frontHandler = new NodejsFunction(this, "frontHandler", {
Expand Down Expand Up @@ -102,7 +110,9 @@ export class FriendMicroservicesStack extends Stack {
friendTable.grantWriteData(unfriendStateHandler);
friendTable.grantReadData(readHandler);

const frontQueue = new Queue(this, "frontQueue");
const frontQueue = new Queue(this, "frontQueue", {
queueName: cdk.PhysicalName.GENERATE_IF_NEEDED,
});
frontHandler.addEventSource(
new SqsEventSource(frontQueue, {
reportBatchItemFailures: true,
Expand Down Expand Up @@ -199,5 +209,14 @@ export class FriendMicroservicesStack extends Stack {
.addResource("{playerId}")
.addResource("{friendId}")
.addMethod("GET");

// Expose resource identifiers for integration tests
this.queueUrl = frontQueue.queueUrl;
this.tableName = friendTable.tableName;
this.apiUrl = readAPI.url;

new cdk.CfnOutput(this, "QueueUrl", { value: frontQueue.queueUrl });
new cdk.CfnOutput(this, "TableName", { value: friendTable.tableName });
new cdk.CfnOutput(this, "ApiUrl", { value: readAPI.url });
}
}
26 changes: 13 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,25 +7,25 @@
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
"cdk": "cdk",
"cdklocal": "cdklocal",
"integ-test": "./run-integ-tests.sh"
},
"devDependencies": {
"@types/aws-lambda": "^8.10.101",
"@types/jest": "^28.1.5",
"@types/node": "18.0.4",
"aws-cdk": "2.54.0",
"jest": "^28.1.3",
"prettier": "^2.7.1",
"ts-jest": "^28.0.6",
"ts-node": "^10.9.1",
"typescript": "~4.7.4"
"@aws-cdk/integ-runner": "latest",
"@aws-cdk/integ-tests-alpha": "latest",
"@types/aws-lambda": "^8.10.145",
"@types/node": "^22.0.0",
"aws-cdk": "latest",
"aws-cdk-local": "^3.0.0",
"ts-node": "^10.9.2",
"typescript": "~5.5.0"
},
"dependencies": {
"aigle": "^1.14.1",
"aws-cdk-lib": "2.54.0",
"aws-cdk-lib": "latest",
"aws-sdk": "^2.1174.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.16"
"source-map-support": "^0.5.21"
}
}
Loading
Loading