Skip to content

Commit 0b999f2

Browse files
authored
implement tests with the testcontainers library (#10)
* get base test setup working * get tests to a working stage * format the code * run tests on gha ci * remove .DS_Store * try fixing the tests * shift logs step to higher up * rename the ci job * rename the ci job
1 parent f9c7c1c commit 0b999f2

12 files changed

+1001
-5
lines changed

.github/workflows/ci.yml

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Create Infra on LocalStack
1+
name: Deploy with Terraform
22

33
on:
44
push:
@@ -15,8 +15,8 @@ on:
1515
workflow_dispatch:
1616

1717
jobs:
18-
infrastructure-check:
19-
name: Setup infrastructure using Terraform
18+
test:
19+
name: Run Integration Tests
2020
runs-on: ubuntu-latest
2121
steps:
2222
- name: Checkout
@@ -81,6 +81,18 @@ jobs:
8181
run: |
8282
localstack logs
8383
84+
- name: Stop LocalStack
85+
run: |
86+
localstack stop
87+
88+
- name: Run Testcontainers tests
89+
env:
90+
AWS_ACCESS_KEY_ID: test
91+
AWS_SECRET_ACCESS_KEY: test
92+
AWS_REGION: us-east-1
93+
run: |
94+
mvn test -Dtest=dev.ancaghenade.shipmentlistdemo.integrationtests.ShipmentServiceIntegrationTest
95+
8496
- name: Send a Slack notification
8597
if: failure() || github.event_name != 'pull_request'
8698
uses: ravsamhq/notify-slack-action@v2

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ target/
33
!.mvn/wrapper/maven-wrapper.jar
44
!**/src/main/**/target/
55
!**/src/test/**/target/
6+
.DS_Store
67

78
### STS ###
89
.apt_generated

pom.xml

+42
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
<maven.compiler.source>17</maven.compiler.source>
2323
<maven.compiler.target>17</maven.compiler.target>
2424
<maven-checkstyle-plugin.version>3.2.0</maven-checkstyle-plugin.version>
25+
<testcontainers.version>1.19.7</testcontainers.version>
2526

2627
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
2728
</properties>
@@ -69,16 +70,57 @@
6970
<artifactId>spring-boot-starter-test</artifactId>
7071
<scope>test</scope>
7172
</dependency>
73+
<dependency>
74+
<groupId>org.testcontainers</groupId>
75+
<artifactId>testcontainers</artifactId>
76+
<version>${testcontainers.version}</version>
77+
<scope>test</scope>
78+
</dependency>
7279
<dependency>
7380
<groupId>org.json</groupId>
7481
<artifactId>json</artifactId>
7582
<version>20231013</version>
7683
</dependency>
7784

85+
<!-- LocalStack TestContainers -->
86+
<dependency>
87+
<groupId>org.testcontainers</groupId>
88+
<artifactId>localstack</artifactId>
89+
<version>${testcontainers.version}</version>
90+
<scope>test</scope>
91+
</dependency>
92+
<dependency>
93+
<groupId>org.testcontainers</groupId>
94+
<artifactId>junit-jupiter</artifactId>
95+
<version>${testcontainers.version}</version>
96+
<scope>test</scope>
97+
</dependency>
98+
99+
<!-- AWS SDK v2 Dependencies -->
100+
<dependency>
101+
<groupId>software.amazon.awssdk</groupId>
102+
<artifactId>lambda</artifactId>
103+
</dependency>
104+
<dependency>
105+
<groupId>software.amazon.awssdk</groupId>
106+
<artifactId>iam</artifactId>
107+
</dependency>
108+
<dependency>
109+
<groupId>software.amazon.awssdk</groupId>
110+
<artifactId>sns</artifactId>
111+
</dependency>
112+
78113
</dependencies>
79114

80115
<dependencyManagement>
81116
<dependencies>
117+
<dependency>
118+
<groupId>org.testcontainers</groupId>
119+
<artifactId>testcontainers-bom</artifactId>
120+
<version>${testcontainers.version}</version>
121+
<type>pom</type>
122+
<scope>import</scope>
123+
</dependency>
82124
<dependency>
83125
<groupId>software.amazon.awssdk</groupId>
84126
<artifactId>bom</artifactId>

src/test/java/dev/ancaghenade/shipmentlistdemo/ShipmentListDemoApplicationTests.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
class ShipmentListDemoApplicationTests {
88

99
@Test
10-
void contextLoads() {
11-
}
10+
void contextLoads() {}
1211

1312
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
package dev.ancaghenade.shipmentlistdemo.integrationtests;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
5+
import static org.junit.jupiter.api.Assertions.assertTrue;
6+
7+
import java.io.IOException;
8+
import java.nio.file.Files;
9+
import java.nio.file.Path;
10+
import java.security.MessageDigest;
11+
import java.security.NoSuchAlgorithmException;
12+
import java.util.Map;
13+
import org.junit.jupiter.api.BeforeAll;
14+
import org.junit.jupiter.api.MethodOrderer;
15+
import org.junit.jupiter.api.Order;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.TestMethodOrder;
18+
import org.springframework.core.io.ByteArrayResource;
19+
import org.springframework.http.HttpEntity;
20+
import org.springframework.http.HttpHeaders;
21+
import org.springframework.http.HttpMethod;
22+
import org.springframework.http.HttpStatus;
23+
import org.springframework.http.MediaType;
24+
import org.springframework.http.ResponseEntity;
25+
import org.springframework.test.context.ActiveProfiles;
26+
import org.springframework.util.LinkedMultiValueMap;
27+
import org.springframework.util.MultiValueMap;
28+
import software.amazon.awssdk.services.dynamodb.model.AttributeValue;
29+
import software.amazon.awssdk.services.dynamodb.model.GetItemRequest;
30+
import software.amazon.awssdk.services.s3.model.GetObjectRequest;
31+
import software.amazon.awssdk.services.s3.model.NoSuchKeyException;
32+
33+
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
34+
public class LambdaIntegrationTest extends LocalStackSetupConfigurations {
35+
36+
@BeforeAll
37+
public static void setup() throws IOException, InterruptedException, org.json.JSONException {
38+
LocalStackSetupConfigurations.setupConfig();
39+
localStack.followOutput(logConsumer);
40+
41+
createClients();
42+
43+
createS3Bucket();
44+
createDynamoDBResources();
45+
createIAMRole();
46+
createLambdaResources();
47+
createBucketNotificationConfiguration();
48+
createSNS();
49+
createSQS();
50+
createSNSSubscription();
51+
52+
lambdaClient.close();
53+
snsClient.close();
54+
sqsClient.close();
55+
iamClient.close();
56+
57+
}
58+
59+
@Test
60+
@Order(1)
61+
void testFileAddWatermarkInLambda() {
62+
63+
// prepare the file to upload
64+
var imageData = new byte[0];
65+
try {
66+
imageData = Files.readAllBytes(Path.of("src/test/java/resources/cat.jpg"));
67+
} catch (IOException e) {
68+
e.printStackTrace();
69+
}
70+
var resource = new ByteArrayResource(imageData) {
71+
@Override
72+
public String getFilename() {
73+
return "cat.jpg";
74+
}
75+
};
76+
77+
var originalHash = applyHash(imageData);
78+
79+
var shipmentId = "3317ac4f-1f9b-4bab-a974-4aa9876d5547";
80+
// build the URL with the id as a path variable
81+
var postUrl = "/api/shipment/" + shipmentId + "/image/upload";
82+
var getUrl = "/api/shipment/" + shipmentId + "/image/download";
83+
84+
// set the request headers
85+
var headers = new HttpHeaders();
86+
headers.setContentType(MediaType.MULTIPART_FORM_DATA);
87+
// request body with the file resource and headers
88+
MultiValueMap < String, Object > requestBody = new LinkedMultiValueMap < > ();
89+
requestBody.add("file", resource);
90+
HttpEntity < MultiValueMap < String, Object >> requestEntity = new HttpEntity < > (requestBody,
91+
headers);
92+
93+
ResponseEntity < String > postResponse = restTemplate.exchange(BASE_URL + postUrl,
94+
HttpMethod.POST, requestEntity, String.class);
95+
96+
assertEquals(HttpStatus.OK, postResponse.getStatusCode());
97+
98+
// give the Lambda time to start up and process the image
99+
try {
100+
Thread.sleep(15000);
101+
102+
} catch (InterruptedException e) {
103+
e.printStackTrace();
104+
}
105+
106+
ResponseEntity < byte[] > responseEntity = restTemplate.exchange(BASE_URL + getUrl,
107+
HttpMethod.GET, null, byte[].class);
108+
109+
assertEquals(HttpStatus.OK, responseEntity.getStatusCode());
110+
111+
var watermarkHash = applyHash(responseEntity.getBody());
112+
113+
assertNotEquals(originalHash, watermarkHash);
114+
115+
}
116+
117+
@Test
118+
@Order(2)
119+
void testFileProcessedInLambdaHasMetadata() {
120+
var getItemRequest = GetItemRequest.builder()
121+
.tableName("shipment")
122+
.key(Map.of(
123+
"shipmentId",
124+
AttributeValue.builder().s("3317ac4f-1f9b-4bab-a974-4aa9876d5547").build())).build();
125+
126+
var getItemResponse = dynamoDbClient.getItem(getItemRequest);
127+
128+
dynamoDbClient.getItem(getItemRequest);
129+
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
130+
.bucket(BUCKET_NAME)
131+
.key(getItemResponse.item().get("imageLink").s())
132+
.build();
133+
try {
134+
// already processed objects have a metadata field added, not be processed again
135+
var s3ObjectResponse = s3Client.getObject(getObjectRequest);
136+
assertTrue(s3ObjectResponse.response().metadata().entrySet().stream().anyMatch(
137+
entry -> entry.getKey().equals("exclude-lambda") && entry.getValue().equals("true")));
138+
} catch (NoSuchKeyException noSuchKeyException) {
139+
noSuchKeyException.printStackTrace();
140+
}
141+
dynamoDbClient.close();
142+
s3Client.close();
143+
144+
}
145+
146+
private String applyHash(byte[] data) {
147+
String hashValue = null;
148+
try {
149+
var digest = MessageDigest.getInstance("SHA-256");
150+
151+
// get the hash of the byte array
152+
var hash = digest.digest(data);
153+
154+
// convert the hash bytes to a hexadecimal representation
155+
var hexString = new StringBuilder();
156+
for (byte b: hash) {
157+
var hex = Integer.toHexString(0xff & b);
158+
if (hex.length() == 1) {
159+
hexString.append('0');
160+
}
161+
hexString.append(hex);
162+
}
163+
hashValue = hexString.toString();
164+
System.out.println("Hash value: " + hashValue);
165+
} catch (NoSuchAlgorithmException e) {
166+
e.printStackTrace();
167+
}
168+
return hashValue;
169+
}
170+
}

0 commit comments

Comments
 (0)