Skip to content

Commit

Permalink
Add endpoint to retrieve all songs persisted in the database
Browse files Browse the repository at this point in the history
  • Loading branch information
Migwel authored and Migwel committed Feb 11, 2024
1 parent d66a982 commit c5c2fb6
Show file tree
Hide file tree
Showing 14 changed files with 433 additions and 2 deletions.
6 changes: 5 additions & 1 deletion spotbugsExclude.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
<Class name="dev.migwel.sts.spotify.SpotifyAuthenticationService"/>
<Class name="dev.migwel.sts.sonos.SonosService"/>
<Class name="dev.migwel.sts.sonos.SonosAuthenticationService"/>
<Class name="~dev.migwel.sts.controller.api.dto.*"/>
</Or>
<Or>
<Bug code="EI" />
<Bug code="EI2" />
</Or>
<Bug code="EI2" />
</Match>
<Match>
<Or>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package dev.migwel.sts.controller.api;

import dev.migwel.sts.controller.api.dto.GetSongsResponse;
import dev.migwel.sts.controller.api.dto.database.PersistedSong;
import dev.migwel.sts.database.DatabaseService;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Collections;
import java.util.List;

@Controller
@RequestMapping("/api/database")
public class DatabaseController {

private final DatabaseService databaseService;
private final ResultConverter converter;

@Autowired
public DatabaseController(DatabaseService databaseService, ResultConverter converter) {
this.databaseService = databaseService;
this.converter = converter;
}

@GetMapping("/songs")
ResponseEntity<GetSongsResponse> getSongs(
Authentication authentication,
@RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "limit", required = false) Integer limit) {
List<dev.migwel.sts.domain.model.PersistedSong> songs =
databaseService.getSongs(
authentication.getName(),
limit != null ? limit : 50,
page != null ? page : 0);
if (songs.isEmpty()) {
return new ResponseEntity<>(
new GetSongsResponse(Collections.emptyList()), HttpStatus.NO_CONTENT);
}
List<PersistedSong> dtoSongs = songs.stream().map(converter::convert).toList();
var response = new GetSongsResponse(dtoSongs);
return new ResponseEntity<>(response, HttpStatus.OK);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@
import dev.migwel.sts.controller.api.dto.SaveResponse;
import dev.migwel.sts.controller.api.dto.Song;
import dev.migwel.sts.controller.api.dto.ToResult;
import dev.migwel.sts.controller.api.dto.database.PersistedSong;
import dev.migwel.sts.domain.model.SaveResult;

import org.springframework.stereotype.Component;

import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;

@Component
Expand All @@ -30,4 +32,9 @@ public Song convert(@CheckForNull dev.migwel.sts.domain.model.Song song) {
}
return new Song(song.artist(), song.title(), song.rawData());
}

@Nonnull
public PersistedSong convert(@Nonnull dev.migwel.sts.domain.model.PersistedSong persistedSong) {
return new PersistedSong(convert(persistedSong.song()), persistedSong.creationDate());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.migwel.sts.controller.api.dto;

import dev.migwel.sts.controller.api.dto.database.PersistedSong;

import java.util.List;

public record GetSongsResponse(List<PersistedSong> songs) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package dev.migwel.sts.controller.api.dto.database;

import dev.migwel.sts.controller.api.dto.Song;

import java.util.Date;

public record PersistedSong(Song song, Date creationDate) {}
44 changes: 44 additions & 0 deletions src/main/java/dev/migwel/sts/database/DatabaseService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package dev.migwel.sts.database;

import dev.migwel.sts.database.entities.Converter;
import dev.migwel.sts.domain.model.PersistedSong;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Comparator;
import java.util.List;

import javax.annotation.Nonnull;
import javax.annotation.ParametersAreNonnullByDefault;

@Service
@ParametersAreNonnullByDefault
public class DatabaseService {

private final SongRepository songRepository;
private final Converter converter;
private static final int MAX_LIMIT = 50;

@Autowired
public DatabaseService(SongRepository songRepository, Converter converter) {
this.songRepository = songRepository;
this.converter = converter;
}

@Nonnull
public List<PersistedSong> getSongs(String username, int limit, int page) {
if (page < 0) {
throw new IllegalArgumentException("page cannot be negative");
}
if (limit < 0) {
throw new IllegalArgumentException("limit cannot be negative");
}
List<dev.migwel.sts.database.entities.Song> songs =
songRepository.findByUsername(username, Math.min(limit, MAX_LIMIT), page);
return songs.stream()
.map(converter::convertToPersistedSong)
.sorted(Comparator.comparing(PersistedSong::creationDate))
.toList();
}
}
14 changes: 14 additions & 0 deletions src/main/java/dev/migwel/sts/database/SongRepository.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
package dev.migwel.sts.database;

import dev.migwel.sts.database.entities.Song;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;

import java.util.List;
import java.util.Optional;

public interface SongRepository extends Repository<Song, Long> {
Song save(Song song);

Optional<Song> findByArtistAndTitle(String artist, String title);

@Query(
"""
select
s
from Song s
where s.username =?1
order by s.creationDate
limit ?2
offset ?3
""")
List<Song> findByUsername(String username, int limit, int offset);
}
5 changes: 5 additions & 0 deletions src/main/java/dev/migwel/sts/database/entities/Converter.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,9 @@ public dev.migwel.sts.domain.model.Song convert(Song entity) {
return new dev.migwel.sts.domain.model.Song(
entity.getArtist(), entity.getTitle(), entity.getRawData());
}

public dev.migwel.sts.domain.model.PersistedSong convertToPersistedSong(Song entity) {
return new dev.migwel.sts.domain.model.PersistedSong(
convert(entity), entity.getUsername(), entity.getCreationDate());
}
}
15 changes: 15 additions & 0 deletions src/main/java/dev/migwel/sts/database/entities/Song.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ public Song(String username, String artist, String title, String rawData) {
this.rawData = rawData;
}

