diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt index a13f721..9f4d80b 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/FilmRepository.kt @@ -62,11 +62,12 @@ class FilmRepository( ) override fun findByTitle(title: String): Film? { - val films = jdbc.query( - "SELECT id, title, description FROM films WHERE title = ?", - filmRowMapper, - title - ) + val films = + jdbc.query( + "SELECT id, title, description FROM films WHERE title = ? ORDER BY id LIMIT 1", + filmRowMapper, + title, + ) return films.firstOrNull() } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt index 659fdf6..df375e4 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt @@ -1,6 +1,5 @@ package com.project.movienight.adapters.web - import com.project.movienight.adapters.web.dto.request.CreateFilmRequest import com.project.movienight.adapters.web.dto.request.EditFilmRequest import com.project.movienight.adapters.web.dto.response.FilmResponse @@ -9,17 +8,21 @@ import com.project.movienight.application.ports.input.CreateFilmUseCase import com.project.movienight.application.ports.input.DeleteFilmUseCase import com.project.movienight.application.ports.input.EditFilmCommand import com.project.movienight.application.ports.input.EditFilmUseCase -import com.project.movienight.application.services.FilmService +import com.project.movienight.application.ports.input.GetAllFilmsUseCase +import com.project.movienight.application.ports.input.GetFilmByIdUseCase +import com.project.movienight.application.ports.input.SearchFilmByTitleUseCase import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.DeleteMapping +import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PatchMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController -import org.springframework.web.bind.annotation.* import java.util.UUID @RestController @@ -28,7 +31,9 @@ class FilmController( private val createFilmUseCase: CreateFilmUseCase, private val editFilmUseCase: EditFilmUseCase, private val deleteFilmUseCase: DeleteFilmUseCase, - private val filmService: FilmService, + private val getFilmByIdUseCase: GetFilmByIdUseCase, + private val getAllFilmsUseCase: GetAllFilmsUseCase, + private val searchFilmByTitleUseCase: SearchFilmByTitleUseCase, ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -52,11 +57,10 @@ class FilmController( FilmResponse.fromDomain( editFilmUseCase.edit( id = id, - command = - EditFilmCommand( - title = request.title, - description = request.description, - ), + command = EditFilmCommand( + title = request.title, + description = request.description, + ), ), ) @@ -66,9 +70,25 @@ class FilmController( @PathVariable id: UUID, ) = deleteFilmUseCase.delete(id) + @GetMapping("/{id}") + fun getById( + @PathVariable id: UUID, + ): FilmResponse = + FilmResponse.fromDomain(getFilmByIdUseCase.getById(id)) + + @GetMapping + fun getAll(): List = + getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) } + @GetMapping("/search") fun searchByTitle( @RequestParam title: String, - ): FilmResponse? = - filmService.findByTitle(title)?.let { FilmResponse.fromDomain(it) } + ): ResponseEntity { + val film = searchFilmByTitleUseCase.searchByTitle(title) + return if (film != null) { + ResponseEntity.ok(FilmResponse.fromDomain(film)) + } else { + ResponseEntity.notFound().build() + } + } } diff --git a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt index 32937c6..a4a6175 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -7,11 +7,13 @@ import com.project.movienight.application.ports.input.AddFilmToLibraryCommand import com.project.movienight.application.ports.input.AddFilmToLibraryUseCase import com.project.movienight.application.ports.input.CreateFilmLibraryCommand import com.project.movienight.application.ports.input.CreateFilmLibraryUseCase +import com.project.movienight.application.ports.input.GetAllFilmsUseCase +import com.project.movienight.application.ports.input.GetFilmByIdUseCase import com.project.movienight.application.ports.input.GetFilmLibraryQuery import com.project.movienight.application.ports.input.GetFilmLibraryUseCase import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand import com.project.movienight.application.ports.input.RemoveFilmFromLibraryUseCase -import com.project.movienight.application.services.FilmService +import com.project.movienight.domain.exception.EntityNotFoundException import org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -21,7 +23,6 @@ import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.RestController -import org.springframework.web.bind.annotation.* import java.util.UUID @RestController @@ -31,7 +32,8 @@ class FilmLibraryController( private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase, private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase, private val getFilmLibraryUseCase: GetFilmLibraryUseCase, - private val filmService: FilmService, + private val getFilmByIdUseCase: GetFilmByIdUseCase, + private val getAllFilmsUseCase: GetAllFilmsUseCase, ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -58,6 +60,17 @@ class FilmLibraryController( ), ) + @GetMapping("/films") + fun getAllFilmsInLibrary( + @PathVariable userId: UUID, + ): List { + val library = getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId), + ) + val film = getFilmByIdUseCase.getById(library.filmId) + return listOf(FilmResponse.fromDomain(film)) + } + @PostMapping("/films/{filmId}") @ResponseStatus(HttpStatus.CREATED) fun addFilm( @@ -89,13 +102,21 @@ class FilmLibraryController( fun getAvailableFilms( @PathVariable userId: UUID, ): List { - val userLibrary = getFilmLibraryUseCase.getLibrary( - GetFilmLibraryQuery(userId = userId) - ) + val userLibrary = try { + getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId), + ) + } catch (e: EntityNotFoundException) { + null + } - val allFilms = filmService.findAll() + val allFilms = getAllFilmsUseCase.getAll() - val availableFilms = allFilms.filter { it.id != userLibrary.filmId } + val availableFilms = if (userLibrary != null) { + allFilms.filter { it.id != userLibrary.filmId } + } else { + allFilms + } return availableFilms.map { FilmResponse.fromDomain(it) } } diff --git a/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt b/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt index 3622878..db3f4b0 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/input/FilmUseCase.kt @@ -27,3 +27,15 @@ data class EditFilmCommand( interface DeleteFilmUseCase { fun delete(id: UUID) } + +interface GetFilmByIdUseCase { + fun getById(id: UUID): Film +} + +interface GetAllFilmsUseCase { + fun getAll(): List +} + +interface SearchFilmByTitleUseCase { + fun searchByTitle(title: String): Film? +} diff --git a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt index 9410c3f..555655a 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -5,6 +5,9 @@ import com.project.movienight.application.ports.input.CreateFilmUseCase import com.project.movienight.application.ports.input.DeleteFilmUseCase import com.project.movienight.application.ports.input.EditFilmCommand import com.project.movienight.application.ports.input.EditFilmUseCase +import com.project.movienight.application.ports.input.GetAllFilmsUseCase +import com.project.movienight.application.ports.input.GetFilmByIdUseCase +import com.project.movienight.application.ports.input.SearchFilmByTitleUseCase import com.project.movienight.application.ports.output.FilmRepositoryPort import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.config.FilmServiceProperties @@ -21,7 +24,11 @@ class FilmService( private val filmConfig: FilmServiceProperties, ) : CreateFilmUseCase, EditFilmUseCase, - DeleteFilmUseCase { + DeleteFilmUseCase, + GetFilmByIdUseCase, + GetAllFilmsUseCase, + SearchFilmByTitleUseCase { + override fun create(command: CreateFilmCommand): Film { if (filmConfig.isBlocked(command.title)) { throw BlockedValueException(target = "Film", field = "title") @@ -30,19 +37,15 @@ class FilmService( throw BlockedValueException(target = "Film", field = "description") } - val film = - Film( - id = idGenerator.generateId(), - title = command.title, - description = command.description, - ) + val film = Film( + id = idGenerator.generateId(), + title = command.title, + description = command.description, + ) return filmRepository.save(film) } - override fun edit( - id: UUID, - command: EditFilmCommand, - ): Film { + override fun edit(id: UUID, command: EditFilmCommand): Film { if (filmConfig.isBlocked(command.title)) { throw BlockedValueException(target = "Film", field = "title") } @@ -51,19 +54,20 @@ class FilmService( } var film = filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) - film = film.copy(title = command.title, description = command.description) - return filmRepository.save(film) } override fun delete(id: UUID) { filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) - filmRepository.deleteById(id) } - fun findByTitle(title: String): Film? = filmRepository.findByTitle(title) + override fun getById(id: UUID): Film { + return filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) + } - fun findAll(): List = filmRepository.findAll() + override fun getAll(): List = filmRepository.findAll() + + override fun searchByTitle(title: String): Film? = filmRepository.findByTitle(title) } diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt index c28b5f5..0784280 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt @@ -26,7 +26,7 @@ class FilmControllerTest { fun `create film should return 201 CREATED`() { val request = CreateFilmRequest( title = "The Matrix", - description = "A computer hacker learns about the true nature of reality" + description = "A computer hacker learns about the true nature of reality", ) mockMvc.perform( @@ -40,7 +40,6 @@ class FilmControllerTest { .andExpect(jsonPath("$.id").exists()) } - @Test fun `edit film should return updated film`() { val createRequest = CreateFilmRequest( @@ -58,7 +57,7 @@ class FilmControllerTest { val editRequest = EditFilmRequest( title = "New Title", - description = "New Description" + description = "New Description", ) mockMvc.perform( @@ -71,7 +70,6 @@ class FilmControllerTest { .andExpect(jsonPath("$.description").value("New Description")) } - @Test fun `search film by title should return film`() { val request = CreateFilmRequest( @@ -94,7 +92,6 @@ class FilmControllerTest { .andExpect(jsonPath("$.description").value("Dream within a dream")) } - @Test fun `search film by non-existent title should return empty`() { mockMvc.perform( @@ -105,7 +102,6 @@ class FilmControllerTest { .andExpect(content().string("")) } - @Test fun `delete film should return 204 NO CONTENT`() { val request = CreateFilmRequest( diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt index 39b795d..d24e60e 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt @@ -9,8 +9,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.transaction.annotation.Transactional @SpringBootTest @@ -24,46 +25,57 @@ class FilmLibraryControllerTest { @Autowired private lateinit var objectMapper: ObjectMapper - @Test fun `add film to library should work`() { - val userRequest = CreateUserRequest("Film Adder", "adder@example.com") + val userRequest = CreateUserRequest( + name = "Film Adder", + email = "adder@example.com", + ) val userResponse = mockMvc.perform( post("/api/users") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(userRequest)) + .content(objectMapper.writeValueAsString(userRequest)), ).andReturn() val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() - val filmRequest = CreateFilmRequest("Library Film", "Film description") + val filmRequest = CreateFilmRequest( + title = "Library Film", + description = "Film description", + ) val filmResponse = mockMvc.perform( post("/api/films") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(filmRequest)) + .content(objectMapper.writeValueAsString(filmRequest)), ).andReturn() val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText() mockMvc.perform( - post("/api/users/$userId/library/films/$filmId") + post("/api/users/$userId/library/films/$filmId"), ) - .andExpect(status().isCreated) + .andExpect(status().isCreated()) } @Test fun `remove film from library should return 204`() { - val userRequest = CreateUserRequest("Remove Film", "remove@example.com") + val userRequest = CreateUserRequest( + name = "Remove Film", + email = "remove@example.com", + ) val userResponse = mockMvc.perform( post("/api/users") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(userRequest)) + .content(objectMapper.writeValueAsString(userRequest)), ).andReturn() val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() - val filmRequest = CreateFilmRequest("Film To Remove", "Will be removed") + val filmRequest = CreateFilmRequest( + title = "Film To Remove", + description = "Will be removed", + ) val filmResponse = mockMvc.perform( post("/api/films") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(filmRequest)) + .content(objectMapper.writeValueAsString(filmRequest)), ).andReturn() val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText() diff --git a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt index df266fe..db2eecc 100644 --- a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt @@ -9,8 +9,11 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock import org.springframework.boot.test.context.SpringBootTest import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc -import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* -import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.patch +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status import org.springframework.transaction.annotation.Transactional @SpringBootTest @@ -28,13 +31,13 @@ class UserControllerTest { fun `create user should return 201 CREATED`() { val request = CreateUserRequest( name = "John Doe", - email = "john@example.com" + email = "john@example.com", ) mockMvc.perform( post("/api/users") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(request)), ) .andExpect(status().isCreated) .andExpect(jsonPath("$.name").value("John Doe")) @@ -42,18 +45,17 @@ class UserControllerTest { .andExpect(jsonPath("$.id").exists()) } - @Test fun `edit user should return updated user`() { val createRequest = CreateUserRequest( name = "Old Name", - email = "edit@example.com" + email = "edit@example.com", ) val response = mockMvc.perform( post("/api/users") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(createRequest)) + .content(objectMapper.writeValueAsString(createRequest)), ).andReturn() val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText() @@ -63,7 +65,7 @@ class UserControllerTest { mockMvc.perform( patch("/api/users/$userId") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(editRequest)) + .content(objectMapper.writeValueAsString(editRequest)), ) .andExpect(status().isOk) .andExpect(jsonPath("$.name").value("New Name")) @@ -74,13 +76,13 @@ class UserControllerTest { fun `delete user should return 204 NO CONTENT`() { val request = CreateUserRequest( name = "User To Delete", - email = "delete@example.com" + email = "delete@example.com", ) val response = mockMvc.perform( post("/api/users") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(request)), ).andReturn() val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText()