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 2883aca..a13f721 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 @@ -61,6 +61,15 @@ class FilmRepository( filmRowMapper, ) + override fun findByTitle(title: String): Film? { + val films = jdbc.query( + "SELECT id, title, description FROM films WHERE title = ?", + filmRowMapper, + title + ) + return films.firstOrNull() + } + override fun deleteById(id: UUID) { jdbc.update("DELETE FROM films WHERE id = ?", id) } 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 d4d52ef..659fdf6 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt @@ -1,5 +1,6 @@ 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 @@ -8,6 +9,7 @@ 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 org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.PatchMapping @@ -17,6 +19,7 @@ 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 @@ -25,6 +28,7 @@ class FilmController( private val createFilmUseCase: CreateFilmUseCase, private val editFilmUseCase: EditFilmUseCase, private val deleteFilmUseCase: DeleteFilmUseCase, + private val filmService: FilmService, ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -61,4 +65,10 @@ class FilmController( fun delete( @PathVariable id: UUID, ) = deleteFilmUseCase.delete(id) + + @GetMapping("/search") + fun searchByTitle( + @RequestParam title: String, + ): FilmResponse? = + filmService.findByTitle(title)?.let { FilmResponse.fromDomain(it) } } 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 59022b0..32937c6 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -2,6 +2,7 @@ package com.project.movienight.adapters.web import com.project.movienight.adapters.web.dto.request.CreateFilmLibraryRequest import com.project.movienight.adapters.web.dto.response.FilmLibraryResponse +import com.project.movienight.adapters.web.dto.response.FilmResponse import com.project.movienight.application.ports.input.AddFilmToLibraryCommand import com.project.movienight.application.ports.input.AddFilmToLibraryUseCase import com.project.movienight.application.ports.input.CreateFilmLibraryCommand @@ -10,6 +11,7 @@ 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 org.springframework.http.HttpStatus import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping @@ -19,6 +21,7 @@ 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 @@ -28,6 +31,7 @@ class FilmLibraryController( private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase, private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase, private val getFilmLibraryUseCase: GetFilmLibraryUseCase, + private val filmService: FilmService, ) { @PostMapping @ResponseStatus(HttpStatus.CREATED) @@ -80,4 +84,19 @@ class FilmLibraryController( filmId = filmId, ), ) + + @GetMapping("/available-films") + fun getAvailableFilms( + @PathVariable userId: UUID, + ): List { + val userLibrary = getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId) + ) + + val allFilms = filmService.findAll() + + val availableFilms = allFilms.filter { it.id != userLibrary.filmId } + + return availableFilms.map { FilmResponse.fromDomain(it) } + } } diff --git a/src/main/kotlin/com/project/movienight/application/ports/output/FilmRepositoryPort.kt b/src/main/kotlin/com/project/movienight/application/ports/output/FilmRepositoryPort.kt index 91d45b6..c0d3938 100644 --- a/src/main/kotlin/com/project/movienight/application/ports/output/FilmRepositoryPort.kt +++ b/src/main/kotlin/com/project/movienight/application/ports/output/FilmRepositoryPort.kt @@ -10,5 +10,7 @@ interface FilmRepositoryPort { fun findAll(): List + fun findByTitle(title: String): Film? + fun deleteById(id: UUID) } 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 4166760..9410c3f 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -62,4 +62,8 @@ class FilmService( filmRepository.deleteById(id) } + + fun findByTitle(title: String): Film? = filmRepository.findByTitle(title) + + fun findAll(): List = filmRepository.findAll() } diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt new file mode 100644 index 0000000..c28b5f5 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt @@ -0,0 +1,131 @@ +package com.project.movienight.controllers + +import com.fasterxml.jackson.databind.ObjectMapper +import com.project.movienight.adapters.web.dto.request.CreateFilmRequest +import com.project.movienight.adapters.web.dto.request.EditFilmRequest +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.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.* + +@SpringBootTest +@AutoConfigureMockMvc +class FilmControllerTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var objectMapper: ObjectMapper + + @Test + fun `create film should return 201 CREATED`() { + val request = CreateFilmRequest( + title = "The Matrix", + description = "A computer hacker learns about the true nature of reality" + ) + + mockMvc.perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isCreated) + .andExpect(jsonPath("$.title").value("The Matrix")) + .andExpect(jsonPath("$.description").value("A computer hacker learns about the true nature of reality")) + .andExpect(jsonPath("$.id").exists()) + } + + + @Test + fun `edit film should return updated film`() { + val createRequest = CreateFilmRequest( + title = "Old Title", + description = "Old Description" + ) + + val response = mockMvc.perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createRequest)) + ).andReturn() + + val filmId = objectMapper.readTree(response.response.contentAsString).get("id").asText() + + val editRequest = EditFilmRequest( + title = "New Title", + description = "New Description" + ) + + mockMvc.perform( + patch("/api/films/$filmId") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(editRequest)) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.title").value("New Title")) + .andExpect(jsonPath("$.description").value("New Description")) + } + + + @Test + fun `search film by title should return film`() { + val request = CreateFilmRequest( + title = "Inception", + description = "Dream within a dream" + ) + + mockMvc.perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + + mockMvc.perform( + get("/api/films/search") + .param("title", "Inception") + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.title").value("Inception")) + .andExpect(jsonPath("$.description").value("Dream within a dream")) + } + + + @Test + fun `search film by non-existent title should return empty`() { + mockMvc.perform( + get("/api/films/search") + .param("title", "NonExistentFilm12345") + ) + .andExpect(status().isOk) + .andExpect(content().string("")) + } + + + @Test + fun `delete film should return 204 NO CONTENT`() { + val request = CreateFilmRequest( + title = "Film To Delete", + description = "This film will be deleted" + ) + + val response = mockMvc.perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andReturn() + + val filmId = objectMapper.readTree(response.response.contentAsString).get("id").asText() + + mockMvc.perform(delete("/api/films/$filmId")) + .andExpect(status().isNoContent()) + + mockMvc.perform(get("/api/films/search").param("title", "Film To Delete")) + .andExpect(status().isOk) + .andExpect(content().string("")) + } +} diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt new file mode 100644 index 0000000..39b795d --- /dev/null +++ b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt @@ -0,0 +1,74 @@ +package com.project.movienight.controllers + +import com.fasterxml.jackson.databind.ObjectMapper +import com.project.movienight.adapters.web.dto.request.CreateFilmRequest +import com.project.movienight.adapters.web.dto.request.CreateUserRequest +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.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.transaction.annotation.Transactional + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class FilmLibraryControllerTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var objectMapper: ObjectMapper + + + @Test + fun `add film to library should work`() { + val userRequest = CreateUserRequest("Film Adder", "adder@example.com") + val userResponse = mockMvc.perform( + post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(userRequest)) + ).andReturn() + val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() + + val filmRequest = CreateFilmRequest("Library Film", "Film description") + val filmResponse = mockMvc.perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(filmRequest)) + ).andReturn() + val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText() + + mockMvc.perform( + post("/api/users/$userId/library/films/$filmId") + ) + .andExpect(status().isCreated) + } + + @Test + fun `remove film from library should return 204`() { + val userRequest = CreateUserRequest("Remove Film", "remove@example.com") + val userResponse = mockMvc.perform( + post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .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 filmResponse = mockMvc.perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(filmRequest)) + ).andReturn() + val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText() + + mockMvc.perform(post("/api/users/$userId/library/films/$filmId")) + mockMvc.perform(delete("/api/users/$userId/library/films/$filmId")) + .andExpect(status().isNoContent()) + } +} diff --git a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt new file mode 100644 index 0000000..df266fe --- /dev/null +++ b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt @@ -0,0 +1,98 @@ +package com.project.movienight.controllers + +import com.fasterxml.jackson.databind.ObjectMapper +import com.project.movienight.adapters.web.dto.request.CreateUserRequest +import com.project.movienight.adapters.web.dto.request.EditUserRequest +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.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.transaction.annotation.Transactional + +@SpringBootTest +@AutoConfigureMockMvc +@Transactional +class UserControllerTest { + + @Autowired + private lateinit var mockMvc: MockMvc + + @Autowired + private lateinit var objectMapper: ObjectMapper + + @Test + fun `create user should return 201 CREATED`() { + val request = CreateUserRequest( + name = "John Doe", + email = "john@example.com" + ) + + mockMvc.perform( + post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ) + .andExpect(status().isCreated) + .andExpect(jsonPath("$.name").value("John Doe")) + .andExpect(jsonPath("$.email").value("john@example.com")) + .andExpect(jsonPath("$.id").exists()) + } + + + @Test + fun `edit user should return updated user`() { + val createRequest = CreateUserRequest( + name = "Old Name", + email = "edit@example.com" + ) + + val response = mockMvc.perform( + post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(createRequest)) + ).andReturn() + + val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText() + + val editRequest = EditUserRequest(name = "New Name") + + mockMvc.perform( + patch("/api/users/$userId") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(editRequest)) + ) + .andExpect(status().isOk) + .andExpect(jsonPath("$.name").value("New Name")) + .andExpect(jsonPath("$.email").value("edit@example.com")) + } + + @Test + fun `delete user should return 204 NO CONTENT`() { + val request = CreateUserRequest( + name = "User To Delete", + email = "delete@example.com" + ) + + val response = mockMvc.perform( + post("/api/users") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request)) + ).andReturn() + + val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText() + + mockMvc.perform(delete("/api/users/$userId")) + .andExpect(status().isNoContent()) + } + + @Test + fun `delete non-existent user should return 404`() { + val nonExistentId = "123e4567-e89b-12d3-a456-426614174000" + mockMvc.perform(delete("/api/users/$nonExistentId")) + .andExpect(status().isNotFound()) + } +}