diff --git a/postify/pom.xml b/postify/pom.xml
index 7cea5f7..e46cbe4 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
@@ -27,14 +27,21 @@
- 25
+ 17
org.springframework.boot
- spring-boot-starter-webmvc
+ spring-boot-starter-web
+
+
+ org.springframework.boot
+ spring-boot-starter-jdbc
+
+
+ org.springframework.boot
+ spring-boot-starter-webflux
-
org.postgresql
postgresql
@@ -45,11 +52,17 @@
lombok
true
+
+ io.github.cdimascio
+ java-dotenv
+ 5.2.2
+
org.springframework.boot
- spring-boot-starter-webmvc-test
+ spring-boot-starter-test
test
+
@@ -58,12 +71,7 @@
org.springframework.boot
spring-boot-maven-plugin
-
-
- org.projectlombok
- lombok
-
-
+
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);
}
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..8c6a38d
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/config/RestClientConfig.java
@@ -0,0 +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();
+ }
+}
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..4fd4055
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsController.java
@@ -0,0 +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
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..732b1f8
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsController.java
@@ -0,0 +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);
+ }
+ }
+
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..8420798
--- /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.dto;
+
+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/dto/TrackLyricsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java
new file mode 100644
index 0000000..b13d8d4
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/dto/TrackLyricsResponse.java
@@ -0,0 +1,19 @@
+package net.hackyourfuture.backend.week6.postify.dto;
+
+
+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;
+
+}
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/model/UserStatisticsResponse.java b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java
new file mode 100644
index 0000000..10a3a99
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponse.java
@@ -0,0 +1,36 @@
+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",
+ "userName",
+ "userCountry",
+ "totalStreams",
+ "uniqueTracksStreamed",
+ "uniqueArtistsStreamed",
+ "favoriteGenre",
+ "totalListeningTimeSeconds"
+})
+@Getter
+@Setter
+@AllArgsConstructor
+@NoArgsConstructor
+public class UserStatisticsResponse {
+
+ public int userId;
+ public String userName;
+ public String userCountry;
+ public int totalStreams;
+ public int uniqueTracksStreamed;
+ public int uniqueArtistsStreamed;
+ public String favoriteGenre;
+ public int totalListeningTimeSeconds;
+
+}
+
+
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/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..e567487
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/repository/UserStatisticsRepository.java
@@ -0,0 +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);
+ }
+}
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..dae500d
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/TrackLyricsService.java
@@ -0,0 +1,56 @@
+package net.hackyourfuture.backend.week6.postify.service;
+
+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 org.springframework.stereotype.Service;
+import org.springframework.web.client.HttpClientErrorException;
+import org.springframework.web.client.RestClient;
+
+@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 getLyrics(Long trackId) {
+
+ TrackRepository.TrackWithArtist track =
+ trackRepository.findTrackWithArtistsById(trackId.intValue())
+ .orElseThrow(() ->
+ new TrackNotFoundException(trackId.intValue()));
+
+ try {
+
+ 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()
+ );
+ }
+ }
+}
\ No newline at end of file
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..14b7e60
--- /dev/null
+++ b/postify/src/main/java/net/hackyourfuture/backend/week6/postify/service/UserStatisticsService.java
@@ -0,0 +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
diff --git a/postify/src/main/resources/application.properties b/postify/src/main/resources/application.properties
index 8f4842f..149ea04 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/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
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..b3b6ef6
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/TrackLyricsControllerTest.java
@@ -0,0 +1,90 @@
+package net.hackyourfuture.backend.week6.postify.controller;
+
+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
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..0b35692
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/controller/UserStatisticsControllerTest.java
@@ -0,0 +1,58 @@
+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.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());
+ }
+}
+
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..5f172f2
--- /dev/null
+++ b/postify/src/test/java/net/hackyourfuture/backend/week6/postify/model/UserStatisticsResponseTest.java
@@ -0,0 +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);
+ }
+}
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 {
+}
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 {
+}