From 653144b2941d98b1477e29a984ec64df4cb960ef Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 00:19:00 +0200 Subject: [PATCH 01/24] Add Spring Boot dependencies and set up PostgreSQL JDBC connection --- postify/pom.xml | 17 +++++++++-------- .../src/main/resources/application.properties | 5 +++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/postify/pom.xml b/postify/pom.xml index 7cea5f7..44043f2 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -34,12 +34,19 @@ org.springframework.boot spring-boot-starter-webmvc - + + org.springframework.boot + spring-boot-starter-jdbc + org.postgresql postgresql runtime + + org.springframework.boot + spring-boot-starter-webflux + org.projectlombok lombok @@ -51,19 +58,13 @@ test - org.springframework.boot spring-boot-maven-plugin - - - org.projectlombok - lombok - - + diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties index 8f4842f..3b5944a 100644 --- a/postify/src/main/resources/application.properties +++ b/postify/src/main/resources/application.properties @@ -1 +1,6 @@ spring.application.name=postify + +spring.datasource.url=jdbc:postgresql://localhost:5432/Bob +spring.datasource.username=postgres +spring.datasource.password=yourpassword +spring.datasource.driver-class-name=org.postgresql.Driver \ No newline at end of file From f45732a2180f7670cbd2ef67e4a01376fc5ec111 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 00:39:41 +0200 Subject: [PATCH 02/24] Remove exposed database password and implement environment variable setup --- postify/pom.xml | 6 ++++++ postify/src/main/resources/application.properties | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/postify/pom.xml b/postify/pom.xml index 44043f2..beb5d2b 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -38,6 +38,12 @@ org.springframework.boot spring-boot-starter-jdbc + + io.github.cdimascio + java-dotenv + 5.2.2 + + org.postgresql postgresql diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties index 3b5944a..cf90328 100644 --- a/postify/src/main/resources/application.properties +++ b/postify/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=postify spring.datasource.url=jdbc:postgresql://localhost:5432/Bob -spring.datasource.username=postgres -spring.datasource.password=yourpassword +spring.datasource.username=${DB_USERNAME} +spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver \ No newline at end of file From 5a1f3af76387d7aca04eeac9b49ba8c51ba7d02c Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 12:26:38 +0200 Subject: [PATCH 03/24] Change DB name in project setup --- postify/src/main/resources/application.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties index cf90328..149ea04 100644 --- a/postify/src/main/resources/application.properties +++ b/postify/src/main/resources/application.properties @@ -1,6 +1,6 @@ spring.application.name=postify -spring.datasource.url=jdbc:postgresql://localhost:5432/Bob +spring.datasource.url=jdbc:postgresql://localhost:5432/postify_db spring.datasource.username=${DB_USERNAME} spring.datasource.password=${DB_PASSWORD} spring.datasource.driver-class-name=org.postgresql.Driver \ No newline at end of file From 20fd0bd2fda5367858cae5d352e292f6e9780d58 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 12:59:41 +0200 Subject: [PATCH 04/24] Set up project structure with model, repository, service, and controller --- .../week6/postify/controller/UserStatisticsController.java | 4 ++++ .../backend/week6/postify/model/UserStatisticsResponse.java | 4 ++++ .../week6/postify/repository/UserStatisticsRepository.java | 4 ++++ .../backend/week6/postify/service/UserStatisticsService.java | 4 ++++ .../postify/controller/UserStatisticsControllerTest.java | 4 ++++ .../week6/postify/model/UserStatisticsResponseTest.java | 4 ++++ .../postify/repository/UserStatisticsRepositoryTest.java | 4 ++++ .../week6/postify/service/UserStatisticsServiceTest.java | 4 ++++ 8 files changed, 32 insertions(+) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepositoryTest.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsServiceTest.java diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java new file mode 100644 index 0000000..6fb33b3 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.controller; + +public class UserStatisticsController { +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java new file mode 100644 index 0000000..8235017 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.model; + +public class UserStatisticsResponse { +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java new file mode 100644 index 0000000..e41b20c --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.repository; + +public class UserStatisticsRepository { +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java new file mode 100644 index 0000000..be341d6 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.service; + +public class UserStatisticsService { +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java new file mode 100644 index 0000000..f1e54d1 --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.controller; + +public class UserStatisticsControllerTest { +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java new file mode 100644 index 0000000..801c3a2 --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.model; + +public class UserStatisticsResponseTest { +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepositoryTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepositoryTest.java new file mode 100644 index 0000000..18b2bbf --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepositoryTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.repository; + +public class UserStatisticsRepositoryTest { +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsServiceTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsServiceTest.java new file mode 100644 index 0000000..4c7a3f9 --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsServiceTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.service; + +public class UserStatisticsServiceTest { +} From ea8c4f543d9cadfa1a89a1ccff8f498cff040e20 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 13:11:38 +0200 Subject: [PATCH 05/24] Create UserStatisticsResponse data model --- .../week6/postify/model/UserStatisticsResponse.java | 13 ++++++++++++- .../postify/service/UserStatisticsService.java | 3 --- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java index 8235017..ee5933b 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java @@ -1,4 +1,15 @@ package net.hackyourfuture.backend.week6.postify.model; public class UserStatisticsResponse { -} + + public int userID; + public String userName; + public String userCountry; + public int totalStreams; + public int uniqueTrackStreamed; + public int uniqueArtistsStreamed; + public String favoriteGenre; + public int totalListeningTimeSeconds; + } + + diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java index be341d6..8f0d3a0 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java @@ -1,4 +1 @@ package net.hackyourfuture.backend.week6.postify.service; - -public class UserStatisticsService { -} From 0e2d640a1fc4bb7b146532734bff420847a81fcf Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 13:27:14 +0200 Subject: [PATCH 06/24] Implement UserStatisticsService with proper repository wiring --- .../postify/service/UserStatisticsService.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java index 8f0d3a0..14b7e60 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java @@ -1 +1,18 @@ package net.hackyourfuture.backend.week6.postify.service; + +import net.hackyourfuture.backend.week6.postify.model.UserStatisticsResponse; +import net.hackyourfuture.backend.week6.postify.repository.UserStatisticsRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class UserStatisticsService { + + @Autowired + private UserStatisticsRepository repo; + + public UserStatisticsResponse getStats(int userId) { + return repo.getUserStats(userId); + } + +} \ No newline at end of file From 3abef7a316de6013785109982e799df419de7be6 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 13:33:32 +0200 Subject: [PATCH 07/24] Add UserStatisticsController with statistics endpoint --- .../controller/UserStatisticsController.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java index 6fb33b3..732b1f8 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java @@ -1,4 +1,32 @@ package net.hackyourfuture.backend.week6.postify.controller; + +import net.hackyourfuture.backend.week6.postify.model.UserStatisticsResponse; +import net.hackyourfuture.backend.week6.postify.service.UserStatisticsService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + + +@RestController +@RequestMapping("/users") public class UserStatisticsController { -} + + @Autowired + private UserStatisticsService service; + + @GetMapping("/{id}/statistics") + public ResponseEntity getStats(@PathVariable int id) { + UserStatisticsResponse stats = service.getStats(id); + + if (stats == null) { + return ResponseEntity.status(404).body("User not found"); + } + + return ResponseEntity.ok(stats); + } + } + From 0852d49d50e699d254ac508c55f41e5ae0ee139e Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 15:19:29 +0200 Subject: [PATCH 08/24] Use Dotenv to inject DB_USERNAME and DB_PASSWORD at startup --- .../backend/week6/postify/PostifyApplication.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/PostifyApplication.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/PostifyApplication.java index aa5e99f..2241443 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/PostifyApplication.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/PostifyApplication.java @@ -1,5 +1,6 @@ package net.hackyourfuture.backend.week6.postify; +import io.github.cdimascio.dotenv.Dotenv; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @@ -7,6 +8,13 @@ public class PostifyApplication { public static void main(String[] args) { + + Dotenv dotenv = Dotenv.load(); + + System.setProperty("DB_USERNAME", dotenv.get("DB_USERNAME")); + System.setProperty("DB_PASSWORD", dotenv.get("DB_PASSWORD")); + + SpringApplication.run(PostifyApplication.class, args); } From 9508ba8972941fc006159f42cb99fdba94b257a3 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 15:22:19 +0200 Subject: [PATCH 09/24] Add JsonPropertyOrder to enforce response field order --- .../postify/model/UserStatisticsResponse.java | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java index ee5933b..0e03eb3 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java @@ -1,12 +1,25 @@ package net.hackyourfuture.backend.week6.postify.model; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonPropertyOrder({ + "userId", + "userName", + "userCountry", + "totalStreams", + "uniqueTracksStreamed", + "uniqueArtistsStreamed", + "favoriteGenre", + "totalListeningTimeSeconds" +}) + public class UserStatisticsResponse { - public int userID; + public int userId; public String userName; public String userCountry; public int totalStreams; - public int uniqueTrackStreamed; + public int uniqueTracksStreamed; public int uniqueArtistsStreamed; public String favoriteGenre; public int totalListeningTimeSeconds; From 2c3677ac443947893fbd0bc7f657aed6f058a485 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 15:25:16 +0200 Subject: [PATCH 10/24] Implement UserStatisticsRepository for user stats retrieval --- .../repository/UserStatisticsRepository.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java index e41b20c..e567487 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java @@ -1,4 +1,60 @@ package net.hackyourfuture.backend.week6.postify.repository; +import net.hackyourfuture.backend.week6.postify.model.UserStatisticsResponse; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.beans.factory.annotation.Autowired; +import java.util.List; + +@Repository public class UserStatisticsRepository { + + @Autowired + private JdbcTemplate jdbc; + + public UserStatisticsResponse getUserStats(int userId) { + String sql = """ + SELECT + u.user_id AS user_id, + u.user_name AS user_name, + u.user_country AS user_country, + COUNT(s.stream_id) AS total_streams, + COUNT(DISTINCT t.track_id) AS unique_tracks, + COUNT(DISTINCT ar.artist_id) AS unique_artists, + ( + SELECT t2.genre + FROM streams s2 + JOIN tracks t2 ON s2.track_id = t2.track_id + JOIN albums al2 ON t2.album_id = al2.album_id + JOIN artists ar2 ON al2.artist_id = ar2.artist_id + WHERE s2.user_id = u.user_id + GROUP BY t2.genre + ORDER BY COUNT(*) DESC + LIMIT 1 + ) AS favorite_genre, + SUM(t.track_duration_s) AS total_listening_seconds + FROM users u + LEFT JOIN streams s ON u.user_id = s.user_id + LEFT JOIN tracks t ON s.track_id = t.track_id + LEFT JOIN albums al ON t.album_id = al.album_id + LEFT JOIN artists ar ON al.artist_id = ar.artist_id + WHERE u.user_id = ? + GROUP BY u.user_id, u.user_name, u.user_country + """; + + List result = jdbc.query(sql, (rs, rowNum) -> { + UserStatisticsResponse r = new UserStatisticsResponse(); + r.userId = rs.getInt("user_id"); + r.userName = rs.getString("user_name"); + r.userCountry = rs.getString("user_country"); + r.totalStreams = rs.getInt("total_streams"); + r.uniqueTracksStreamed = rs.getInt("unique_tracks"); + r.uniqueArtistsStreamed = rs.getInt("unique_artists"); + r.favoriteGenre = rs.getString("favorite_genre"); + r.totalListeningTimeSeconds = rs.getInt("total_listening_seconds"); + return r; + }, userId); + + return result.isEmpty() ? null : result.get(0); + } } From c290957b6c2efe309104df7df50c2c3a5dd8d840 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 17:36:06 +0200 Subject: [PATCH 11/24] Update pom.xml to use Java 17 for compatibility --- postify/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/postify/pom.xml b/postify/pom.xml index beb5d2b..fec2b91 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -27,7 +27,7 @@ - 25 + 17 From 0f9d3408b06b11ed5f1d1142cfd6bc6e65eccf84 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Tue, 9 Jun 2026 18:13:39 +0200 Subject: [PATCH 12/24] Add unit test for UserStatisticsResponse fields --- .../model/UserStatisticsResponseTest.java | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java index 801c3a2..5f172f2 100644 --- a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java @@ -1,4 +1,21 @@ package net.hackyourfuture.backend.week6.postify.model; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + public class UserStatisticsResponseTest { + + @Test + void shouldContainCorrectUserStatistics() { + UserStatisticsResponse response = new UserStatisticsResponse(); + + response.userId = 1; + response.userName = "Abraham"; + response.userCountry = "CA"; + + assertEquals(1, response.userId); + assertEquals("Abraham", response.userName); + assertEquals("CA", response.userCountry); + } } From 9c134333a46abb5e0856e384168fe47d7ceaa1de Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 11:07:03 +0200 Subject: [PATCH 13/24] Add Lombok annotations for getters, setters, and constructors --- .../week6/postify/model/UserStatisticsResponse.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java index 0e03eb3..10a3a99 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java @@ -1,6 +1,10 @@ package net.hackyourfuture.backend.week6.postify.model; import com.fasterxml.jackson.annotation.JsonPropertyOrder; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; @JsonPropertyOrder({ "userId", @@ -12,7 +16,10 @@ "favoriteGenre", "totalListeningTimeSeconds" }) - +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor public class UserStatisticsResponse { public int userId; @@ -23,6 +30,7 @@ public class UserStatisticsResponse { public int uniqueArtistsStreamed; public String favoriteGenre; public int totalListeningTimeSeconds; - } + +} From ee8347f4ba8a12d85ba344a11ebff8508fac4a60 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 12:17:34 +0200 Subject: [PATCH 14/24] test: add MockMvc integration tests for UserStatisticsController --- .../UserStatisticsControllerTest.java | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java index f1e54d1..0b35692 100644 --- a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java @@ -1,4 +1,58 @@ package net.hackyourfuture.backend.week6.postify.controller; -public class UserStatisticsControllerTest { +import net.hackyourfuture.backend.week6.postify.model.UserStatisticsResponse; +import net.hackyourfuture.backend.week6.postify.service.UserStatisticsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +class UserStatisticsControllerTest { + + private MockMvc mockMvc; + + @Mock + private UserStatisticsService userStatisticsService; + + @InjectMocks + private UserStatisticsController userStatisticsController; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + mockMvc = MockMvcBuilders.standaloneSetup(userStatisticsController).build(); + } + + @Test + void shouldReturnUserStats_whenUserExists() throws Exception { + UserStatisticsResponse mockResponse = new UserStatisticsResponse( + 1, "lena_v", "NL", 65, 34, 8, "Nederpop", 14021 + ); + + when(userStatisticsService.getStats(1)).thenReturn(mockResponse); + + mockMvc.perform(get("/users/1/statistics")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.userId").value(1)) + .andExpect(jsonPath("$.userName").value("lena_v")) + .andExpect(jsonPath("$.totalStreams").value(65)); + } + + @Test + void shouldReturn404_whenUserDoesNotExist() throws Exception { + when(userStatisticsService.getStats(999)).thenReturn(null); + + mockMvc.perform(get("/users/999/statistics")) + .andExpect(status().isNotFound()); + } } + From 21c499255b49900519811a189c2440fd138efadd Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 12:19:57 +0200 Subject: [PATCH 15/24] chore: clean pom.xml and add required test dependencies --- postify/pom.xml | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/postify/pom.xml b/postify/pom.xml index fec2b91..1f5ae47 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -6,7 +6,7 @@ org.springframework.boot spring-boot-starter-parent 4.0.6 - + net.hackyourfuture.backend.week6 postify @@ -60,7 +60,25 @@ org.springframework.boot - spring-boot-starter-webmvc-test + spring-boot-starter-test + test + + + com.jayway.jsonpath + json-path + test + + + org.mockito + mockito-core + 5.11.0 + test + + + + org.mockito + mockito-junit-jupiter + 5.11.0 test From 2eacbe04d62c3652a6b7d75125d4ff20dd88b541 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 13:00:46 +0200 Subject: [PATCH 16/24] feat: add response model for track lyrics API --- .../postify/model/TrackLyricsResponse.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponse.java diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponse.java new file mode 100644 index 0000000..2c7ee56 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponse.java @@ -0,0 +1,19 @@ +package net.hackyourfuture.backend.week6.postify.model; + + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TrackLyricsResponse { + private int trackId; + private String trackTitle; + private String artistName; + private String lyrics; + +} From 8c1cf4e5c6a25c017da27a92eeac921081b95007 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 14:33:13 +0200 Subject: [PATCH 17/24] Add TrackRepository implementation --- .../postify/repository/TrackRepository.java | 44 +++++++++++++++++++ .../model/TrackLyricsResponsesTest.java | 4 ++ .../repository/TrackRepositoryTest.java | 4 ++ 3 files changed, 52 insertions(+) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepositoryTest.java diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java new file mode 100644 index 0000000..c86ef71 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java @@ -0,0 +1,44 @@ +package net.hackyourfuture.backend.week6.postify.repository; + + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + + +@Repository +public class TrackRepository { + + private final JdbcTemplate jdbcTemplate; + + public TrackRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public Optional findTrackWithArtistsById(int trackId) { + String sql = """ + SELECT + t.track_id, + t.track_title, + a.artist_name + FROM tracks t + JOIN albums al ON t.album_id = al.album_id + JOIN artists a ON al.artist_id = a.artist_id + WHERE t.track_id = ? + """; + + return jdbcTemplate.query(sql, rs -> { + if (!rs.next()) { + return Optional.empty(); + } + return Optional.of(new TrackWithArtist( + rs.getInt("track_id"), + rs.getString("track_title"), + rs.getString("artist_name") + )); + + }, trackId); + } + public record TrackWithArtist(int trackId, String trackTitle, String artistName) {} +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java new file mode 100644 index 0000000..4b4a063 --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.model; + +public class TrackLyricsResponses { +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepositoryTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepositoryTest.java new file mode 100644 index 0000000..451c569 --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepositoryTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.repository; + +public class TrackRepositoryTest { +} From 5de10825f1cdefeb77a9fa02091230f193e66e30 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:32:54 +0200 Subject: [PATCH 18/24] chore: update GlobalExceptionHandler and related exception classes --- .../postify/config/RestClientConfig.java | 4 ++ .../controller/TrackLyricsController.java | 4 ++ .../week6/postify/dto/LyricsApiResponse.java | 11 ++++ .../{model => dto}/TrackLyricsResponse.java | 0 .../exception/GlobalExceptionHandler.java | 34 +++++++++++ .../exception/LyricsNotFoundException.java | 13 ++++ .../exception/TrackNotFoundException.java | 7 +++ .../postify/service/TrackLyricsService.java | 59 +++++++++++++++++++ .../controller/TrackLyricsControllerTest.java | 4 ++ .../model/TrackLyricsResponsesTest.java | 4 -- 10 files changed, 136 insertions(+), 4 deletions(-) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java rename postify/src/main/java/net/hackyourfuture/backend/week6/postify/{model => dto}/TrackLyricsResponse.java (100%) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/GlobalExceptionHandler.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/LyricsNotFoundException.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/TrackNotFoundException.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java delete mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java new file mode 100644 index 0000000..eddc589 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.config; + +public class RestClientConfig { +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java new file mode 100644 index 0000000..83b3d15 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.controller; + +public class TrackLyricsController { +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java new file mode 100644 index 0000000..6815356 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java @@ -0,0 +1,11 @@ +package net.hackyourfuture.backend.week6.postify.model; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class LyricsApiResponse { + + private String lyrics; +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java similarity index 100% rename from postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponse.java rename to postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/GlobalExceptionHandler.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..b45e90f --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/GlobalExceptionHandler.java @@ -0,0 +1,34 @@ +package net.hackyourfuture.backend.week6.postify.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import java.util.Map; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(TrackNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Map handleTrackNotFound( + TrackNotFoundException exception) { + + return Map.of( + "message", + exception.getMessage() + ); + } + + @ExceptionHandler(LyricsNotFoundException.class) + @ResponseStatus(HttpStatus.NOT_FOUND) + public Map handleLyricsNotFound( + LyricsNotFoundException exception) { + + return Map.of( + "message", + exception.getMessage() + ); + } +} \ No newline at end of file diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/LyricsNotFoundException.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/LyricsNotFoundException.java new file mode 100644 index 0000000..cbd00f1 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/LyricsNotFoundException.java @@ -0,0 +1,13 @@ + +package net.hackyourfuture.backend.week6.postify.exception; + +public class LyricsNotFoundException extends RuntimeException { + + public LyricsNotFoundException(String artistName, String trackTitle) { + super("Lyrics not found for '" + trackTitle + "' by " + artistName); + } + +} + + + diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/TrackNotFoundException.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/TrackNotFoundException.java new file mode 100644 index 0000000..7b9c6b1 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/TrackNotFoundException.java @@ -0,0 +1,7 @@ +package net.hackyourfuture.backend.week6.postify.exception; + +public class TrackNotFoundException extends RuntimeException { + public TrackNotFoundException(int trackId) { + super("Track with id " + trackId + " not found"); + } +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java new file mode 100644 index 0000000..d2351cb --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java @@ -0,0 +1,59 @@ +package net.hackyourfuture.backend.week6.postify.service; + +import net.hackyourfuture.backend.week6.postify.model.TrackLyricsResponse; +import net.hackyourfuture.backend.week6.postify.repository.TrackRepository; +import net.hackyourfuture.backend.week6.postify.repository.TrackRepository.TrackWithArtist; +import org.springframework.http.HttpStatus; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestClientResponseException; + +@Service +public class TrackLyricsService { + + private final TrackRepository trackRepository; + private final RestClient lyricsRestClient; + + public TrackLyricsService(TrackRepository trackRepository, RestClient lyricsRestClient) { + this.trackRepository = trackRepository; + this.lyricsRestClient = lyricsRestClient; + } + + public TrackLyricsResponse getLyricsForTrack(int trackId) { + TrackWithArtist track = trackRepository.findTrackWithArtistById(trackId) + .orElseThrow(() -> new TrackNotFoundException(trackId)); + + String artist = track.artistName(); + String title = track.trackTitle(); + + LyricsApiResponse apiResponse; + try { + apiResponse = lyricsRestClient.get() + .uri("/{artist}/{title}", encode(artist), encode(title)) + .retrieve() + .body(LyricsApiResponse.class); + } catch (RestClientResponseException ex) { + if (ex.getStatusCode() == HttpStatus.NOT_FOUND) { + throw new LyricsNotFoundException(trackId); + } + throw ex; + } + + if (apiResponse == null || apiResponse.lyrics() == null || apiResponse.lyrics().isBlank()) { + throw new LyricsNotFoundException(trackId); + } + + return new TrackLyricsResponse( + track.trackId(), + track.trackTitle(), + track.artistName(), + apiResponse.lyrics() + ); + } + + private String encode(String value) { + return value.replace(" ", "+"); + } + + public record LyricsApiResponse(String lyrics) {} +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java new file mode 100644 index 0000000..c114f5f --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java @@ -0,0 +1,4 @@ +package net.hackyourfuture.backend.week6.postify.controller; + +public class TrackLyricsControllerTest { +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java deleted file mode 100644 index 4b4a063..0000000 --- a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/TrackLyricsResponsesTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package net.hackyourfuture.backend.week6.postify.model; - -public class TrackLyricsResponses { -} From ca9d67481fbef99e6042e4c2eeb90078b7cf15d2 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:34:56 +0200 Subject: [PATCH 19/24] feat: update lyrics DTOs for improved API response handling --- .../backend/week6/postify/dto/LyricsApiResponse.java | 2 +- .../backend/week6/postify/dto/TrackLyricsResponse.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java index 6815356..8420798 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/LyricsApiResponse.java @@ -1,4 +1,4 @@ -package net.hackyourfuture.backend.week6.postify.model; +package net.hackyourfuture.backend.week6.postify.dto; import lombok.Getter; import lombok.Setter; diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java index 2c7ee56..b13d8d4 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java @@ -1,4 +1,4 @@ -package net.hackyourfuture.backend.week6.postify.model; +package net.hackyourfuture.backend.week6.postify.dto; import lombok.AllArgsConstructor; From dfa31bd5c5af300caf83d921aa4140d8027c1894 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:36:53 +0200 Subject: [PATCH 20/24] chore: add RestClient configuration class --- .../week6/postify/config/RestClientConfig.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java index eddc589..8c6a38d 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java @@ -1,4 +1,16 @@ package net.hackyourfuture.backend.week6.postify.config; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestClient; + +@Configuration public class RestClientConfig { + + @Bean + public RestClient lyricsRestClient() { + return RestClient.builder() + .baseUrl("https://api.lyrics.ovh/v1") + .build(); + } } From 634853e1c956950a859a646fbac8efb05f2a9fa9 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:39:05 +0200 Subject: [PATCH 21/24] feat: implement TrackLyricsController for lyrics endpoint --- .../controller/TrackLyricsController.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java index 83b3d15..4fd4055 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java @@ -1,4 +1,26 @@ + package net.hackyourfuture.backend.week6.postify.controller; +import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse; +import net.hackyourfuture.backend.week6.postify.service.TrackLyricsService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/tracks") public class TrackLyricsController { -} + + private final TrackLyricsService trackLyricsService; + + public TrackLyricsController( + TrackLyricsService trackLyricsService) { + + this.trackLyricsService = trackLyricsService; + } + + @GetMapping("/{id}/lyrics") + public TrackLyricsResponse getLyrics( + @PathVariable Long id) { + + return trackLyricsService.getLyrics(id); + } +} \ No newline at end of file From 3fa14a6846863824231a897ebb5c4f61eceeff82 Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:40:44 +0200 Subject: [PATCH 22/24] feat: implement TrackLyricsService to fetch lyrics from external API --- .../postify/service/TrackLyricsService.java | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java index d2351cb..dae500d 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java @@ -1,12 +1,13 @@ package net.hackyourfuture.backend.week6.postify.service; -import net.hackyourfuture.backend.week6.postify.model.TrackLyricsResponse; +import net.hackyourfuture.backend.week6.postify.exception.LyricsNotFoundException; +import net.hackyourfuture.backend.week6.postify.exception.TrackNotFoundException; +import net.hackyourfuture.backend.week6.postify.dto.LyricsApiResponse; +import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse; import net.hackyourfuture.backend.week6.postify.repository.TrackRepository; -import net.hackyourfuture.backend.week6.postify.repository.TrackRepository.TrackWithArtist; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClient; -import org.springframework.web.client.RestClientResponseException; @Service public class TrackLyricsService { @@ -14,46 +15,42 @@ public class TrackLyricsService { private final TrackRepository trackRepository; private final RestClient lyricsRestClient; - public TrackLyricsService(TrackRepository trackRepository, RestClient lyricsRestClient) { + public TrackLyricsService(TrackRepository trackRepository, + RestClient lyricsRestClient) { this.trackRepository = trackRepository; this.lyricsRestClient = lyricsRestClient; } - public TrackLyricsResponse getLyricsForTrack(int trackId) { - TrackWithArtist track = trackRepository.findTrackWithArtistById(trackId) - .orElseThrow(() -> new TrackNotFoundException(trackId)); + public TrackLyricsResponse getLyrics(Long trackId) { - String artist = track.artistName(); - String title = track.trackTitle(); + TrackRepository.TrackWithArtist track = + trackRepository.findTrackWithArtistsById(trackId.intValue()) + .orElseThrow(() -> + new TrackNotFoundException(trackId.intValue())); - LyricsApiResponse apiResponse; try { - apiResponse = lyricsRestClient.get() - .uri("/{artist}/{title}", encode(artist), encode(title)) - .retrieve() - .body(LyricsApiResponse.class); - } catch (RestClientResponseException ex) { - if (ex.getStatusCode() == HttpStatus.NOT_FOUND) { - throw new LyricsNotFoundException(trackId); - } - throw ex; - } - if (apiResponse == null || apiResponse.lyrics() == null || apiResponse.lyrics().isBlank()) { - throw new LyricsNotFoundException(trackId); + LyricsApiResponse lyricsResponse = + lyricsRestClient.get() + .uri("/{artist}/{title}", + track.artistName(), + track.trackTitle()) + .retrieve() + .body(LyricsApiResponse.class); + + return new TrackLyricsResponse( + track.trackId(), + track.trackTitle(), + track.artistName(), + lyricsResponse.getLyrics() + ); + + } catch (HttpClientErrorException.NotFound exception) { + + throw new LyricsNotFoundException( + track.artistName(), + track.trackTitle() + ); } - - return new TrackLyricsResponse( - track.trackId(), - track.trackTitle(), - track.artistName(), - apiResponse.lyrics() - ); } - - private String encode(String value) { - return value.replace(" ", "+"); - } - - public record LyricsApiResponse(String lyrics) {} -} +} \ No newline at end of file From a9a18d54eee8b9a3adeedd4fca3836f9fc356b3e Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:42:57 +0200 Subject: [PATCH 23/24] test: add integration test for TrackLyricsController --- .../controller/TrackLyricsControllerTest.java | 90 ++++++++++++++++++- 1 file changed, 88 insertions(+), 2 deletions(-) diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java index c114f5f..b3b6ef6 100644 --- a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java @@ -1,4 +1,90 @@ package net.hackyourfuture.backend.week6.postify.controller; -public class TrackLyricsControllerTest { -} +import net.hackyourfuture.backend.week6.postify.exception.GlobalExceptionHandler; +import net.hackyourfuture.backend.week6.postify.exception.LyricsNotFoundException; +import net.hackyourfuture.backend.week6.postify.exception.TrackNotFoundException; +import net.hackyourfuture.backend.week6.postify.dto.TrackLyricsResponse; +import net.hackyourfuture.backend.week6.postify.service.TrackLyricsService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +class TrackLyricsControllerTest { + + private MockMvc mockMvc; + + @Mock + private TrackLyricsService trackLyricsService; + + @InjectMocks + private TrackLyricsController trackLyricsController; + + @BeforeEach + void setup() { + MockitoAnnotations.openMocks(this); + + mockMvc = MockMvcBuilders + .standaloneSetup(trackLyricsController) + .setControllerAdvice(new GlobalExceptionHandler()) + .build(); + } + + @Test + void shouldReturnLyrics_whenTrackExistsAndLyricsAvailable() throws Exception { + + TrackLyricsResponse response = new TrackLyricsResponse( + 41, + "LUNCH", + "Billie Eilish", + "I'm doing good, I'm on some new shit..." + ); + + when(trackLyricsService.getLyrics(41L)) + .thenReturn(response); + + mockMvc.perform(get("/tracks/41/lyrics")) + .andExpect(status().isOk()) + .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.trackId").value(41)) + .andExpect(jsonPath("$.trackTitle").value("LUNCH")) + .andExpect(jsonPath("$.artistName").value("Billie Eilish")) + .andExpect(jsonPath("$.lyrics") + .value("I'm doing good, I'm on some new shit...")); + } + + @Test + void shouldReturn404_whenTrackDoesNotExist() throws Exception { + + when(trackLyricsService.getLyrics(999L)) + .thenThrow(new TrackNotFoundException(999)); + + mockMvc.perform(get("/tracks/999/lyrics")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message") + .value("Track with id 999 not found")); + } + + @Test + void shouldReturn404_whenLyricsNotAvailable() throws Exception { + + when(trackLyricsService.getLyrics(83L)) + .thenThrow(new LyricsNotFoundException( + "Tyler the Creator", + "WUSYANAME" + )); + + mockMvc.perform(get("/tracks/83/lyrics")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.message") + .value("Lyrics not found for 'WUSYANAME' by Tyler the Creator")); + } +} \ No newline at end of file From 5a898f347e7dce194b1a3e2cf153bc40c585a7ec Mon Sep 17 00:00:00 2001 From: Dagim02 Date: Wed, 10 Jun 2026 19:45:54 +0200 Subject: [PATCH 24/24] chore: remove duplicate deps and add required ones --- postify/pom.xml | 35 +++++++++-------------------------- 1 file changed, 9 insertions(+), 26 deletions(-) diff --git a/postify/pom.xml b/postify/pom.xml index 1f5ae47..e46cbe4 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -32,56 +32,39 @@ org.springframework.boot - spring-boot-starter-webmvc + spring-boot-starter-web org.springframework.boot spring-boot-starter-jdbc - io.github.cdimascio - java-dotenv - 5.2.2 + org.springframework.boot + spring-boot-starter-webflux - org.postgresql postgresql runtime - - org.springframework.boot - spring-boot-starter-webflux - org.projectlombok lombok true - org.springframework.boot - spring-boot-starter-test - test - - - com.jayway.jsonpath - json-path - test + io.github.cdimascio + java-dotenv + 5.2.2 - org.mockito - mockito-core - 5.11.0 + org.springframework.boot + spring-boot-starter-test test - - org.mockito - mockito-junit-jupiter - 5.11.0 - test - +