From 192907a584ffcd6d03793e4ff37d302253413f19 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Mon, 8 Jun 2026 23:47:45 +0200 Subject: [PATCH 1/8] database is connected --- postify/pom.xml | 12 +++++++++++ .../postify/DatabaseConnectionCheck.java | 21 +++++++++++++++++++ .../src/main/resources/application.properties | 5 ++++- 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/DatabaseConnectionCheck.java diff --git a/postify/pom.xml b/postify/pom.xml index 7cea5f7..3b2baa3 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -50,6 +50,18 @@ spring-boot-starter-webmvc-test test + + + org.springframework.boot + spring-boot-starter-jdbc + + + + + org.postgresql + postgresql + runtime + diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/DatabaseConnectionCheck.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/DatabaseConnectionCheck.java new file mode 100644 index 0000000..ab13703 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/DatabaseConnectionCheck.java @@ -0,0 +1,21 @@ +package net.hackyourfuture.backend.week6.postify; + +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +public class DatabaseConnectionCheck { + private final JdbcTemplate jdbcTemplate; + + public DatabaseConnectionCheck(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @EventListener(ApplicationReadyEvent.class) + public void checkConnection() { + String result = this.jdbcTemplate.queryForObject("SELECT 'connected'", String.class); + System.out.println("Database status: " + result); + } +} diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties index 8f4842f..4b603f3 100644 --- a/postify/src/main/resources/application.properties +++ b/postify/src/main/resources/application.properties @@ -1 +1,4 @@ -spring.application.name=postify +spring.datasource.url=jdbc:postgresql://localhost:5432/POSTIFY +spring.datasource.username=hyfuser +spring.datasource.password=hyfpassword +spring.datasource.driver-class-name=org.postgresql.Driver From dd2d235e82ff3bb47bdd87e1a28cd81744172dc3 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Tue, 9 Jun 2026 13:56:25 +0200 Subject: [PATCH 2/8] Add controller, DTO, service stub, repository skeleton, UserNotFoundException, and test placeholder. Compiles; SQL and logic still to be implemented. --- .../controller/UserStatisticsController.java | 22 +++++++++++++++++++ .../week6/postify/dto/UserStatistics.java | 18 +++++++++++++++ .../exception/UserNotFoundException.java | 11 ++++++++++ .../postify/repository/StreamRepository.java | 13 +++++++++++ .../service/UserStatisticsService.java | 11 ++++++++++ .../postify/UserStatisticsControllerTest.java | 18 +++++++++++++++ 6 files changed, 93 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/dto/UserStatistics.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/UserNotFoundException.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.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/UserStatisticsControllerTest.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..74b159d --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java @@ -0,0 +1,22 @@ +package net.hackyourfuture.backend.week6.postify.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import net.hackyourfuture.backend.week6.postify.dto.UserStatistics; +import net.hackyourfuture.backend.week6.postify.service.UserStatisticsService; + +@RestController +public class UserStatisticsController { + private final UserStatisticsService service; + + public UserStatisticsController(UserStatisticsService service){ + this.service = service; + } + @GetMapping("/users/{id}/stats") + public UserStatistics getStats(@PathVariable Long id) { + return service.getStats(id); + } + +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/UserStatistics.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/UserStatistics.java new file mode 100644 index 0000000..a15d857 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/UserStatistics.java @@ -0,0 +1,18 @@ +package net.hackyourfuture.backend.week6.postify.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class UserStatistics { + private long userId; + private String userName; + private String userCountry; + private long totalStreams; + private long uniqueTracksStreamed; + private long uniqueArtistsStreamed; + private String favoriteGenre; + private long totalListeningTimeSeconds; + +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/UserNotFoundException.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/UserNotFoundException.java new file mode 100644 index 0000000..8a8237c --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/UserNotFoundException.java @@ -0,0 +1,11 @@ +package net.hackyourfuture.backend.week6.postify.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(Long id) { + super("User not found: " + id); + } +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java new file mode 100644 index 0000000..dea05ef --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.backend.week6.postify.repository; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +@Repository +public class StreamRepository { + private final JdbcTemplate jdbcTemplate; + + public StreamRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } +} 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..592ab4f --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java @@ -0,0 +1,11 @@ +package net.hackyourfuture.backend.week6.postify.service; + +import net.hackyourfuture.backend.week6.postify.dto.UserStatistics; +import org.springframework.stereotype.Service; + +@Service +public class UserStatisticsService { + public UserStatistics getStats(Long id) { + return null; + } +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java new file mode 100644 index 0000000..a64a612 --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java @@ -0,0 +1,18 @@ +package net.hackyourfuture.backend.week6.postify; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +class UserStatisticsControllerTest { + + @Test + @Disabled("TODO: enable once service + repository are implemented") + void returnsStatsForExistingUser() { + } + + @Test + @Disabled("TODO: enable once 404 handling is implemented") + void returns404ForUnknownUser() { + } +} + From 2bc6fc6bbefd59931cd848843fcbd889631ef977 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Tue, 9 Jun 2026 17:36:51 +0200 Subject: [PATCH 3/8] queries are added --- .../postify/repository/StreamRepository.java | 52 ++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java index dea05ef..6bd946f 100644 --- a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java @@ -1,13 +1,63 @@ package net.hackyourfuture.backend.week6.postify.repository; +import java.util.Optional; + import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; +import net.hackyourfuture.backend.week6.postify.dto.UserStatistics; + @Repository public class StreamRepository { - private final JdbcTemplate jdbcTemplate; + private final JdbcTemplate jdbcTemplate; public StreamRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } + + public Optional findStats(Long userId) { + String sql = """ + SELECT users.user_id, users.user_name, users.user_country, + COUNT(streams.stream_id) AS total_streams, + COUNT(DISTINCT streams.track_id) AS unique_tracks, + COUNT(DISTINCT albums.artist_id) AS unique_artists, + COALESCE(SUM(tracks.track_duration_s), 0) AS total_listening_seconds + FROM users + LEFT JOIN streams ON streams.user_id = users.user_id + LEFT JOIN tracks ON tracks.track_id = streams.track_id + LEFT JOIN albums ON albums.album_id = tracks.album_id + WHERE users.user_id = ? + GROUP BY users.user_id, users.user_name, users.user_country + """; + + return jdbcTemplate.query(sql, (rs, rowNum) -> UserStatistics.builder() + .userId(rs.getLong("user_id")) + .userName(rs.getString("user_name")) + .userCountry(rs.getString("user_country")) + .totalStreams(rs.getLong("total_streams")) + .uniqueTracksStreamed(rs.getLong("unique_tracks")) + .uniqueArtistsStreamed(rs.getLong("unique_artists")) + .totalListeningTimeSeconds(rs.getLong("total_listening_seconds")) + .build(), + userId) + .stream() + .findFirst(); + } + + + public Optional findFavoriteGenre(Long userId) { + String sql = """ + SELECT tracks.genre + FROM streams + JOIN tracks ON tracks.track_id = streams.track_id + WHERE streams.user_id = ? AND tracks.genre IS NOT NULL + GROUP BY tracks.genre + ORDER BY COUNT(*) DESC + LIMIT 1 + """; + + return jdbcTemplate.query(sql, (rs, rowNum) -> rs.getString("genre"), userId) + .stream() + .findFirst(); + } } From cddde45349a1ba02e9a92e022ae3e3c888d227ed Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Tue, 9 Jun 2026 18:06:09 +0200 Subject: [PATCH 4/8] service is ok --- .../service/UserStatisticsService.java | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) 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 592ab4f..480ab66 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,11 +1,27 @@ package net.hackyourfuture.backend.week6.postify.service; -import net.hackyourfuture.backend.week6.postify.dto.UserStatistics; import org.springframework.stereotype.Service; +import net.hackyourfuture.backend.week6.postify.dto.UserStatistics; +import net.hackyourfuture.backend.week6.postify.exception.UserNotFoundException; +import net.hackyourfuture.backend.week6.postify.repository.StreamRepository; + @Service public class UserStatisticsService { + private final StreamRepository streamRepository; + + public UserStatisticsService(StreamRepository streamRepository) { + this.streamRepository = streamRepository; + } + public UserStatistics getStats(Long id) { - return null; + // Fetch the aggregate row. Empty means the user id does not exist -> 404. + UserStatistics stats = streamRepository.findStats(id) + .orElseThrow(() -> new UserNotFoundException(id)); + + // Favorite genre is a separate query; absent when the user has no streams. + streamRepository.findFavoriteGenre(id).ifPresent(stats::setFavoriteGenre); + + return stats; } } From 425570eae5e94f49954d0ce3ab9e1d703f535c89 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Tue, 9 Jun 2026 20:18:51 +0200 Subject: [PATCH 5/8] RestClientConfig is added --- .../week6/postify/config/RestClientConfig.java | 17 +++++++++++++++++ .../src/main/resources/application.properties | 3 +++ 2 files changed, 20 insertions(+) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.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..40d989d --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java @@ -0,0 +1,17 @@ +package net.hackyourfuture.backend.week6.postify.config; + +import org.springframework.beans.factory.annotation.Value; +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(@Value("${lyrics.api.base-url}") String baseUrl) { + return RestClient.builder() + .baseUrl(baseUrl) + .build(); + } +} diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties index 4b603f3..1cf6dfa 100644 --- a/postify/src/main/resources/application.properties +++ b/postify/src/main/resources/application.properties @@ -2,3 +2,6 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/POSTIFY spring.datasource.username=hyfuser spring.datasource.password=hyfpassword spring.datasource.driver-class-name=org.postgresql.Driver + +# External lyrics API (https://lyricsovh.docs.apiary.io) +lyrics.api.base-url=https://api.lyrics.ovh/v1 From 1df502d9c3504ccd07db3bb4cfaa41a83c114434 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Tue, 9 Jun 2026 21:08:45 +0200 Subject: [PATCH 6/8] Scaffold lyrics endpoint: controller, DTO, service stub, repository skeleton, 404 exceptions --- .../controller/TrackLyricsController.java | 23 +++++++++++++++++++ .../week6/postify/dto/TrackLyrics.java | 13 +++++++++++ .../exception/LyricsNotFoundException.java | 11 +++++++++ .../exception/TrackNotFoundException.java | 11 +++++++++ .../postify/repository/TrackRepository.java | 23 +++++++++++++++++++ .../postify/service/TrackLyricsService.java | 23 +++++++++++++++++++ 6 files changed, 104 insertions(+) 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/TrackLyrics.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/repository/TrackRepository.java create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java 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..d102258 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java @@ -0,0 +1,23 @@ +package net.hackyourfuture.backend.week6.postify.controller; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import net.hackyourfuture.backend.week6.postify.dto.TrackLyrics; +import net.hackyourfuture.backend.week6.postify.service.TrackLyricsService; + +@RestController +public class TrackLyricsController { + private final TrackLyricsService service; + + public TrackLyricsController(TrackLyricsService service) { + this.service = service; + } + + @GetMapping("/tracks/{id}/lyrics") + public TrackLyrics getLyrics(@PathVariable Long id) { + return service.getLyrics(id); + } + +} diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyrics.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyrics.java new file mode 100644 index 0000000..f208a4b --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyrics.java @@ -0,0 +1,13 @@ +package net.hackyourfuture.backend.week6.postify.dto; + +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TrackLyrics { + private long trackId; + private String trackTitle; + private String artistName; + private String lyrics; +} 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..009c2ba --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/LyricsNotFoundException.java @@ -0,0 +1,11 @@ +package net.hackyourfuture.backend.week6.postify.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class LyricsNotFoundException extends RuntimeException { + public LyricsNotFoundException(String artistName, String trackTitle) { + super("No lyrics 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..e992901 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exception/TrackNotFoundException.java @@ -0,0 +1,11 @@ +package net.hackyourfuture.backend.week6.postify.exception; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class TrackNotFoundException extends RuntimeException { + public TrackNotFoundException(Long id) { + super("Track not found: " + id); + } +} 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..1ac1b76 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java @@ -0,0 +1,23 @@ +package net.hackyourfuture.backend.week6.postify.repository; + +import java.util.Optional; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; + +import net.hackyourfuture.backend.week6.postify.dto.TrackLyrics; + +@Repository +public class TrackRepository { + private final JdbcTemplate jdbcTemplate; + + public TrackRepository(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + + public Optional findTrackWithArtist(Long trackId) { + + return Optional.empty(); + } +} 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..0ef3e0c --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java @@ -0,0 +1,23 @@ +package net.hackyourfuture.backend.week6.postify.service; + +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +import net.hackyourfuture.backend.week6.postify.dto.TrackLyrics; +import net.hackyourfuture.backend.week6.postify.repository.TrackRepository; + +@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 TrackLyrics getLyrics(Long trackId) { + + return null; + } +} From fce3e6709758de85f805cd4af791af5bfcc27597 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Wed, 10 Jun 2026 16:51:32 +0200 Subject: [PATCH 7/8] Implement lyrics endpoint: track-artist SQL join, RestClient call with 404 handling --- .../postify/repository/TrackRepository.java | 20 +++++++++++--- .../postify/service/TrackLyricsService.java | 27 +++++++++++++++++-- 2 files changed, 42 insertions(+), 5 deletions(-) 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 index 1ac1b76..e2d03f1 100644 --- 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 @@ -15,9 +15,23 @@ public TrackRepository(JdbcTemplate jdbcTemplate) { this.jdbcTemplate = jdbcTemplate; } - + public Optional findTrackWithArtist(Long trackId) { - - return Optional.empty(); + String sql = """ + SELECT tracks.track_id, tracks.track_title, artists.artist_name + FROM tracks + JOIN albums ON albums.album_id = tracks.album_id + JOIN artists ON artists.artist_id = albums.artist_id + WHERE tracks.track_id = ? + """; + + return jdbcTemplate.query(sql, (rs, rowNum) -> TrackLyrics.builder() + .trackId(rs.getLong("track_id")) + .trackTitle(rs.getString("track_title")) + .artistName(rs.getString("artist_name")) + .build(), + trackId) + .stream() + .findFirst(); } } 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 0ef3e0c..41d0c25 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,9 +1,12 @@ package net.hackyourfuture.backend.week6.postify.service; import org.springframework.stereotype.Service; +import org.springframework.web.client.HttpClientErrorException; import org.springframework.web.client.RestClient; import net.hackyourfuture.backend.week6.postify.dto.TrackLyrics; +import net.hackyourfuture.backend.week6.postify.exception.LyricsNotFoundException; +import net.hackyourfuture.backend.week6.postify.exception.TrackNotFoundException; import net.hackyourfuture.backend.week6.postify.repository.TrackRepository; @Service @@ -17,7 +20,27 @@ public TrackLyricsService(TrackRepository trackRepository, RestClient lyricsRest } public TrackLyrics getLyrics(Long trackId) { - - return null; + TrackLyrics track = trackRepository.findTrackWithArtist(trackId) + .orElseThrow(() -> new TrackNotFoundException(trackId)); + + try { + LyricsResponse response = lyricsRestClient.get() + .uri("/{artist}/{title}", track.getArtistName(), track.getTrackTitle()) + .retrieve() + .body(LyricsResponse.class); + + if (response == null || response.lyrics() == null || response.lyrics().isBlank()) { + throw new LyricsNotFoundException(track.getArtistName(), track.getTrackTitle()); + } + track.setLyrics(response.lyrics()); + } catch (HttpClientErrorException.NotFound e) { + throw new LyricsNotFoundException(track.getArtistName(), track.getTrackTitle()); + } + + return track; + } + + // Shape of the lyrics.ovh response: {"lyrics": "..."} + private record LyricsResponse(String lyrics) { } } From ca4bc4d1e5a276222449bb4153f7e824d7093674 Mon Sep 17 00:00:00 2001 From: Yusuprozimemet Date: Wed, 10 Jun 2026 18:31:29 +0200 Subject: [PATCH 8/8] Integration tests are added --- postify/pom.xml | 12 ++- .../week6/postify/client/LyricsApiClient.java | 40 ++++++++++ .../postify/config/RestClientConfig.java | 5 +- .../postify/service/TrackLyricsService.java | 29 ++----- .../postify/TrackLyricsControllerTest.java | 76 +++++++++++++++++++ .../postify/UserStatisticsControllerTest.java | 33 ++++++-- 6 files changed, 158 insertions(+), 37 deletions(-) create mode 100644 postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiClient.java create mode 100644 postify/src/test/java/net/hackyourfuture/backend/week6/postify/TrackLyricsControllerTest.java diff --git a/postify/pom.xml b/postify/pom.xml index 3b2baa3..b013601 100644 --- a/postify/pom.xml +++ b/postify/pom.xml @@ -45,6 +45,11 @@ lombok true + + + org.springframework.boot + spring-boot-restclient + org.springframework.boot spring-boot-starter-webmvc-test @@ -55,13 +60,6 @@ org.springframework.boot spring-boot-starter-jdbc - - - - org.postgresql - postgresql - runtime - diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiClient.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiClient.java new file mode 100644 index 0000000..cbf3954 --- /dev/null +++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiClient.java @@ -0,0 +1,40 @@ +package net.hackyourfuture.backend.week6.postify.client; + +import java.util.Optional; + +import org.springframework.stereotype.Component; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.RestClient; + +/** + * Wraps the external lyrics.ovh API call. + * Returns Optional.empty() when the API has no lyrics for the song. + */ +@Component +public class LyricsApiClient { + private final RestClient lyricsRestClient; + + public LyricsApiClient(RestClient lyricsRestClient) { + this.lyricsRestClient = lyricsRestClient; + } + + public Optional fetchLyrics(String artistName, String trackTitle) { + try { + LyricsResponse response = lyricsRestClient.get() + .uri("/{artist}/{title}", artistName, trackTitle) + .retrieve() + .body(LyricsResponse.class); + + if (response == null || response.lyrics() == null || response.lyrics().isBlank()) { + return Optional.empty(); + } + return Optional.of(response.lyrics()); + } catch (HttpClientErrorException.NotFound e) { + return Optional.empty(); + } + } + + // Shape of the lyrics.ovh response: {"lyrics": "..."} + private record LyricsResponse(String lyrics) { + } +} 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 40d989d..a1f8bc1 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 @@ -9,8 +9,9 @@ public class RestClientConfig { @Bean - public RestClient lyricsRestClient(@Value("${lyrics.api.base-url}") String baseUrl) { - return RestClient.builder() + public RestClient lyricsRestClient(RestClient.Builder builder, + @Value("${lyrics.api.base-url}") String baseUrl) { + return builder .baseUrl(baseUrl) .build(); } 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 41d0c25..b1b061e 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,9 +1,8 @@ package net.hackyourfuture.backend.week6.postify.service; import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpClientErrorException; -import org.springframework.web.client.RestClient; +import net.hackyourfuture.backend.week6.postify.client.LyricsApiClient; import net.hackyourfuture.backend.week6.postify.dto.TrackLyrics; import net.hackyourfuture.backend.week6.postify.exception.LyricsNotFoundException; import net.hackyourfuture.backend.week6.postify.exception.TrackNotFoundException; @@ -12,35 +11,21 @@ @Service public class TrackLyricsService { private final TrackRepository trackRepository; - private final RestClient lyricsRestClient; + private final LyricsApiClient lyricsApiClient; - public TrackLyricsService(TrackRepository trackRepository, RestClient lyricsRestClient) { + public TrackLyricsService(TrackRepository trackRepository, LyricsApiClient lyricsApiClient) { this.trackRepository = trackRepository; - this.lyricsRestClient = lyricsRestClient; + this.lyricsApiClient = lyricsApiClient; } public TrackLyrics getLyrics(Long trackId) { TrackLyrics track = trackRepository.findTrackWithArtist(trackId) .orElseThrow(() -> new TrackNotFoundException(trackId)); - try { - LyricsResponse response = lyricsRestClient.get() - .uri("/{artist}/{title}", track.getArtistName(), track.getTrackTitle()) - .retrieve() - .body(LyricsResponse.class); - - if (response == null || response.lyrics() == null || response.lyrics().isBlank()) { - throw new LyricsNotFoundException(track.getArtistName(), track.getTrackTitle()); - } - track.setLyrics(response.lyrics()); - } catch (HttpClientErrorException.NotFound e) { - throw new LyricsNotFoundException(track.getArtistName(), track.getTrackTitle()); - } + String lyrics = lyricsApiClient.fetchLyrics(track.getArtistName(), track.getTrackTitle()) + .orElseThrow(() -> new LyricsNotFoundException(track.getArtistName(), track.getTrackTitle())); + track.setLyrics(lyrics); return track; } - - // Shape of the lyrics.ovh response: {"lyrics": "..."} - private record LyricsResponse(String lyrics) { - } } diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/TrackLyricsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/TrackLyricsControllerTest.java new file mode 100644 index 0000000..706d12f --- /dev/null +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/TrackLyricsControllerTest.java @@ -0,0 +1,76 @@ +package net.hackyourfuture.backend.week6.postify; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verifyNoInteractions; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.context.bean.override.mockito.MockitoBean; +import org.springframework.test.web.servlet.MockMvc; + +import net.hackyourfuture.backend.week6.postify.client.LyricsApiClient; +import net.hackyourfuture.backend.week6.postify.exception.LyricsNotFoundException; +import net.hackyourfuture.backend.week6.postify.exception.TrackNotFoundException; + +/** + * Integration tests for GET /tracks/{id}/lyrics. + * Requires the POSTIFY Postgres database (Docker) with seed data; + * the external lyrics API client is replaced with a Mockito mock, + * so no internet connection is needed. + */ +@SpringBootTest +@AutoConfigureMockMvc +class TrackLyricsControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockitoBean + private LyricsApiClient lyricsApiClient; + + // Seed data: track 41 = 'LUNCH' by Billie Eilish + + @Test + void returnsLyricsForTrackThatHasLyrics() throws Exception { + when(lyricsApiClient.fetchLyrics("Billie Eilish", "LUNCH")) + .thenReturn(Optional.of("Mocked lyrics line one")); + + mockMvc.perform(get("/tracks/41/lyrics")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.trackId").value(41)) + .andExpect(jsonPath("$.trackTitle").value("LUNCH")) + .andExpect(jsonPath("$.artistName").value("Billie Eilish")) + .andExpect(jsonPath("$.lyrics").value("Mocked lyrics line one")); + } + + @Test + void returns404ForUnknownTrack() throws Exception { + mockMvc.perform(get("/tracks/999999/lyrics")) + .andExpect(status().isNotFound()) + .andExpect(result -> assertThat(result.getResolvedException()) + .isInstanceOf(TrackNotFoundException.class)); + + // The service must not call the API for a track that is not in the database. + verifyNoInteractions(lyricsApiClient); + } + + @Test + void returns404WithMessageWhenApiHasNoLyrics() throws Exception { + when(lyricsApiClient.fetchLyrics("Billie Eilish", "LUNCH")) + .thenReturn(Optional.empty()); + + mockMvc.perform(get("/tracks/41/lyrics")) + .andExpect(status().isNotFound()) + .andExpect(result -> assertThat(result.getResolvedException()) + .isInstanceOf(LyricsNotFoundException.class) + .hasMessageContaining("No lyrics found for \"LUNCH\" by Billie Eilish")); + } +} diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java index a64a612..8be41d4 100644 --- a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java +++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java @@ -1,18 +1,39 @@ package net.hackyourfuture.backend.week6.postify; -import org.junit.jupiter.api.Disabled; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc; +import org.springframework.test.web.servlet.MockMvc; +/** + * Integration tests for GET /users/{id}/stats. + * Requires the POSTIFY Postgres database (Docker) with seed data. + */ +@SpringBootTest +@AutoConfigureMockMvc class UserStatisticsControllerTest { + @Autowired + private MockMvc mockMvc; + @Test - @Disabled("TODO: enable once service + repository are implemented") - void returnsStatsForExistingUser() { + void returnsStatsForExistingUser() throws Exception { + mockMvc.perform(get("/users/1/stats")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.userId").value(1)) + .andExpect(jsonPath("$.userName").value("lena_v")) + .andExpect(jsonPath("$.userCountry").value("NL")) + .andExpect(jsonPath("$.favoriteGenre").value("Nederpop")); } @Test - @Disabled("TODO: enable once 404 handling is implemented") - void returns404ForUnknownUser() { + void returns404ForUnknownUser() throws Exception { + mockMvc.perform(get("/users/999999/stats")) + .andExpect(status().isNotFound()); } } -