public Song(
Long id,
String username,
String artist,
String title,
String rawData,
Date creationDate) {
this.id = id;
this.username = username;
this.artist = artist;
this.title = title;
this.rawData = rawData;
this.creationDate = new Date(creationDate.getTime());
}

public Long getId() {
return id;
}
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/dev/migwel/sts/domain/model/PersistedSong.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package dev.migwel.sts.domain.model;

import javax.annotation.ParametersAreNonnullByDefault;
import java.util.Date;

@ParametersAreNonnullByDefault
public record PersistedSong(Song song, String username, Date creationDate) {
public PersistedSong(Song song, String username, Date creationDate) {
this.song = song;
this.username = username;
this.creationDate = new Date(creationDate.getTime());
}

public Date creationDate() {
return new Date(creationDate.getTime());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package dev.migwel.sts.controller.api;

import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.verify;
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.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import dev.migwel.sts.SecurityConfig;
import dev.migwel.sts.database.DatabaseService;
import dev.migwel.sts.domain.model.PersistedSong;
import dev.migwel.sts.domain.model.Song;

import dev.migwel.sts.util.FileUtil;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import org.springframework.security.test.context.support.WithAnonymousUser;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;

import java.time.Instant;
import java.util.Collections;
import java.util.Date;
import java.util.List;

@Import(value = {SecurityConfig.class, Converter.class, ResultConverter.class})
@AutoConfigureTestDatabase
@WebMvcTest(DatabaseController.class)
class DatabaseControllerTest {

@Autowired private MockMvc mockMvc;

@Autowired private ResultConverter resultConverter;
@MockBean private DatabaseService databaseService;

@Test
@WithAnonymousUser
void getSongs_withoutAuthenticatedUser() throws Exception {
this.mockMvc.perform(get("/api/database/songs")).andExpect(status().isUnauthorized());
;
}

@Test
@WithMockUser
void getSongs_noSongFound() throws Exception {
when(databaseService.getSongs(any(), anyInt(), anyInt()))
.thenReturn(Collections.emptyList());
this.mockMvc
.perform(get("/api/database/songs"))
.andExpect(status().isNoContent())
.andExpect(content().string("{\"songs\":[]}"));
}

@Test
@WithMockUser
void getSongs_songsFound() throws Exception {
when(databaseService.getSongs(any(), anyInt(), anyInt()))
.thenReturn(
List.of(
new PersistedSong(
new Song("artist", "title", "artist - title"),
"username",
Date.from(Instant.parse("2023-01-01T10:00:00.00Z"))),
new PersistedSong(
new Song("artist", "otherTitle", "artist - otherTitle"),
"username",
Date.from(Instant.parse("2023-01-02T10:00:00.00Z")))));
this.mockMvc
.perform(get("/api/database/songs"))
.andExpect(status().isOk())
.andExpect(
content()
.string(
FileUtil.loadFile(
"database/getSongs_songsFound_response.json")));
}

@Test
@WithMockUser
void getSongs_noLimitOrPageProvided() throws Exception {
this.mockMvc.perform(get("/api/database/songs")).andExpect(status().isNoContent());
verify(databaseService).getSongs(any(), intThat(e -> e > 0), eq(0));
}

@Test
@WithMockUser
void getSongs_limitAndPageProvided() throws Exception {
int limit = 25;
int page = 2;
this.mockMvc
.perform(
get("/api/database/songs")
.param("limit", Integer.toString(limit))
.param("page", Integer.toString(page)))
.andExpect(status().isNoContent());
verify(databaseService).getSongs(any(), eq(limit), eq(page));
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package dev.migwel.sts.controller.api;

import static org.junit.jupiter.api.Assertions.*;

import dev.migwel.sts.controller.api.dto.SaveResponse;
import dev.migwel.sts.domain.model.PersistedSong;
import dev.migwel.sts.domain.model.SaveResult;
import dev.migwel.sts.domain.model.Song;
import dev.migwel.sts.domain.model.ToResult;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import java.util.Date;

class ResultConverterTest {

Expand Down Expand Up @@ -52,4 +56,19 @@ void saveResultconvert_saveResultSuccess() {
assertTrue(response.toResult().success());
assertNull(response.toResult().errorMessage());
}

@Test
void convert_persistedSong() {
PersistedSong song =
new PersistedSong(
new Song("artist", "title", "artist - title"), "username", new Date());
var convertedSong = converter.convert(song);

assertNotNull(convertedSong);
assertNotNull(convertedSong.song());
assertEquals(convertedSong.song().artist(), song.song().artist());
assertEquals(convertedSong.song().title(), song.song().title());
assertEquals(convertedSong.song().rawData(), song.song().rawData());
assertEquals(convertedSong.creationDate(), song.creationDate());
}
}
Loading

0 comments on commit c5c2fb6

Please sign in to comment.