diff --git a/postify/pom.xml b/postify/pom.xml
index 7cea5f7..a02f898 100644
--- a/postify/pom.xml
+++ b/postify/pom.xml
@@ -5,7 +5,7 @@
org.springframework.boot
spring-boot-starter-parent
- 4.0.6
+ 3.2.5
net.hackyourfuture.backend.week6
@@ -27,14 +27,28 @@
- 25
+ 21
+
+
+
+ org.testcontainers
+ testcontainers-bom
+ 1.19.7
+ pom
+ import
+
+
+
org.springframework.boot
- spring-boot-starter-webmvc
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
-
org.postgresql
postgresql
@@ -45,11 +59,22 @@
lombok
true
+
+ org.testcontainers
+ junit-jupiter
+ test
+
+
+ org.testcontainers
+ postgresql
+ test
+
org.springframework.boot
- spring-boot-starter-webmvc-test
+ spring-boot-starter-test
test
+
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/DBConnectionCheck.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/DBConnectionCheck.java
new file mode 100644
index 0000000..89c3b4d
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/DBConnectionCheck.java
@@ -0,0 +1,20 @@
+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;
+
+public class DBConnectionCheck {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public DBConnectionCheck(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/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiConfiguration.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiConfiguration.java
new file mode 100644
index 0000000..5a71043
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsApiConfiguration.java
@@ -0,0 +1,23 @@
+package net.hackyourfuture.backend.week6.postify.client;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.web.client.RestClient;
+
+@Configuration
+public class LyricsApiConfiguration {
+
+ @Bean
+ public RestClient.Builder restClientBuilder() {
+ return RestClient.builder();
+ }
+
+ @Bean
+ public RestClient lyricsRestClient(RestClient.Builder builder){
+ return builder.baseUrl("https://api.lyrics.ovh")
+ .defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
+ .build();
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsClient.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsClient.java
new file mode 100644
index 0000000..5ebdba0
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/client/LyricsClient.java
@@ -0,0 +1,30 @@
+package net.hackyourfuture.backend.week6.postify.client;
+
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.LyricApiResponse;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestClient;
+
+@Service
+public class LyricsClient {
+ private final RestClient lyricsRestClient;
+
+ public LyricsClient(@Qualifier("lyricsRestClient") RestClient lyricsRestClient){
+ this.lyricsRestClient = lyricsRestClient;
+ }
+
+ public LyricApiResponse getLyrics(String artistName, String trackTitle){
+ try {
+ return lyricsRestClient.get()
+ .uri(uriBuilder -> uriBuilder
+ .path("/v1/{artist}/{title}")
+ .build(artistName, trackTitle))
+ .retrieve()
+ .body(LyricApiResponse.class);
+ } catch(HttpClientErrorException.NotFound ex){
+ return null;
+ }
+
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controllers/TrackLyricsController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controllers/TrackLyricsController.java
new file mode 100644
index 0000000..23e2c52
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controllers/TrackLyricsController.java
@@ -0,0 +1,23 @@
+package net.hackyourfuture.backend.week6.postify.controllers;
+
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.TrackLyricResponse;
+import net.hackyourfuture.backend.week6.postify.services.TrackLyricsService;
+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("/tracks")
+public class TrackLyricsController {
+ private final TrackLyricsService service;
+
+ public TrackLyricsController(TrackLyricsService service){
+ this.service = service;
+ }
+
+ @GetMapping("/{id}/lyrics")
+ public TrackLyricResponse getLyrics(@PathVariable("id") int trackId){
+ return service.getLyricsForTrack(trackId);
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controllers/UserStatisticsController.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controllers/UserStatisticsController.java
new file mode 100644
index 0000000..72397a2
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controllers/UserStatisticsController.java
@@ -0,0 +1,24 @@
+package net.hackyourfuture.backend.week6.postify.controllers;
+
+import net.hackyourfuture.backend.week6.postify.dto.response.user.UserStatisticsResponse;
+import net.hackyourfuture.backend.week6.postify.services.UserStatisticsService;
+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 {
+
+ private final UserStatisticsService service;
+
+ public UserStatisticsController(UserStatisticsService service){
+ this.service = service;
+ }
+
+ @GetMapping("/{id}/stats")
+ public UserStatisticsResponse getUserStats(@PathVariable int id){
+ return service.getStatisticsForUser(id);
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/LyricApiResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/LyricApiResponse.java
new file mode 100644
index 0000000..96b18d9
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/LyricApiResponse.java
@@ -0,0 +1,4 @@
+package net.hackyourfuture.backend.week6.postify.dto.response.lyric;
+
+public record LyricApiResponse(String lyrics) {
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/TrackBasicInfo.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/TrackBasicInfo.java
new file mode 100644
index 0000000..b017be3
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/TrackBasicInfo.java
@@ -0,0 +1,4 @@
+package net.hackyourfuture.backend.week6.postify.dto.response.lyric;
+
+public record TrackBasicInfo (int trackId, String trackTitle, String artistName){
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/TrackLyricResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/TrackLyricResponse.java
new file mode 100644
index 0000000..8d3f0ef
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/lyric/TrackLyricResponse.java
@@ -0,0 +1,6 @@
+package net.hackyourfuture.backend.week6.postify.dto.response.lyric;
+
+import java.util.List;
+
+public record TrackLyricResponse (int trackId, String trackTitle, String artistName, List lyrics){
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/user/UserBasicInfoResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/user/UserBasicInfoResponse.java
new file mode 100644
index 0000000..21d1c45
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/user/UserBasicInfoResponse.java
@@ -0,0 +1,4 @@
+package net.hackyourfuture.backend.week6.postify.dto.response.user;
+
+public record UserBasicInfoResponse(int id, String name, String country) {
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/user/UserStatisticsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/user/UserStatisticsResponse.java
new file mode 100644
index 0000000..dfb3cc1
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/response/user/UserStatisticsResponse.java
@@ -0,0 +1,13 @@
+package net.hackyourfuture.backend.week6.postify.dto.response.user;
+
+public record UserStatisticsResponse(
+ int userId,
+ String userName,
+ String userCountry,
+ int totalStreams,
+ int uniqueTracksStreamed,
+ int uniqueArtistsStreamed,
+ String favoriteGenre,
+ int totalListeningTimeSeconds
+ ) {
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/GlobalExceptionHandler.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/GlobalExceptionHandler.java
new file mode 100644
index 0000000..f7c0118
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/GlobalExceptionHandler.java
@@ -0,0 +1,25 @@
+package net.hackyourfuture.backend.week6.postify.exceptions;
+
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.ExceptionHandler;
+import org.springframework.web.bind.annotation.RestControllerAdvice;
+
+@RestControllerAdvice
+public class GlobalExceptionHandler {
+
+ @ExceptionHandler(UserNotFoundException.class)
+ public ResponseEntity handleUserNotFound(UserNotFoundException ex){
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
+ }
+
+ @ExceptionHandler(TrackNotFoundException.class)
+ public ResponseEntity handleTrackNotFound(TrackNotFoundException ex){
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
+ }
+
+ @ExceptionHandler(LyricsNotFoundException.class)
+ public ResponseEntity handleLyricsNotFound(LyricsNotFoundException ex){
+ return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ex.getMessage());
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/LyricsNotFoundException.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/LyricsNotFoundException.java
new file mode 100644
index 0000000..eaec387
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/LyricsNotFoundException.java
@@ -0,0 +1,7 @@
+package net.hackyourfuture.backend.week6.postify.exceptions;
+
+public class LyricsNotFoundException extends RuntimeException{
+ public LyricsNotFoundException(String trackTitle, String artistName){
+ super("Lyrics not found for track '" + trackTitle + "' by '" + artistName + "'");
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/TrackNotFoundException.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/TrackNotFoundException.java
new file mode 100644
index 0000000..2dbf1e3
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/TrackNotFoundException.java
@@ -0,0 +1,7 @@
+package net.hackyourfuture.backend.week6.postify.exceptions;
+
+public class TrackNotFoundException extends RuntimeException{
+ public TrackNotFoundException(int id){
+ super("Track with id " + id + " not found");
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/UserNotFoundException.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/UserNotFoundException.java
new file mode 100644
index 0000000..b44add9
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/exceptions/UserNotFoundException.java
@@ -0,0 +1,7 @@
+package net.hackyourfuture.backend.week6.postify.exceptions;
+
+public class UserNotFoundException extends RuntimeException{
+ public UserNotFoundException(int id){
+ super("User with id " + id + " not found");
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TracksLyricsRepository.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TracksLyricsRepository.java
new file mode 100644
index 0000000..0464caf
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TracksLyricsRepository.java
@@ -0,0 +1,36 @@
+package net.hackyourfuture.backend.week6.postify.repository;
+
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.TrackBasicInfo;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public class TracksLyricsRepository {
+ private final JdbcTemplate jdbcTemplate;
+
+ public TracksLyricsRepository(JdbcTemplate jdbcTemplate){
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public Optional findTrackWithArtist(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.of(new TrackBasicInfo(
+ rs.getInt("track_id"),
+ rs.getString("track_title"),
+ rs.getString("artist_name")
+ ));
+ } return Optional.empty();
+ }, trackId);
+ }
+}
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..e14c6eb
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java
@@ -0,0 +1,130 @@
+package net.hackyourfuture.backend.week6.postify.repository;
+
+import net.hackyourfuture.backend.week6.postify.dto.response.user.UserBasicInfoResponse;
+import net.hackyourfuture.backend.week6.postify.dto.response.user.UserStatisticsResponse;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public class UserStatisticsRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public UserStatisticsRepository(JdbcTemplate jdbcTemplate){
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public boolean userExists(int userId){
+ String sql = """
+ SELECT COUNT(*)
+ FROM users
+ WHERE user_id = ?
+ """;
+
+ Integer count = jdbcTemplate.queryForObject(sql, Integer.class, userId);
+ return count !=null && count>0;
+ }
+
+ public Optional fetchUserBasic(int userId){
+ String sql = """
+ SELECT user_id, user_name, user_country
+ FROM users
+ WHERE user_id = ?
+ """;
+
+ return jdbcTemplate.query(sql, rs -> {
+ if(rs.next()){
+ return Optional.of(new UserBasicInfoResponse(
+ rs.getInt("user_id"),
+ rs.getString("user_name"),
+ rs.getString("user_country")
+ ));
+ }
+ return Optional.empty();
+ }, userId);
+ }
+
+ public int fetchTotalStreams(int userId){
+ String sql = """
+ SELECT COUNT(*)
+ FROM streams
+ WHERE user_id = ?
+ """;
+ return jdbcTemplate.queryForObject(sql, Integer.class , userId);
+ }
+
+ private int fetchUniqueTracks(int userId){
+ String sql = """
+ SELECT COUNT(DISTINCT track_id)
+ FROM streams
+ WHERE user_id = ?
+ """;
+
+ return jdbcTemplate.queryForObject(sql, Integer.class, userId);
+ }
+
+ private int fetchUniqueArtists(int userId){
+ String sql = """
+ SELECT COUNT(DISTINCT a.artist_id)
+ FROM streams s
+ JOIN tracks t ON s.track_id = t.track_id
+ JOIN albums al ON t.album_id = al.album_id
+ JOIN artists a ON al.artist_id = a.artist_id
+ WHERE s.user_id = ?
+ """;
+ return jdbcTemplate.queryForObject(sql, Integer.class, userId);
+ }
+
+ private String fetchFavoriteGenre(int userId){
+ String sql = """
+ SELECT t.genre
+ FROM streams s
+ JOIN tracks t ON s.track_id = t.track_id
+ WHERE user_id = ?
+ GROUP BY t.genre
+ ORDER BY COUNT(*) DESC
+ LIMIT 1
+ """;
+ return jdbcTemplate.query(sql,
+ rs -> rs.next() ? rs.getString("genre") : null,
+ userId);
+ }
+
+ private int fetchTotalListeningTime(int userId){
+ String sql = """
+ SELECT COALESCE(SUM(t.track_duration_s), 0)
+ FROM streams s
+ JOIN tracks t ON s.track_id = t.track_id
+ WHERE s.user_id = ?
+ """;
+ return jdbcTemplate.queryForObject(sql, Integer.class, userId);
+ }
+
+ public Optional fetchStatistics(int userId){
+
+ Optional basic = fetchUserBasic(userId);
+
+ if(basic.isEmpty()) return Optional.empty();
+
+ int totalStreams = fetchTotalStreams(userId);
+ int uniqueTracks = fetchUniqueTracks(userId);
+ int uniqueArtists = fetchUniqueArtists(userId);
+ String favoriteGenre = fetchFavoriteGenre(userId);
+ int totalListeningTime = fetchTotalListeningTime(userId);
+
+ UserBasicInfoResponse user = basic.get();
+
+ return Optional.of(new UserStatisticsResponse(
+ user.id(),
+ user.name(),
+ user.country(),
+ totalStreams,
+ uniqueTracks,
+ uniqueArtists,
+ favoriteGenre,
+ totalListeningTime
+ ));
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/services/TrackLyricsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/services/TrackLyricsService.java
new file mode 100644
index 0000000..a2753eb
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/services/TrackLyricsService.java
@@ -0,0 +1,45 @@
+package net.hackyourfuture.backend.week6.postify.services;
+
+import net.hackyourfuture.backend.week6.postify.client.LyricsClient;
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.LyricApiResponse;
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.TrackBasicInfo;
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.TrackLyricResponse;
+import net.hackyourfuture.backend.week6.postify.exceptions.LyricsNotFoundException;
+import net.hackyourfuture.backend.week6.postify.exceptions.TrackNotFoundException;
+import net.hackyourfuture.backend.week6.postify.repository.TracksLyricsRepository;
+import org.springframework.stereotype.Service;
+
+import java.util.Arrays;
+import java.util.List;
+
+@Service
+public class TrackLyricsService {
+ private final TracksLyricsRepository repository;
+ private final LyricsClient lyricsClient;
+
+ public TrackLyricsService(TracksLyricsRepository repository, LyricsClient lyricsClient){
+ this.repository = repository;
+ this.lyricsClient = lyricsClient;
+ }
+
+ public TrackLyricResponse getLyricsForTrack(int trackId){
+
+ TrackBasicInfo track = repository.findTrackWithArtist(trackId)
+ .orElseThrow(() -> new TrackNotFoundException(trackId));
+
+ LyricApiResponse apiResponse = lyricsClient.getLyrics(track.artistName(), track.trackTitle());
+
+ if (apiResponse == null || apiResponse.lyrics() == null || apiResponse.lyrics().isBlank()){
+ throw new LyricsNotFoundException(track.trackTitle(), track.artistName());
+ }
+
+ List lines = Arrays.asList(apiResponse.lyrics().split("\n"));
+
+ return new TrackLyricResponse(
+ track.trackId(),
+ track.trackTitle(),
+ track.artistName(),
+ lines
+ );
+ }
+}
diff --git a/postify/src/main/java/net/hackyourfuture/backend/week6/postify/services/UserStatisticsService.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/services/UserStatisticsService.java
new file mode 100644
index 0000000..f1f7d2e
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/services/UserStatisticsService.java
@@ -0,0 +1,25 @@
+package net.hackyourfuture.backend.week6.postify.services;
+
+import net.hackyourfuture.backend.week6.postify.dto.response.user.UserStatisticsResponse;
+import net.hackyourfuture.backend.week6.postify.exceptions.UserNotFoundException;
+import net.hackyourfuture.backend.week6.postify.repository.UserStatisticsRepository;
+import org.springframework.stereotype.Service;
+
+@Service
+public class UserStatisticsService {
+
+ private final UserStatisticsRepository repository;
+
+ public UserStatisticsService(UserStatisticsRepository repository){
+ this.repository = repository;
+ }
+
+ public UserStatisticsResponse getStatisticsForUser(int userId) {
+ if (!repository.userExists(userId)) {
+ throw new UserNotFoundException(userId);
+ }
+
+ return repository.fetchStatistics(userId).orElseThrow(() -> new UserNotFoundException(userId));
+ }
+}
+
diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties
index 8f4842f..6792daf 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=postgres
+spring.datasource.password=postgres
+spring.datasource.driver-class-name=org.postgresql.Driver
diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/PostifyApplicationTests.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/PostifyApplicationTests.java
deleted file mode 100644
index f5ec046..0000000
--- a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/PostifyApplicationTests.java
+++ /dev/null
@@ -1,13 +0,0 @@
-package net.hackyourfuture.backend.week6.postify;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.boot.test.context.SpringBootTest;
-
-@SpringBootTest
-class PostifyApplicationTests {
-
- @Test
- void contextLoads() {
- }
-
-}
diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controllers/TrackLyricsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controllers/TrackLyricsControllerTest.java
new file mode 100644
index 0000000..17d64d6
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controllers/TrackLyricsControllerTest.java
@@ -0,0 +1,63 @@
+package net.hackyourfuture.backend.week6.postify.controllers;
+
+import net.hackyourfuture.backend.week6.postify.client.LyricsClient;
+import net.hackyourfuture.backend.week6.postify.dto.response.lyric.LyricApiResponse;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.test.web.servlet.MockMvc;
+
+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.*;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class TrackLyricsControllerTest{
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @MockBean
+ private LyricsClient lyricsClient;
+
+ @Test
+ void getLyrics_existingTrack_returnsLyrics() throws Exception{
+
+ when(lyricsClient.getLyrics("Billie Eilish", "LUNCH"))
+ .thenReturn(new LyricApiResponse("""
+ Line 1
+ Line 2
+ """));
+
+ 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[0]").value("Line 1"))
+ .andExpect(jsonPath("$.lyrics[1]").value("Line 2"));
+
+
+ }
+
+ @Test
+ void getLyrics_nonExistingTrack_returns404() throws Exception {
+ mockMvc.perform(get("/tracks/9999/lyrics"))
+ .andExpect(status().isNotFound())
+ .andExpect(content().string("Track with id 9999 not found"));
+ }
+
+ @Test
+ void getLyrics_existingTrackButNoLyrics_returns404() throws Exception {
+
+ when(lyricsClient.getLyrics("Billie Eilish", "LUNCH"))
+ .thenReturn(new LyricApiResponse(""));
+
+ mockMvc.perform(get("/tracks/41/lyrics"))
+ .andExpect(status().isNotFound())
+ .andExpect(content().string( "Lyrics not found for track 'LUNCH' by 'Billie Eilish'"));
+ }
+}
diff --git a/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controllers/UserStatisticsControllerTest.java b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controllers/UserStatisticsControllerTest.java
new file mode 100644
index 0000000..984dc77
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controllers/UserStatisticsControllerTest.java
@@ -0,0 +1,38 @@
+package net.hackyourfuture.backend.week6.postify.controllers;
+
+
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.web.servlet.MockMvc;
+
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
+
+@SpringBootTest
+@AutoConfigureMockMvc
+class UserStatisticsControllerTest {
+
+ @Autowired
+ private MockMvc mockMvc;
+
+ @Test
+ void getUserStats_existingUser_returnsOkAndBody() 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("$.totalStreams").isNumber());
+ }
+
+ @Test
+ void getUserStats_nonExistingUser_returns404() throws Exception {
+ mockMvc.perform(get("/users/9999/stats"))
+ .andExpect(status().isNotFound())
+ .andExpect(content().string("User with id 9999 not found"));
+
+ }
+}
\ No newline at end of file