Skip to content

Commit 5e16a3e

Browse files
committed
add full spring-boot-starter-docker-compose setup and integration test setup
1 parent 723eeb5 commit 5e16a3e

File tree

11 files changed

+110
-79
lines changed

11 files changed

+110
-79
lines changed

README.md

+12-19
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,10 @@ Here's a diagram of the setup:
6161

6262
## How to Run this Project Locally
6363

64-
The app can be built in 3 steps:
64+
The app can be built in 2 steps:
6565

66-
1. Run `docker-compose` to set up preloaded backend services (MySQL, Elasticsearch and
67-
MinIO)
68-
2. Run the Spring Boot Backend with `./gradlew bootRun`
69-
3. Run the React Frontend with `yarn install` & `yarn start`
66+
1. Run the Spring Boot Backend with `./gradlew bootRun` (this will automatically spin up mysql/elastic/minio containers)
67+
2. Run the React Frontend with `yarn install` & `yarn start`
7068

7169
---
7270

@@ -76,21 +74,12 @@ At first, we have to run the with data preloaded stateful services (MySQL, Elast
7674
MinIO) which are used by the backend. I created a docker image of each service preloaded with
7775
data, so we just have to execute the `docker-compose.yaml`.
7876

79-
```shell
80-
cd infrastructure/deployment/development
81-
docker-compose up -d
82-
```
83-
84-
Be aware: the images are x86-based. So when you're using ARM-based apple silicone machine,
85-
pay attention that emulation is activated. All images will be pulled and mounted to your device.
86-
The spring boot backend can connect to the containers now.
87-
8877
For more information on how data were collected, processed and imported look into
8978
the [infrastructure](./infrastructure/README.md)-folder.
9079

9180
---
9281

93-
### 2. Set Up Spring Boot Backend
82+
### 1. Set Up Spring Boot Backend and Stateful Services: MySQL, Elasticsearch and MinIO
9483

9584
Now we can start the Spring Boot app:
9685

