Skip to content

Commit 6c9903c

Browse files
committed
Add out-of-process component tests for order-service and order-history-service
- OrderServiceOutOfProcessComponentTest: Uses ServiceContainer with PostgreSQL - OrderHistoryServiceOutOfProcessComponentTest: Uses ServiceContainer with LocalStack DynamoDB - Move Dockerfiles to *-main directories - Add TramNoopDuplicateMessageDetectorConfiguration to OrderHistoryServiceMain - Add OrderServiceWithRepositoriesConfiguration to OrderServiceMain Co-authored by Claude Code
1 parent 63bc387 commit 6c9903c

8 files changed

Lines changed: 300 additions & 6 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
ARG baseImageVersion
2+
FROM eventuateio/eventuate-examples-docker-images-spring-example-base-image:$baseImageVersion
3+
COPY build/libs/order-history-service-main-0.1.0-SNAPSHOT.jar service.jar
4+
HEALTHCHECK --interval=3s --retries=30 CMD curl -f http://localhost:8080/actuator/health || exit 1

ftgo-order-history-service/order-history-service-main/build.gradle

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
plugins {
22
id 'org.springframework.boot'
33
id 'io.eventuate.plugins.gradle.testing.integration-tests'
4+
id 'io.eventuate.plugins.gradle.testing.component-tests'
45
}
56

