From b7b9061ba6e8181b17248db5a1292b31e58bc8bc Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Sun, 19 Apr 2026 15:46:59 +0300 Subject: [PATCH 1/9] =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B8=D0=BD=D0=B3=20?= =?UTF-8?q?=D1=82=D0=B5=D1=81=D1=82=D1=8B=20=D0=B4=D0=BB=D1=8F=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BD=D1=82=D1=80=D0=BE=D0=BB=D0=BB=D0=B5=D1=80=D0=BE?= =?UTF-8?q?=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controllers/FilmControllerTest.kt | 131 ++++++++++++++++++ .../controllers/FilmLibraryControllerTest.kt | 74 ++++++++++ .../controllers/UserControllerTest.kt | 98 +++++++++++++ 3 files changed, 303 insertions(+) create mode 100644 src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt create mode 100644 src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt create mode 100644 src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt 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()) + } +} -- 2.54.0 From 8926e5b08c95fc5a475d5143988d92bff4188c7a Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Tue, 21 Apr 2026 00:19:14 +0300 Subject: [PATCH 2/9] =?UTF-8?q?=D0=B8=D0=BC=D0=BF=D0=BE=D1=80=D1=82=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../project/movienight/adapters/web/FilmController.kt | 9 +++++++++ .../movienight/adapters/web/FilmLibraryController.kt | 8 ++++++++ 2 files changed, 17 insertions(+) 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 35f963e..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 @@ -10,6 +11,14 @@ 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 +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.ResponseStatus +import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.* import java.util.UUID 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 446e57a..32937c6 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -13,6 +13,14 @@ import com.project.movienight.application.ports.input.RemoveFilmFromLibraryComma 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 +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.ResponseStatus +import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.* import java.util.UUID -- 2.54.0 From f563c3e7cfefdb898eae7250f40b5e680591516c Mon Sep 17 00:00:00 2001 From: Elena Ponomareva Date: Fri, 24 Apr 2026 23:47:56 +0300 Subject: [PATCH 3/9] =?UTF-8?q?=D0=B8=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../persistence/jdbc/FilmRepository.kt | 11 ++--- .../movienight/adapters/web/FilmController.kt | 42 ++++++++++++++----- .../adapters/web/FilmLibraryController.kt | 37 ++++++++++++---- .../application/ports/input/FilmUseCase.kt | 12 ++++++ .../application/services/FilmService.kt | 36 +++++++++------- .../controllers/FilmControllerTest.kt | 8 +--- .../controllers/FilmLibraryControllerTest.kt | 38 +++++++++++------ .../controllers/UserControllerTest.kt | 22 +++++----- 8 files changed, 137 insertions(+), 69 deletions(-) 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() -- 2.54.0 From b7a246e0b40389ba6c21f720fd54ed13a70faaf5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 12:02:31 +0000 Subject: [PATCH 4/9] fix: address copilot review issues - explicit imports, trailing commas, 404 for not-found search, tests for available-films endpoint Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/60cd9b1b-e3e1-46a1-bfd9-04a75bd0d569 Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../movienight/adapters/web/FilmController.kt | 15 +- .../adapters/web/FilmLibraryController.kt | 33 +-- .../application/services/FilmService.kt | 22 +- .../controllers/FilmControllerTest.kt | 137 ++++++------ .../controllers/FilmLibraryControllerTest.kt | 202 ++++++++++++++---- .../controllers/UserControllerTest.kt | 82 +++---- 6 files changed, 317 insertions(+), 174 deletions(-) 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 df375e4..3699e33 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt @@ -57,10 +57,11 @@ class FilmController( FilmResponse.fromDomain( editFilmUseCase.edit( id = id, - command = EditFilmCommand( - title = request.title, - description = request.description, - ), + command = + EditFilmCommand( + title = request.title, + description = request.description, + ), ), ) @@ -73,12 +74,10 @@ class FilmController( @GetMapping("/{id}") fun getById( @PathVariable id: UUID, - ): FilmResponse = - FilmResponse.fromDomain(getFilmByIdUseCase.getById(id)) + ): FilmResponse = FilmResponse.fromDomain(getFilmByIdUseCase.getById(id)) @GetMapping - fun getAll(): List = - getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) } + fun getAll(): List = getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) } @GetMapping("/search") fun searchByTitle( 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 a4a6175..e4e000d 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -64,9 +64,10 @@ class FilmLibraryController( fun getAllFilmsInLibrary( @PathVariable userId: UUID, ): List { - val library = getFilmLibraryUseCase.getLibrary( - GetFilmLibraryQuery(userId = userId), - ) + val library = + getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId), + ) val film = getFilmByIdUseCase.getById(library.filmId) return listOf(FilmResponse.fromDomain(film)) } @@ -102,21 +103,23 @@ class FilmLibraryController( fun getAvailableFilms( @PathVariable userId: UUID, ): List { - val userLibrary = try { - getFilmLibraryUseCase.getLibrary( - GetFilmLibraryQuery(userId = userId), - ) - } catch (e: EntityNotFoundException) { - null - } + val userLibrary = + try { + getFilmLibraryUseCase.getLibrary( + GetFilmLibraryQuery(userId = userId), + ) + } catch (e: EntityNotFoundException) { + null + } val allFilms = getAllFilmsUseCase.getAll() - val availableFilms = if (userLibrary != null) { - allFilms.filter { it.id != userLibrary.filmId } - } else { - allFilms - } + 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/services/FilmService.kt b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt index 555655a..f775de3 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -28,7 +28,6 @@ class FilmService( GetFilmByIdUseCase, GetAllFilmsUseCase, SearchFilmByTitleUseCase { - override fun create(command: CreateFilmCommand): Film { if (filmConfig.isBlocked(command.title)) { throw BlockedValueException(target = "Film", field = "title") @@ -37,15 +36,19 @@ 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") } @@ -63,9 +66,8 @@ class FilmService( filmRepository.deleteById(id) } - override fun getById(id: UUID): Film { - return filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) - } + override fun getById(id: UUID): Film = + filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) override fun getAll(): 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 index 0784280..18a952a 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmControllerTest.kt @@ -9,13 +9,16 @@ 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.get +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 @SpringBootTest @AutoConfigureMockMvc class FilmControllerTest { - @Autowired private lateinit var mockMvc: MockMvc @@ -24,17 +27,18 @@ class FilmControllerTest { @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", - ) + 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) + 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()) @@ -42,86 +46,95 @@ class FilmControllerTest { @Test fun `edit film should return updated film`() { - val createRequest = CreateFilmRequest( - title = "Old Title", - description = "Old Description" - ) + 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 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", - ) + 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) + 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" - ) + val request = + CreateFilmRequest( + title = "Inception", + description = "Dream within a dream", + ) mockMvc.perform( post("/api/films") .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(request)) + .content(objectMapper.writeValueAsString(request)), ) - mockMvc.perform( - get("/api/films/search") - .param("title", "Inception") - ) - .andExpect(status().isOk) + 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("")) + fun `search film by non-existent title should return 404`() { + mockMvc + .perform( + get("/api/films/search") + .param("title", "NonExistentFilm12345"), + ).andExpect(status().isNotFound) } @Test fun `delete film should return 204 NO CONTENT`() { - val request = CreateFilmRequest( - title = "Film To Delete", - description = "This film will be deleted" - ) + 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 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")) + 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("")) + mockMvc + .perform( + get("/api/films/search").param("title", "Film To Delete"), + ).andExpect(status().isNotFound) } } diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt index d24e60e..3bbc177 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt @@ -10,7 +10,9 @@ 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.delete +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get 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 @@ -18,7 +20,6 @@ import org.springframework.transaction.annotation.Transactional @AutoConfigureMockMvc @Transactional class FilmLibraryControllerTest { - @Autowired private lateinit var mockMvc: MockMvc @@ -27,60 +28,177 @@ class FilmLibraryControllerTest { @Test fun `add film to library should work`() { - 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)), - ).andReturn() + 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)), + ).andReturn() val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() - val filmRequest = CreateFilmRequest( - title = "Library Film", - description = "Film description", - ) - val filmResponse = mockMvc.perform( - post("/api/films") - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(filmRequest)), - ).andReturn() + val filmRequest = + CreateFilmRequest( + title = "Library Film", + description = "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()) + mockMvc + .perform( + post("/api/users/$userId/library/films/$filmId"), + ).andExpect(status().isCreated()) } @Test fun `remove film from library should return 204`() { - 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)), - ).andReturn() + 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)), + ).andReturn() val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() - 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)), - ).andReturn() + 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)), + ).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")) + mockMvc + .perform(delete("/api/users/$userId/library/films/$filmId")) .andExpect(status().isNoContent()) } + + @Test + fun `get available films should exclude film in user's library`() { + val userRequest = + CreateUserRequest( + name = "Available Films User", + email = "availablefilms@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 film1Request = + CreateFilmRequest( + title = "Film In Library", + description = "This will be in the library", + ) + val film1Response = + mockMvc + .perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(film1Request)), + ).andReturn() + val film1Id = objectMapper.readTree(film1Response.response.contentAsString).get("id").asText() + + val film2Request = + CreateFilmRequest( + title = "Film Not In Library", + description = "This will not be in the library", + ) + val film2Response = + mockMvc + .perform( + post("/api/films") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(film2Request)), + ).andReturn() + val film2Id = objectMapper.readTree(film2Response.response.contentAsString).get("id").asText() + + mockMvc.perform(post("/api/users/$userId/library/films/$film1Id")) + + val result = + mockMvc + .perform( + get("/api/users/$userId/library/available-films"), + ).andExpect(status().isOk) + .andReturn() + + val responseBody = result.response.contentAsString + val films = objectMapper.readTree(responseBody) + val returnedIds = (0 until films.size()).map { films[it].get("id").asText() } + assert(!returnedIds.contains(film1Id)) { "Film in library should not appear in available films" } + assert(returnedIds.contains(film2Id)) { "Film not in library should appear in available films" } + } + + @Test + fun `get available films for user without library returns all films`() { + val userRequest = + CreateUserRequest( + name = "No Library User", + email = "nolibrary@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( + title = "Available Film", + description = "Should appear in available films", + ) + 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() + + val result = + mockMvc + .perform( + get("/api/users/$userId/library/available-films"), + ).andExpect(status().isOk) + .andExpect(jsonPath("$[*].id").isArray) + .andReturn() + + val responseBody = result.response.contentAsString + val films = objectMapper.readTree(responseBody) + val returnedIds = (0 until films.size()).map { films[it].get("id").asText() } + assert(returnedIds.contains(filmId)) { "Film should appear in available films when user has no library" } + } } diff --git a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt index db2eecc..7a2dca3 100644 --- a/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/UserControllerTest.kt @@ -20,7 +20,6 @@ import org.springframework.transaction.annotation.Transactional @AutoConfigureMockMvc @Transactional class UserControllerTest { - @Autowired private lateinit var mockMvc: MockMvc @@ -29,17 +28,18 @@ class UserControllerTest { @Test fun `create user should return 201 CREATED`() { - val request = CreateUserRequest( - name = "John Doe", - email = "john@example.com", - ) + 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) + 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()) @@ -47,54 +47,62 @@ class UserControllerTest { @Test fun `edit user should return updated user`() { - val createRequest = CreateUserRequest( - name = "Old Name", - email = "edit@example.com", - ) + 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 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) + 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 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 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")) + 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")) + mockMvc + .perform(delete("/api/users/$nonExistentId")) .andExpect(status().isNotFound()) } } -- 2.54.0 From 44483d748fdbf1f08bbd687866cd10e248f6ea66 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 3 May 2026 12:03:23 +0000 Subject: [PATCH 5/9] refactor: use idiomatic Kotlin iteration for JsonNode in tests Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/60cd9b1b-e3e1-46a1-bfd9-04a75bd0d569 Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../movienight/controllers/FilmLibraryControllerTest.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt index 3bbc177..560cf38 100644 --- a/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt +++ b/src/test/kotlin/com/project/movienight/controllers/FilmLibraryControllerTest.kt @@ -153,7 +153,7 @@ class FilmLibraryControllerTest { val responseBody = result.response.contentAsString val films = objectMapper.readTree(responseBody) - val returnedIds = (0 until films.size()).map { films[it].get("id").asText() } + val returnedIds = films.toList().map { it.get("id").asText() } assert(!returnedIds.contains(film1Id)) { "Film in library should not appear in available films" } assert(returnedIds.contains(film2Id)) { "Film not in library should appear in available films" } } @@ -198,7 +198,7 @@ class FilmLibraryControllerTest { val responseBody = result.response.contentAsString val films = objectMapper.readTree(responseBody) - val returnedIds = (0 until films.size()).map { films[it].get("id").asText() } + val returnedIds = films.toList().map { it.get("id").asText() } assert(returnedIds.contains(filmId)) { "Film should appear in available films when user has no library" } } } -- 2.54.0 From e91ebb7bd07fc71f892e73453824b5c3237460a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:48:57 +0000 Subject: [PATCH 6/9] fix: resolve detekt/ktlint issues blocking CI Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/42b7a686-07a7-41f5-baf7-737b968e12cf Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../persistence/jdbc/UserRepository.kt | 15 ++++++++++----- .../movienight/adapters/web/FilmController.kt | 9 +++++---- .../adapters/web/FilmLibraryController.kt | 10 +++++++--- .../movienight/adapters/web/UserController.kt | 13 ++++++------- .../application/services/FilmService.kt | 16 ++++++++++------ .../application/services/UserService.kt | 19 +++++++++---------- 6 files changed, 47 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt index 09d8f9d..686899f 100644 --- a/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt +++ b/src/main/kotlin/com/project/movienight/adapters/persistence/jdbc/UserRepository.kt @@ -69,10 +69,11 @@ class UserRepository( } override fun findAll(): List = - jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users", - userEntityRowMapper, - ).map { it.toDomain() } + jdbc + .query( + "SELECT id, name, email, provider, provider_id, created_at FROM users", + userEntityRowMapper, + ).map { it.toDomain() } override fun deleteById(id: UUID) { jdbc.update("DELETE FROM users WHERE id = ?", id) @@ -84,7 +85,11 @@ class UserRepository( ): User? { val entities = jdbc.query( - "SELECT id, name, email, provider, provider_id, created_at FROM users WHERE provider = ? AND provider_id = ?", + """ + SELECT id, name, email, provider, provider_id, created_at + FROM users + WHERE provider = ? AND provider_id = ? + """.trimIndent(), userEntityRowMapper, provider.name, providerId, 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 9195987..3699e33 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmController.kt @@ -57,10 +57,11 @@ class FilmController( FilmResponse.fromDomain( editFilmUseCase.edit( id = id, - command = EditFilmCommand( - title = request.title, - description = request.description, - ), + command = + EditFilmCommand( + title = request.title, + description = request.description, + ), ), ) 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 e4e000d..fa95bad 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -104,12 +104,16 @@ class FilmLibraryController( @PathVariable userId: UUID, ): List { val userLibrary = - try { + runCatching { getFilmLibraryUseCase.getLibrary( GetFilmLibraryQuery(userId = userId), ) - } catch (e: EntityNotFoundException) { - null + }.getOrElse { exception -> + if (exception is EntityNotFoundException) { + null + } else { + throw exception + } } val allFilms = getAllFilmsUseCase.getAll() diff --git a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt index 6c1a996..bccf5bc 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/UserController.kt @@ -46,14 +46,12 @@ class UserController( ) @GetMapping - fun getAll(): List = - getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) } + fun getAll(): List = getAllUsersUseCase.getAll().map { UserResponse.fromDomain(it) } @GetMapping("/{id}") fun getById( @PathVariable id: UUID, - ): UserResponse = - UserResponse.fromDomain(getUserByIdUseCase.getById(id)) + ): UserResponse = UserResponse.fromDomain(getUserByIdUseCase.getById(id)) @PatchMapping("/{id}") fun edit( @@ -63,9 +61,10 @@ class UserController( UserResponse.fromDomain( editUserUseCase.edit( id = id, - command = EditUserCommand( - name = request.name, - ), + command = + EditUserCommand( + name = request.name, + ), ), ) 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 5f77455..f775de3 100644 --- a/src/main/kotlin/com/project/movienight/application/services/FilmService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/FilmService.kt @@ -36,15 +36,19 @@ 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") } diff --git a/src/main/kotlin/com/project/movienight/application/services/UserService.kt b/src/main/kotlin/com/project/movienight/application/services/UserService.kt index 4d16ea1..da2a84b 100644 --- a/src/main/kotlin/com/project/movienight/application/services/UserService.kt +++ b/src/main/kotlin/com/project/movienight/application/services/UserService.kt @@ -26,18 +26,18 @@ class UserService( DeleteUserUseCase, GetUserByIdUseCase, GetAllUsersUseCase { - override fun create(command: CreateUserCommand): User { if (userConfig.isBlocked(command.name)) { throw BlockedValueException(target = "User", field = "name") } - val user = User( - id = idGenerator.generateId(), - name = command.name, - email = command.email, - library = null, - ) + val user = + User( + id = idGenerator.generateId(), + name = command.name, + email = command.email, + library = null, + ) return userRepository.save(user) } @@ -59,9 +59,8 @@ class UserService( userRepository.deleteById(id) } - override fun getById(id: UUID): User { - return userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString()) - } + override fun getById(id: UUID): User = + userRepository.findById(id) ?: throw EntityNotFoundException(entity = "User", id = id.toString()) override fun getAll(): List = userRepository.findAll() } -- 2.54.0 From 47ff6d80bfe3bfc06cdd05d83c17412d3bdfa11c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:50:42 +0000 Subject: [PATCH 7/9] merge: sync develop into feat/spring-tests Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/42b7a686-07a7-41f5-baf7-737b968e12cf Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../adapters/web/FilmLibraryController.kt | 14 ++-- src/main/resources/db/migration/V1__init.sql | 5 +- .../entity/UserEntityMappingTest.kt | 59 +++++++------- .../adapters/web/FilmControllerSearchTest.kt | 77 +++++++++++++++++++ 4 files changed, 119 insertions(+), 36 deletions(-) create mode 100644 src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt 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 fa95bad..5a9f002 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -92,12 +92,14 @@ class FilmLibraryController( fun removeFilm( @PathVariable userId: UUID, @PathVariable filmId: UUID, - ) = removeFilmFromLibraryUseCase.removeFilm( - RemoveFilmFromLibraryCommand( - userId = userId, - filmId = filmId, - ), - ) + ) { + removeFilmFromLibraryUseCase.removeFilm( + RemoveFilmFromLibraryCommand( + userId = userId, + filmId = filmId, + ), + ) + } @GetMapping("/available-films") fun getAvailableFilms( diff --git a/src/main/resources/db/migration/V1__init.sql b/src/main/resources/db/migration/V1__init.sql index 11017d1..900f6b5 100644 --- a/src/main/resources/db/migration/V1__init.sql +++ b/src/main/resources/db/migration/V1__init.sql @@ -1,7 +1,10 @@ CREATE TABLE IF NOT EXISTS public.users ( id UUID PRIMARY KEY, name VARCHAR(255) NOT NULL, - email VARCHAR(320) NOT NULL UNIQUE + email VARCHAR(320) NOT NULL UNIQUE, + provider VARCHAR(64), + provider_id VARCHAR(255), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS public.films ( diff --git a/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt b/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt index bec7f20..cdf0c42 100644 --- a/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt +++ b/src/test/kotlin/com/project/movienight/adapters/persistence/entity/UserEntityMappingTest.kt @@ -9,17 +9,17 @@ import kotlin.test.assertEquals import kotlin.test.assertNull class UserEntityMappingTest { - @Test fun `toDomain maps UserEntity correctly`() { - val entity = UserEntity( - id = UUID.randomUUID(), - name = "John Pork", - email = "john@email.com", - provider = "GOOGLE", - providerId = "google1234", - createdAt = LocalDateTime.now(), - ) + val entity = + UserEntity( + id = UUID.randomUUID(), + name = "John Pork", + email = "john@email.com", + provider = "GOOGLE", + providerId = "google1234", + createdAt = LocalDateTime.now(), + ) val user = entity.toDomain() assertEquals(entity.id, user.id) @@ -30,12 +30,13 @@ class UserEntityMappingTest { @Test fun `toEntity maps User with OAuth provider`() { - val user = User( - id = UUID.randomUUID(), - name = "Jane", - email = "jane@mail.com", - library = null - ) + val user = + User( + id = UUID.randomUUID(), + name = "Jane", + email = "jane@mail.com", + library = null, + ) val entity = user.toEntity(AuthProvider.YANDEX, "yandex456") @@ -48,12 +49,13 @@ class UserEntityMappingTest { @Test fun `toEntity maps User without OAuth provider`() { - val user = User( - id = UUID.randomUUID(), - name = "Bob", - email = "bob@mail.com", - library = null - ) + val user = + User( + id = UUID.randomUUID(), + name = "Bob", + email = "bob@mail.com", + library = null, + ) val entity = user.toEntity() @@ -63,12 +65,13 @@ class UserEntityMappingTest { @Test fun `mapping is reversible for basic fields`() { - val original = User( - id = UUID.randomUUID(), - name = "Alice", - email = "alice@email.com", - library = null - ) + val original = + User( + id = UUID.randomUUID(), + name = "Alice", + email = "alice@email.com", + library = null, + ) val mapped = original.toEntity().toDomain() @@ -76,6 +79,4 @@ class UserEntityMappingTest { assertEquals(original.name, mapped.name) assertEquals(original.email, mapped.email) } - - } diff --git a/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt b/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt new file mode 100644 index 0000000..dd6bd33 --- /dev/null +++ b/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt @@ -0,0 +1,77 @@ +package com.project.movienight.adapters.web + +import com.project.movienight.application.ports.input.CreateFilmUseCase +import com.project.movienight.application.ports.input.DeleteFilmUseCase +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.domain.model.Film +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.get +import org.springframework.test.web.servlet.setup.MockMvcBuilders +import java.util.UUID + +class FilmControllerSearchTest { + private lateinit var mockMvc: MockMvc + private lateinit var searchFilmByTitleUseCase: SearchFilmByTitleUseCase + + @BeforeEach + fun setup() { + searchFilmByTitleUseCase = mockk() + + val controller = + FilmController( + createFilmUseCase = mockk(), + editFilmUseCase = mockk(), + deleteFilmUseCase = mockk(), + getFilmByIdUseCase = mockk(), + getAllFilmsUseCase = mockk(), + searchFilmByTitleUseCase = searchFilmByTitleUseCase, + ) + + mockMvc = MockMvcBuilders.standaloneSetup(controller).build() + } + + @Test + fun `search returns film when title exists`() { + val title = "Inception" + val film = Film(id = UUID.randomUUID(), title = title, description = "A dream heist") + + every { searchFilmByTitleUseCase.searchByTitle(title) } returns film + + mockMvc + .get("/api/films/search") { + param("title", title) + }.andExpect { + status { isOk() } + jsonPath("$.id") { value(film.id.toString()) } + jsonPath("$.title") { value(title) } + jsonPath("$.description") { value("A dream heist") } + } + + verify(exactly = 1) { searchFilmByTitleUseCase.searchByTitle(title) } + } + + @Test + fun `search returns empty body when title is missing`() { + val title = "Unknown Title" + + every { searchFilmByTitleUseCase.searchByTitle(title) } returns null + + mockMvc + .get("/api/films/search") { + param("title", title) + }.andExpect { + status { isOk() } + content { string("") } + } + + verify(exactly = 1) { searchFilmByTitleUseCase.searchByTitle(title) } + } +} -- 2.54.0 From 22025326e77ff1fc82a560b5ddaa60c1a4618c6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:52:01 +0000 Subject: [PATCH 8/9] fix: align search test and available-films error handling Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/42b7a686-07a7-41f5-baf7-737b968e12cf Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../movienight/adapters/web/FilmLibraryController.kt | 8 +++----- .../movienight/adapters/web/FilmControllerSearchTest.kt | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) 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 5a9f002..998a330 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -110,13 +110,11 @@ class FilmLibraryController( getFilmLibraryUseCase.getLibrary( GetFilmLibraryQuery(userId = userId), ) - }.getOrElse { exception -> - if (exception is EntityNotFoundException) { - null - } else { + }.onFailure { exception -> + if (exception !is EntityNotFoundException) { throw exception } - } + }.getOrNull() val allFilms = getAllFilmsUseCase.getAll() diff --git a/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt b/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt index dd6bd33..033cccd 100644 --- a/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt +++ b/src/test/kotlin/com/project/movienight/adapters/web/FilmControllerSearchTest.kt @@ -59,7 +59,7 @@ class FilmControllerSearchTest { } @Test - fun `search returns empty body when title is missing`() { + fun `search returns 404 when title is not found`() { val title = "Unknown Title" every { searchFilmByTitleUseCase.searchByTitle(title) } returns null @@ -68,7 +68,7 @@ class FilmControllerSearchTest { .get("/api/films/search") { param("title", title) }.andExpect { - status { isOk() } + status { isNotFound() } content { string("") } } -- 2.54.0 From 267ecfef626fe4d1a1ebfd957c6952df407e1db2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 8 May 2026 17:52:48 +0000 Subject: [PATCH 9/9] fix: keep removeFilm expression body with 204 response Agent-Logs-Url: https://github.com/devitq/movienight-backend/sessions/42b7a686-07a7-41f5-baf7-737b968e12cf Co-authored-by: devitq <118541411+devitq@users.noreply.github.com> --- .../adapters/web/FilmLibraryController.kt | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 998a330..5d81414 100644 --- a/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt +++ b/src/main/kotlin/com/project/movienight/adapters/web/FilmLibraryController.kt @@ -92,14 +92,12 @@ class FilmLibraryController( fun removeFilm( @PathVariable userId: UUID, @PathVariable filmId: UUID, - ) { - removeFilmFromLibraryUseCase.removeFilm( - RemoveFilmFromLibraryCommand( - userId = userId, - filmId = filmId, - ), - ) - } + ) = removeFilmFromLibraryUseCase.removeFilm( + RemoveFilmFromLibraryCommand( + userId = userId, + filmId = filmId, + ), + ) @GetMapping("/available-films") fun getAvailableFilms( -- 2.54.0