diff --git a/postify/pom.xml b/postify/pom.xml
index 7cea5f7..b013601 100644
--- a/postify/pom.xml
+++ b/postify/pom.xml
@@ -45,11 +45,21 @@
lombok
true
+
+
+ org.springframework.boot
+ spring-boot-restclient
+
org.springframework.boot
spring-boot-starter-webmvc-test
test
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
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/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
new file mode 100644
index 0000000..a1f8bc1
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java
@@ -0,0 +1,18 @@
+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(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/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/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/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/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/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/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..6bd946f
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/StreamRepository.java
@@ -0,0 +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;
+
+ 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();
+ }
+}
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..e2d03f1
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/TrackRepository.java
@@ -0,0 +1,37 @@
+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) {
+ 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
new file mode 100644
index 0000000..b1b061e
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java
@@ -0,0 +1,31 @@
+package net.hackyourfuture.backend.week6.postify.service;
+
+import org.springframework.stereotype.Service;
+
+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;
+import net.hackyourfuture.backend.week6.postify.repository.TrackRepository;
+
+@Service
+public class TrackLyricsService {
+ private final TrackRepository trackRepository;
+ private final LyricsApiClient lyricsApiClient;
+
+ public TrackLyricsService(TrackRepository trackRepository, LyricsApiClient lyricsApiClient) {
+ this.trackRepository = trackRepository;
+ this.lyricsApiClient = lyricsApiClient;
+ }
+
+ public TrackLyrics getLyrics(Long trackId) {
+ TrackLyrics track = trackRepository.findTrackWithArtist(trackId)
+ .orElseThrow(() -> new TrackNotFoundException(trackId));
+
+ String lyrics = lyricsApiClient.fetchLyrics(track.getArtistName(), track.getTrackTitle())
+ .orElseThrow(() -> new LyricsNotFoundException(track.getArtistName(), track.getTrackTitle()));
+
+ track.setLyrics(lyrics);
+ return track;
+ }
+}
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..480ab66
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java
@@ -0,0 +1,27 @@
+package net.hackyourfuture.backend.week6.postify.service;
+
+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) {
+ // 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;
+ }
+}
diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties
index 8f4842f..1cf6dfa 100644
--- a/postify/src/main/resources/application.properties
+++ b/postify/src/main/resources/application.properties
@@ -1 +1,7 @@
-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
+
+# External lyrics API (https://lyricsovh.docs.apiary.io)
+lyrics.api.base-url=https://api.lyrics.ovh/v1
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
new file mode 100644
index 0000000..8be41d4
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/UserStatisticsControllerTest.java
@@ -0,0 +1,39 @@
+package net.hackyourfuture.backend.week6.postify;
+
+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
+ 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
+ void returns404ForUnknownUser() throws Exception {
+ mockMvc.perform(get("/users/999999/stats"))
+ .andExpect(status().isNotFound());
+ }
+}