@@ -99,12 +88,16 @@ Now we can start the Spring Boot app:
9988
./gradlew bootRun
10089
```
10190

91+
This will automatically pull/start the stateful containers. So pulling might take time depending
92+
on your bandwidth. For more information on how data were collected, processed and imported look into
93+
the [infrastructure](./infrastructure/README.md)-folder.
94+
10295
The backend can now be reached at port 8080 on localhost. You can test if the backend works properly by
10396
sending some http requests. Use the provided [.http](./src/main/resources/api-calls) files.
10497

10598
---
10699

107-
### 3. Set Up React Frontend
100+
### 2. Set Up React Frontend
108101

109102
Now we can run the React frontend. We have to move into the frontend-folder and build & run with yarn or npm.
110103

@@ -129,8 +122,8 @@ we use during development.
129122
- [x] Set up Elasticsearch, Photos / File Storage
130123
- [x] Deploy on Home Server with Docker-Compose
131124
- [x] enable HTTPS with reverse-proxy
132-
- [ ] simplify local development with spring-boot-starter-docker-compose
133-
- [ ] setup and add integration tests
125+
- [x] simplify local development with spring-boot-starter-docker-compose
126+
- [x] setup and add integration tests
134127
- [ ] Add Monitoring (Prometheus, Grafana, Exporters) and expose Grafana
135128
- [ ] Add Logging (ELK Stack) and expose Kibana
136129
- [ ] Create React Frontend
@@ -147,5 +140,5 @@ we use during development.
147140
- [ ] Deploy on HA K3s Home Server
148141
- [ ] Use Flux for GitOps CD
149142
- [ ] Add Integration Namespace in K3s next to the Prod Env for Testing
150-
- [ ] Add Unit / Integration Tests in BE and FE
143+
- [ ] Add Selenium for E2E-tests from FE to BE
151144
- [ ] Add more Features like Chat Functionality

build.gradle

+2-5
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ plugins {
55
id 'io.spring.dependency-management' version '1.1.4'
66
id 'com.diffplug.spotless' version '6.25.0'
77
id 'com.github.ben-manes.versions' version '0.51.0'
8-
id "org.openrewrite.rewrite" version "6.10.1"
8+
id "org.openrewrite.rewrite" version "6.11.2"
99
}
1010

1111
rewrite {
@@ -77,9 +77,6 @@ dependencies {
7777
implementation 'com.mysql:mysql-connector-j:8.3.0'
7878
implementation "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}"
7979

80-
//-- Shared Cache
81-
implementation "org.springframework.boot:spring-boot-starter-data-redis:${springBootVersion}"
82-
8380
//-- SearchEngine
8481
implementation 'org.springframework.data:spring-data-elasticsearch:5.2.4'
8582
implementation 'org.eclipse.parsson:parsson:1.1.5' // fix transitive vulnerability
@@ -117,7 +114,7 @@ dependencies {
117114
testImplementation "org.testcontainers:junit-jupiter:${testContainersVersion}"
118115
testImplementation "org.testcontainers:mysql:${testContainersVersion}"
119116
testImplementation "org.testcontainers:elasticsearch:${testContainersVersion}"
120-
implementation "org.testcontainers:minio:${testContainersVersion}"
117+
testImplementation "org.testcontainers:minio:${testContainersVersion}"
121118

122119
if (System.getProperty('os.name').toLowerCase().contains('mac')) {
123120
runtimeOnly 'io.netty:netty-resolver-dns-native-macos:4.1.108.Final:osx-aarch_64'

compose.yaml

+42-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
version: '3.8'
21
services:
32

43
### ----------------------- MySQL ----------------------- ###
5-
mysql:
4+
imdb-clone-mysql:
65
container_name: imdb-clone-mysql
7-
image: mysql:latest
8-
restart: always
6+
image: mysql:8.3.0
7+
restart: unless-stopped
98
environment:
109
- MYSQL_DATABASE=movie_db
1110
- MYSQL_ROOT_PASSWORD=supersecret
@@ -18,6 +17,45 @@ services:
1817
networks:
1918
- imdb-clone-network
2019

20+
imdb-clone-elasticsearch:
21+
container_name: imdb-clone-elasticsearch
22+
image: docker.elastic.co/elasticsearch/elasticsearch:8.13.0
23+
restart: unless-stopped
24+
environment:
25+
- xpack.security.enabled=false
26+
- discovery.type=single-node
27+
- ES_JAVA_OPTS=-Xms512m -Xmx2g
28+
ports:
29+
- "9200:9200"
30+
volumes:
31+
- imdb-clone-elasticsearch-data:/usr/share/elasticsearch/data
32+
networks:
33+
- imdb-clone-network
34+
35+
### ----------------------- MinIO ----------------------- ###
36+
imdb-clone-minio:
37+
container_name: imdb-clone-minio
38+
image: bitnami/minio:2024.3.26
39+
restart: unless-stopped
40+
user: root
41+
environment:
42+
- MINIO_ROOT_USER=ROOTNAME
43+
- MINIO_ROOT_PASSWORD=CHANGEME123
44+
ports:
45+
- "9000:9000"
46+
- "9090:9090"
47+
volumes:
48+
- imdb-clone-minio-data:/data
49+
networks:
50+
- imdb-clone-network
51+
command: /opt/bitnami/minio/bin/minio server --console-address :9090 /data
52+
53+
volumes:
54+
imdb-clone-elasticsearch-data:
55+
name: imdb-clone-elasticsearch-data
56+
imdb-clone-minio-data:
57+
name: imdb-clone-minio-data
58+
2159
networks:
2260
imdb-clone-network:
2361
name: imdb-clone-network

src/main/java/com/thecodinglab/imdbclone/service/impl/ElasticSearchServiceImpl.java

+1-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ public void indexMovie(Movie movie) {
5959
} catch (IOException e) {
6060
logger.error(
6161
"Document of type movie with [{}] was not indexed successfully.", kv(MOVIE_ID,movie.getId()));
62-
// throw new RuntimeException(e);
63-
throw new ElasticsearchOperationException("Document of type movie with [{}] was not indexed successfully.", e);
62+
throw new ElasticsearchOperationException("Document of type movie with id [%s] was not indexed successfully.".formatted(movie.getId()), e);
6463
}
6564
}
6665

src/main/java/com/thecodinglab/imdbclone/service/impl/FileStorageServiceImpl.java

+10-10
Original file line numberDiff line numberDiff line change
@@ -167,8 +167,8 @@ public String storeFile(InputStream file, int fileSize, String fileName, String
167167
.stream(file, fileSize, -1)
168168
.build());
169169
return "Image was stored with etag [" + resp.etag() + "]";
170-
} catch (Exception e) {
171-
throw new MinioOperationException("Error while storing file in MinIO", e);
170+
} catch (Exception ex) {
171+
throw new MinioOperationException("Error while storing file in MinIO", ex);
172172
}
173173
}
174174

@@ -194,9 +194,9 @@ public void setUpBucket() {
194194
createBucketPolicyFrom(bucketPolicy);
195195
logger.info("bucket [{}] was created and bucketPolicy set successfully", bucketName);
196196

197-
} catch (Exception e) {
197+
} catch (Exception ex) {
198198
logger.error("Creation of bucket [{}] failed", bucketName);
199-
throw new MinioOperationException("Error while creating bucket in MinIO", e);
199+
throw new MinioOperationException("Error while creating bucket in MinIO", ex);
200200
}
201201
}
202202

@@ -205,9 +205,9 @@ private void createBucketPolicyFrom(String bucketPolicy) {
205205
try {
206206
minioClient.setBucketPolicy(
207207
SetBucketPolicyArgs.builder().bucket(bucketName).config(policyConfig).build());
208-
} catch (Exception e) {
208+
} catch (Exception ex) {
209209
logger.error("Creation of bucket policy failed");
210-
throw new MinioOperationException("Error while creating bucket policy in MinIO", e);
210+
throw new MinioOperationException("Error while creating bucket policy in MinIO", ex);
211211
}
212212
}
213213

@@ -227,9 +227,9 @@ private String readResourceFile(String resourcePath) {
227227
}
228228
return stringBuilder.toString();
229229
}
230-
} catch (IOException e) {
230+
} catch (IOException ex) {
231231
logger.error("Reading file with path [{}] failed", resourcePath);
232-
throw new MinioOperationException("Error while reading policy config", e);
232+
throw new MinioOperationException("Error while reading policy config", ex);
233233
}
234234
}
235235

@@ -246,9 +246,9 @@ public String generateUrl(String imageName) {
246246
.expiry(60 * 60 * 24)
247247
.build());
248248
return presignedUrl;
249-
} catch (Exception e) {
249+
} catch (Exception ex) {
250250
logger.error("Generate presigned object URL file with image name [{}] failed", imageName);
251-
throw new MinioOperationException("Error while generating presigned URL in MinIO", e);
251+
throw new MinioOperationException("Error while generating presigned URL in MinIO", ex);
252252
}
253253
}
254254
}

src/main/resources/sql/init.sql renamed to src/main/resources/sql/1_init_schema.sql

+5-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
-- Attempt to create the database if it doesn't already exist
2+
create database if not exists movie_db;
3+
use movie_db;
4+
15
-- Drop tables to clean
26
drop table if exists comment;
37
drop table if exists watched_movie;
@@ -109,9 +113,6 @@ create table verification_token (
109113
);
110114

111115

112-
-- Insert records into the tables
116+
-- Mandatory Inserts
113117
insert into role (name) values('ROLE_ADMIN');
114118
insert into role (name) values('ROLE_USER');
115-
116-
# insert into movie(id, primary_title, original_title, start_year, end_year, runtime_minutes, created_at_in_utc, modified_at_in_utc, movie_genre, movie_type, imdb_rating, imdb_rating_count, adult, rating, rating_count, description, image_url_token)
117-
# values(2872718,'Nightcrawler','Nightcrawler',2014,null,117,'2023-07-31 06:07:12','2023-07-31 06:07:12',16409,1,7.8,528339,0,null,0,'When Lou Bloom, desperate for work, muscles into the world of L.A. crime journalism, he blurs the line between observer and participant to become the star of his own story. Aiding him in his effort is Nina, a TV-news veteran.','9BGAIYNfdY90aIkV66dIJ6Olee7JGn');
+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
2+
insert into movie(id, primary_title, original_title, start_year, end_year, runtime_minutes, created_at_in_utc, modified_at_in_utc, movie_genre, movie_type, imdb_rating, imdb_rating_count, adult, rating, rating_count, description, image_url_token)
3+
values(2872718,'Nightcrawler','Nightcrawler',2014,null,117,'2023-07-31 06:07:12','2023-07-31 06:07:12',16409,1,7.8,528339,0,null,0,'When Lou Bloom, desperate for work, muscles into the world of L.A. crime journalism, he blurs the line between observer and participant to become the star of his own story. Aiding him in his effort is Nina, a TV-news veteran.','9BGAIYNfdY90aIkV66dIJ6Olee7JGn');
4+
5+
# add typical scenarios
6+
7+
# # test data
8+
# insert into movie_db.account(id,username,email,password,first_name,last_name,bio,phone,birthday,locked,enabled) values(1,'les_grossman','tom.cruise@yahoo.de','UnencryptedPa55worD','Tom','Cruise','I will massacre you!','01628264723', '1962-07-03 00:00:00',false,true);
9+
# insert into movie_db.account(id,username,email,password,first_name,last_name,bio,phone,birthday,locked,enabled) values(2,'jeff_portnoy','jack.black@gmail.com','UnencryptedPa55worD','jack','black','You go, girl!','015122973088', '1969-08-28 00:00:00',false,true);
10+
#
11+
# insert into movie_db.rating(rating,movie_id,account_id) values(9.2,942385,1);
12+
# insert into movie_db.rating(rating,movie_id,account_id) values(7.7,369339,1);
13+
# insert into movie_db.rating(rating,movie_id,account_id) values(7.8,942385,2);
14+
# insert into movie_db.rating(rating,movie_id,account_id) values(8.1,332379,2);
15+
#
16+
# insert into movie_db.watched_movie(movie_id,account_id) values(942385,1);
17+
# insert into movie_db.watched_movie(movie_id,account_id) values(369339,1);
18+
# insert into movie_db.watched_movie(movie_id,account_id) values(942385,1);
19+
#
20+
# insert into movie_db.comment(message,movie_id,account_id) values('What an outrageous cast!',942385,1);
21+
# insert into movie_db.comment(message,movie_id,account_id) values('The shoot was sometimes difficult.',369339,1);
22+
# insert into movie_db.comment(message,movie_id,account_id) values('All except for one ;-)',942385,2);
23+
# insert into movie_db.comment(message,movie_id,account_id) values('It was fun to play!',332379,2);

src/main/resources/sql/test-data.sql

-23
This file was deleted.

src/test/java/com/thecodinglab/imdbclone/integration/BaseContainers.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@
2020
public class BaseContainers {
2121

2222
public static MySQLContainer<?> mysqlContainer =
23-
new MySQLContainer<>(DockerImageName.parse("mysql:latest"))
24-
.withDatabaseName("testdb")
23+
new MySQLContainer<>(DockerImageName.parse("mysql:8.3.0"))
24+
.withDatabaseName("movie_db")
2525
.withUsername("test")
2626
.withPassword("test")
27-
.withInitScript("sql/init.sql");
27+
.withInitScript("sql/1_init_schema.sql");
2828

2929
@DynamicPropertySource
3030
static void mysqlProperties(DynamicPropertyRegistry registry) {
@@ -48,7 +48,7 @@ public static void populateTables(String scriptPath) {
4848

4949
static ElasticsearchContainer elasticContainer =
5050
new ElasticsearchContainer(
51-
DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:8.7.1"));
51+
DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:8.13.0"));
5252

5353
@DynamicPropertySource
5454
static void setProperties(DynamicPropertyRegistry registry) {
@@ -57,7 +57,7 @@ static void setProperties(DynamicPropertyRegistry registry) {
5757

5858
// Add this to your existing BaseContainers class
5959
public static MinIOContainer minioContainer =
60-
new MinIOContainer(DockerImageName.parse("minio/minio:latest"))
60+
new MinIOContainer(DockerImageName.parse("minio/minio:RELEASE.2024-03-26T22-10-45Z"))
6161
.withEnv("MINIO_ACCESS_KEY", "minioadmin")
6262
.withEnv("MINIO_SECRET_KEY", "minioadmin")
6363
.withCommand("server /data");

src/test/java/com/thecodinglab/imdbclone/integration/controller/SearchControllerTest.java

+10-7
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
import com.thecodinglab.imdbclone.integration.BaseContainers;
44
import com.thecodinglab.imdbclone.payload.movie.MovieSearchRequest;
5-
//import org.junit.jupiter.api.Test;
5+
import java.util.Collections;
6+
import org.junit.jupiter.api.Test;
67
import org.springframework.beans.factory.annotation.Autowired;
78
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
89
import org.springframework.boot.test.context.SpringBootTest;
@@ -15,11 +16,10 @@ class SearchControllerTest extends BaseContainers {
1516

1617
@Autowired private WebTestClient webTestClient;
1718

18-
// TODO: fix not FOUND Nightcrawler movie (although it seemed to be indexed)
19-
// @Test
19+
@Test
2020
void search_success() {
2121
// Arrange
22-
var request = new MovieSearchRequest(null, null, null, null, null, null);
22+
var request = new MovieSearchRequest(null, null, null, null, Collections.emptySet(), null);
2323

2424
// Act and Assert
2525
webTestClient
@@ -28,9 +28,12 @@ void search_success() {
2828
.bodyValue(request)
2929
.accept(MediaType.APPLICATION_JSON)
3030
.exchange()
31-
.expectStatus().isOk()
32-
.expectHeader().contentType(MediaType.APPLICATION_JSON)
31+
.expectStatus()
32+
.isOk()
33+
.expectHeader()
34+
.contentType(MediaType.APPLICATION_JSON)
3335
.expectBody()
34-
.jsonPath("$.pageNumber").isEqualTo(0);
36+
.jsonPath("$.content[0].primaryTitle")
37+
.isEqualTo("Nightcrawler");
3538
}
3639
}

0 commit comments

Comments
 (0)