67
dependencies {
@@ -23,6 +24,22 @@ dependencies {
2324
integrationTestImplementation 'io.eventuate.common:eventuate-common-testcontainers'
2425
integrationTestImplementation 'io.eventuate.tram.core:eventuate-tram-spring-in-memory'
2526
integrationTestImplementation 'io.eventuate.tram.core:eventuate-tram-test-util'
27+
28+
// Component test dependencies
29+
componentTestImplementation 'org.springframework.boot:spring-boot-starter-test'
30+
componentTestImplementation 'io.rest-assured:rest-assured'
31+
componentTestImplementation 'org.testcontainers:localstack'
32+
componentTestImplementation 'org.testcontainers:junit-jupiter'
33+
componentTestImplementation 'org.testcontainers:testcontainers'
34+
componentTestImplementation 'io.eventuate.messaging.kafka:eventuate-messaging-kafka-testcontainers'
35+
componentTestImplementation 'io.eventuate.common:eventuate-common-testcontainers'
36+
componentTestImplementation 'io.eventuate.tram.core:eventuate-tram-spring-consumer-kafka'
37+
componentTestImplementation 'io.eventuate.tram.core:eventuate-tram-spring-testing-support-producer-kafka'
38+
componentTestImplementation 'io.eventuate.tram.core:eventuate-tram-test-util'
39+
componentTestImplementation 'io.eventuate.tram.core:eventuate-tram-spring-testing-support-outbox'
40+
componentTestImplementation 'io.eventuate.tram.core:eventuate-tram-testing-support'
41+
componentTestImplementation "io.eventuate.platform.testcontainer.support:eventuate-platform-testcontainer-support-service:$eventuatePlatformTestContainerSupportVersion"
42+
componentTestImplementation 'com.amazonaws:aws-java-sdk-dynamodb:1.12.767'
2643
}
2744

2845
bootJar {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
package net.chrisrichardson.ftgo.cqrs.orderhistory;
2+
3+
import com.amazonaws.auth.AWSStaticCredentialsProvider;
4+
import com.amazonaws.auth.BasicAWSCredentials;
5+
import com.amazonaws.client.builder.AwsClientBuilder;
6+
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
7+
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
8+
import com.amazonaws.services.dynamodbv2.model.AttributeDefinition;
9+
import com.amazonaws.services.dynamodbv2.model.CreateTableRequest;
10+
import com.amazonaws.services.dynamodbv2.model.GlobalSecondaryIndex;
11+
import com.amazonaws.services.dynamodbv2.model.KeySchemaElement;
12+
import com.amazonaws.services.dynamodbv2.model.KeyType;
13+
import com.amazonaws.services.dynamodbv2.model.Projection;
14+
import com.amazonaws.services.dynamodbv2.model.ProjectionType;
15+
import com.amazonaws.services.dynamodbv2.model.ProvisionedThroughput;
16+
import com.amazonaws.services.dynamodbv2.model.ScalarAttributeType;
17+
import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaNativeCluster;
18+
import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaNativeContainer;
19+
import io.eventuate.testcontainers.service.ServiceContainer;
20+
import io.restassured.RestAssured;
21+
import org.junit.jupiter.api.BeforeAll;
22+
import org.junit.jupiter.api.BeforeEach;
23+
import org.junit.jupiter.api.Test;
24+
import org.slf4j.Logger;
25+
import org.slf4j.LoggerFactory;
26+
import org.testcontainers.containers.GenericContainer;
27+
import org.testcontainers.containers.localstack.LocalStackContainer;
28+
import org.testcontainers.containers.output.Slf4jLogConsumer;
29+
import org.testcontainers.images.builder.ImageFromDockerfile;
30+
import org.testcontainers.lifecycle.Startables;
31+
import org.testcontainers.utility.DockerImageName;
32+
33+
import java.nio.file.Paths;
34+
import java.util.Arrays;
35+
36+
import static org.assertj.core.api.Assertions.assertThat;
37+
import static org.testcontainers.containers.localstack.LocalStackContainer.Service.DYNAMODB;
38+
39+
public class OrderHistoryServiceOutOfProcessComponentTest {
40+
41+
protected static Logger logger = LoggerFactory.getLogger(OrderHistoryServiceOutOfProcessComponentTest.class);
42+
private String baseUri;
43+
44+
public static EventuateKafkaNativeCluster eventuateKafkaCluster = new EventuateKafkaNativeCluster("order-history-service-oop-tests");
45+
46+
public static EventuateKafkaNativeContainer kafka = eventuateKafkaCluster.kafka
47+
.withNetworkAliases("kafka")
48+
.withReuse(false);
49+
50+
public static LocalStackContainer dynamodb = new LocalStackContainer(DockerImageName.parse("localstack/localstack:3.0"))
51+
.withServices(DYNAMODB)
52+
.withNetwork(eventuateKafkaCluster.network)
53+
.withNetworkAliases("dynamodb")
54+
.withReuse(false);
55+
56+
public static GenericContainer<?> service =
57+
new ServiceContainer(new ImageFromDockerfile()
58+
.withFileFromPath(".", Paths.get(".").toAbsolutePath())
59+
.withDockerfilePath("Dockerfile")
60+
.withBuildArg("baseImageVersion", "BUILD-15"))
61+
.withNetwork(eventuateKafkaCluster.network)
62+
.withKafka(kafka)
63+
.withEnv("AWS_DYNAMODB_ENDPOINT_URL", "http://dynamodb:4566")
64+
.withEnv("AWS_ACCESS_KEY_ID", "test")
65+
.withEnv("AWS_SECRET_ACCESS_KEY", "test")
66+
.withEnv("AWS_REGION", "us-east-1")
67+
.withReuse(false)
68+
.dependsOn(dynamodb)
69+
.withLogConsumer(new Slf4jLogConsumer(logger).withPrefix("SVC order-history-service:"));
70+
71+
@BeforeAll
72+
static void startContainers() {
73+
// Start infrastructure first
74+
Startables.deepStart(kafka, dynamodb).join();
75+
76+
// Create DynamoDB table before starting the service
77+
createDynamoDBTable();
78+
79+
// Now start the service
80+
service.start();
81+
}
82+
83+
private static void createDynamoDBTable() {
84+
String endpoint = dynamodb.getEndpointOverride(DYNAMODB).toString();
85+
AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
86+
.withEndpointConfiguration(new AwsClientBuilder.EndpointConfiguration(endpoint, dynamodb.getRegion()))
87+
.withCredentials(new AWSStaticCredentialsProvider(
88+
new BasicAWSCredentials(dynamodb.getAccessKey(), dynamodb.getSecretKey())))
89+
.build();
90+
91+
CreateTableRequest request = new CreateTableRequest()
92+
.withTableName("ftgo-order-history")
93+
.withKeySchema(new KeySchemaElement("orderId", KeyType.HASH))
94+
.withAttributeDefinitions(Arrays.asList(
95+
new AttributeDefinition("orderId", ScalarAttributeType.S),
96+
new AttributeDefinition("consumerId", ScalarAttributeType.S),
97+
new AttributeDefinition("creationDate", ScalarAttributeType.N)
98+
))
99+
.withGlobalSecondaryIndexes(new GlobalSecondaryIndex()
100+
.withIndexName("ftgo-order-history-by-consumer-id-and-creation-time")
101+
.withKeySchema(Arrays.asList(
102+
new KeySchemaElement("consumerId", KeyType.HASH),
103+
new KeySchemaElement("creationDate", KeyType.RANGE)
104+
))
105+
.withProjection(new Projection().withProjectionType(ProjectionType.ALL))
106+
.withProvisionedThroughput(new ProvisionedThroughput(3L, 3L))
107+
)
108+
.withProvisionedThroughput(new ProvisionedThroughput(3L, 3L));
109+
110+
client.createTable(request);
111+
logger.info("Created DynamoDB table: ftgo-order-history");
112+
}
113+
114+
@BeforeEach
115+
void setup() {
116+
baseUri = String.format("http://localhost:%d", service.getFirstMappedPort());
117+
}
118+
119+
@Test
120+
void shouldStart() {
121+
assertThat(service.isRunning()).isTrue();
122+
assertThat(service.getFirstMappedPort()).isNotNull();
123+
}
124+
125+
@Test
126+
void healthEndpointReturnsOk() {
127+
RestAssured.given()
128+
.baseUri(baseUri)
129+
.when()
130+
.get("/actuator/health")
131+
.then()
132+
.statusCode(200)
133+
.extract()
134+
.body()
135+
.asString()
136+
.contains("UP");
137+
}
138+
139+
@Test
140+
void shouldReturnEmptyOrdersForNonExistentConsumer() {
141+
RestAssured.given()
142+
.baseUri(baseUri)
143+
.when()
144+
.get("/orders?consumerId=999999")
145+
.then()
146+
.statusCode(200);
147+
}
148+
}

ftgo-order-history-service/order-history-service-main/src/main/java/net/chrisrichardson/ftgo/cqrs/orderhistory/main/OrderHistoryServiceMain.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.chrisrichardson.ftgo.cqrs.orderhistory.main;
22

3+
import io.eventuate.tram.spring.consumer.common.TramNoopDuplicateMessageDetectorConfiguration;
34
import io.eventuate.tram.spring.consumer.kafka.EventuateTramKafkaMessageConsumerConfiguration;
45
import net.chrisrichardson.ftgo.cqrs.orderhistory.dynamodb.OrderHistoryDynamoDBConfiguration;
56
import net.chrisrichardson.ftgo.cqrs.orderhistory.messaging.OrderHistoryServiceMessagingConfiguration;
@@ -12,7 +13,8 @@
1213
@Import({OrderHistoryWebConfiguration.class,
1314
OrderHistoryServiceMessagingConfiguration.class,
1415
OrderHistoryDynamoDBConfiguration.class,
15-
EventuateTramKafkaMessageConsumerConfiguration.class})
16+
EventuateTramKafkaMessageConsumerConfiguration.class,
17+
TramNoopDuplicateMessageDetectorConfiguration.class})
1618
public class OrderHistoryServiceMain {
1719

1820
public static void main(String[] args) {

ftgo-order-service/Dockerfile

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
ARG baseImageVersion
22
FROM eventuateio/eventuate-examples-docker-images-spring-example-base-image:$baseImageVersion
3-
COPY build/libs/ftgo-order-history-service.jar service.jar
3+
COPY build/libs/order-service-main-0.1.0-SNAPSHOT.jar service.jar
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
package net.chrisrichardson.ftgo.orderservice;
2+
3+
import io.eventuate.common.testcontainers.EventuateDatabaseContainer;
4+
import io.eventuate.common.testcontainers.EventuateVanillaPostgresContainer;
5+
import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaNativeCluster;
6+
import io.eventuate.messaging.kafka.testcontainers.EventuateKafkaNativeContainer;
7+
import io.eventuate.testcontainers.service.ServiceContainer;
8+
import io.eventuate.tram.spring.testing.outbox.commands.CommandOutboxTestSupport;
9+
import io.eventuate.tram.spring.testing.outbox.commands.CommandOutboxTestSupportConfiguration;
10+
import io.eventuate.tram.testing.producer.kafka.commands.DirectToKafkaCommandProducer;
11+
import io.eventuate.tram.testing.producer.kafka.commands.EnableDirectToKafkaCommandProducer;
12+
import io.eventuate.tram.testing.producer.kafka.events.DirectToKafkaDomainEventPublisher;
13+
import io.eventuate.tram.testing.producer.kafka.events.EnableDirectToKafkaDomainEventPublisher;
14+
import io.restassured.RestAssured;
15+
import org.junit.jupiter.api.BeforeEach;
16+
import org.junit.jupiter.api.Test;
17+
import org.slf4j.Logger;
18+
import org.slf4j.LoggerFactory;
19+
import org.springframework.beans.factory.annotation.Autowired;
20+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
21+
import org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration;
22+
import org.springframework.boot.test.context.SpringBootTest;
23+
import org.springframework.context.annotation.Configuration;
24+
import org.springframework.context.annotation.Import;
25+
import org.springframework.test.context.DynamicPropertyRegistry;
26+
import org.springframework.test.context.DynamicPropertySource;
27+
import org.testcontainers.containers.GenericContainer;
28+
import org.testcontainers.containers.output.Slf4jLogConsumer;
29+
import org.testcontainers.images.builder.ImageFromDockerfile;
30+
import org.testcontainers.lifecycle.Startables;
31+
32+
import java.nio.file.Paths;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
@SpringBootTest(classes = OrderServiceOutOfProcessComponentTest.TestConfiguration.class)
37+
public class OrderServiceOutOfProcessComponentTest {
38+
39+
protected static Logger logger = LoggerFactory.getLogger(OrderServiceOutOfProcessComponentTest.class);
40+
private String baseUri;
41+
42+
@Configuration
43+
@EnableAutoConfiguration(exclude = {JpaRepositoriesAutoConfiguration.class})
44+
@Import({CommandOutboxTestSupportConfiguration.class})
45+
@EnableDirectToKafkaCommandProducer
46+
@EnableDirectToKafkaDomainEventPublisher
47+
static class TestConfiguration {
48+
}
49+
50+
public static EventuateKafkaNativeCluster eventuateKafkaCluster = new EventuateKafkaNativeCluster("order-service-oop-tests");
51+
52+
public static EventuateKafkaNativeContainer kafka = eventuateKafkaCluster.kafka
53+
.withNetworkAliases("kafka")
54+
.withReuse(false);
55+
56+
public static EventuateDatabaseContainer<?> database = new EventuateVanillaPostgresContainer()
57+
.withNetwork(eventuateKafkaCluster.network)
58+
.withNetworkAliases("database")
59+
.withReuse(false);
60+
61+
public static GenericContainer<?> service =
62+
new ServiceContainer(new ImageFromDockerfile()
63+
.withFileFromPath(".", Paths.get(".").toAbsolutePath())
64+
.withDockerfilePath("Dockerfile")
65+
.withBuildArg("baseImageVersion", "BUILD-15"))
66+
.withNetwork(eventuateKafkaCluster.network)
67+
.withDatabase(database)
68+
.withKafka(kafka)
69+
.withEnv("SPRING_PROFILES_ACTIVE", "postgres")
70+
.withEnv("SPRING_JPA_HIBERNATE_DDL_AUTO", "update")
71+
.withReuse(false)
72+
.withLogConsumer(new Slf4jLogConsumer(logger).withPrefix("SVC order-service:"));
73+
74+
@Autowired
75+
private DirectToKafkaCommandProducer commandProducer;
76+
77+
@Autowired
78+
private CommandOutboxTestSupport commandOutboxTestSupport;
79+
80+
@Autowired
81+
private DirectToKafkaDomainEventPublisher directToKafkaDomainEventPublisher;
82+
83+
@DynamicPropertySource
84+
static void registerProperties(DynamicPropertyRegistry registry) {
85+
Startables.deepStart(kafka, database, service).join();
86+
87+
kafka.registerProperties(registry::add);
88+
database.registerProperties(registry::add);
89+
}
90+
91+
@BeforeEach
92+
void setup() {
93+
baseUri = String.format("http://localhost:%d", service.getFirstMappedPort());
94+
}
95+
96+
@Test
97+
void shouldStart() {
98+
assertThat(service.isRunning()).isTrue();
99+
assertThat(service.getFirstMappedPort()).isNotNull();
100+
}
101+
102+
@Test
103+
void healthEndpointReturnsOk() {
104+
RestAssured.given()
105+
.baseUri(baseUri)
106+
.when()
107+
.get("/actuator/health")
108+
.then()
109+
.statusCode(200)
110+
.extract()
111+
.body()
112+
.asString()
113+
.contains("UP");
114+
}
115+
116+
@Test
117+
void shouldReturn404ForNonExistentOrder() {
118+
RestAssured.given()
119+
.baseUri(baseUri)
120+
.when()
121+
.get("/orders/999999")
122+
.then()
123+
.statusCode(404);
124+
}
125+
}

ftgo-order-service/order-service-main/src/main/java/net/chrisrichardson/ftgo/orderservice/main/OrderServiceMain.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package net.chrisrichardson.ftgo.orderservice.main;
22

33
import io.eventuate.tram.spring.jdbckafka.TramJdbcKafkaConfiguration;
4+
import net.chrisrichardson.ftgo.orderservice.domain.OrderServiceWithRepositoriesConfiguration;
45
import net.chrisrichardson.ftgo.orderservice.messaging.OrderServiceMessagingConfiguration;
56
import net.chrisrichardson.ftgo.orderservice.service.OrderCommandHandlersConfiguration;
67
import net.chrisrichardson.ftgo.orderservice.web.OrderWebConfiguration;
@@ -10,7 +11,7 @@
1011

1112
@SpringBootApplication
1213
@Import({OrderWebConfiguration.class, OrderCommandHandlersConfiguration.class, OrderServiceMessagingConfiguration.class,
13-
TramJdbcKafkaConfiguration.class})
14+
TramJdbcKafkaConfiguration.class, OrderServiceWithRepositoriesConfiguration.class})
1415
public class OrderServiceMain {
1516

1617
public static void main(String[] args) {

0 commit comments

Comments
 (0)