исправления

This commit is contained in:
Elena Ponomareva
2026-04-24 23:47:56 +03:00
parent 8926e5b08c
commit f563c3e7cf
8 changed files with 137 additions and 69 deletions
@@ -62,11 +62,12 @@ class FilmRepository(
) )
override fun findByTitle(title: String): Film? { override fun findByTitle(title: String): Film? {
val films = jdbc.query( val films =
"SELECT id, title, description FROM films WHERE title = ?", jdbc.query(
filmRowMapper, "SELECT id, title, description FROM films WHERE title = ? ORDER BY id LIMIT 1",
title filmRowMapper,
) title,
)
return films.firstOrNull() return films.firstOrNull()
} }
@@ -1,6 +1,5 @@
package com.project.movienight.adapters.web package com.project.movienight.adapters.web
import com.project.movienight.adapters.web.dto.request.CreateFilmRequest 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.request.EditFilmRequest
import com.project.movienight.adapters.web.dto.response.FilmResponse 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.DeleteFilmUseCase
import com.project.movienight.application.ports.input.EditFilmCommand import com.project.movienight.application.ports.input.EditFilmCommand
import com.project.movienight.application.ports.input.EditFilmUseCase 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.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping 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.PatchMapping
import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping 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.ResponseStatus
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import java.util.UUID import java.util.UUID
@RestController @RestController
@@ -28,7 +31,9 @@ class FilmController(
private val createFilmUseCase: CreateFilmUseCase, private val createFilmUseCase: CreateFilmUseCase,
private val editFilmUseCase: EditFilmUseCase, private val editFilmUseCase: EditFilmUseCase,
private val deleteFilmUseCase: DeleteFilmUseCase, private val deleteFilmUseCase: DeleteFilmUseCase,
private val filmService: FilmService, private val getFilmByIdUseCase: GetFilmByIdUseCase,
private val getAllFilmsUseCase: GetAllFilmsUseCase,
private val searchFilmByTitleUseCase: SearchFilmByTitleUseCase,
) { ) {
@PostMapping @PostMapping
@ResponseStatus(HttpStatus.CREATED) @ResponseStatus(HttpStatus.CREATED)
@@ -52,11 +57,10 @@ class FilmController(
FilmResponse.fromDomain( FilmResponse.fromDomain(
editFilmUseCase.edit( editFilmUseCase.edit(
id = id, id = id,
command = command = EditFilmCommand(
EditFilmCommand( title = request.title,
title = request.title, description = request.description,
description = request.description, ),
),
), ),
) )
@@ -66,9 +70,25 @@ class FilmController(
@PathVariable id: UUID, @PathVariable id: UUID,
) = deleteFilmUseCase.delete(id) ) = deleteFilmUseCase.delete(id)
@GetMapping("/{id}")
fun getById(
@PathVariable id: UUID,
): FilmResponse =
FilmResponse.fromDomain(getFilmByIdUseCase.getById(id))
@GetMapping
fun getAll(): List<FilmResponse> =
getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) }
@GetMapping("/search") @GetMapping("/search")
fun searchByTitle( fun searchByTitle(
@RequestParam title: String, @RequestParam title: String,
): FilmResponse? = ): ResponseEntity<FilmResponse> {
filmService.findByTitle(title)?.let { FilmResponse.fromDomain(it) } val film = searchFilmByTitleUseCase.searchByTitle(title)
return if (film != null) {
ResponseEntity.ok(FilmResponse.fromDomain(film))
} else {
ResponseEntity.notFound().build()
}
}
} }
@@ -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.AddFilmToLibraryUseCase
import com.project.movienight.application.ports.input.CreateFilmLibraryCommand import com.project.movienight.application.ports.input.CreateFilmLibraryCommand
import com.project.movienight.application.ports.input.CreateFilmLibraryUseCase 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.GetFilmLibraryQuery
import com.project.movienight.application.ports.input.GetFilmLibraryUseCase import com.project.movienight.application.ports.input.GetFilmLibraryUseCase
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand import com.project.movienight.application.ports.input.RemoveFilmFromLibraryCommand
import com.project.movienight.application.ports.input.RemoveFilmFromLibraryUseCase 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.http.HttpStatus
import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping 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.RequestMapping
import org.springframework.web.bind.annotation.ResponseStatus import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.*
import java.util.UUID import java.util.UUID
@RestController @RestController
@@ -31,7 +32,8 @@ class FilmLibraryController(
private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase, private val addFilmToLibraryUseCase: AddFilmToLibraryUseCase,
private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase, private val removeFilmFromLibraryUseCase: RemoveFilmFromLibraryUseCase,
private val getFilmLibraryUseCase: GetFilmLibraryUseCase, private val getFilmLibraryUseCase: GetFilmLibraryUseCase,
private val filmService: FilmService, private val getFilmByIdUseCase: GetFilmByIdUseCase,
private val getAllFilmsUseCase: GetAllFilmsUseCase,
) { ) {
@PostMapping @PostMapping
@ResponseStatus(HttpStatus.CREATED) @ResponseStatus(HttpStatus.CREATED)
@@ -58,6 +60,17 @@ class FilmLibraryController(
), ),
) )
@GetMapping("/films")
fun getAllFilmsInLibrary(
@PathVariable userId: UUID,
): List<FilmResponse> {
val library = getFilmLibraryUseCase.getLibrary(
GetFilmLibraryQuery(userId = userId),
)
val film = getFilmByIdUseCase.getById(library.filmId)
return listOf(FilmResponse.fromDomain(film))
}
@PostMapping("/films/{filmId}") @PostMapping("/films/{filmId}")
@ResponseStatus(HttpStatus.CREATED) @ResponseStatus(HttpStatus.CREATED)
fun addFilm( fun addFilm(
@@ -89,13 +102,21 @@ class FilmLibraryController(
fun getAvailableFilms( fun getAvailableFilms(
@PathVariable userId: UUID, @PathVariable userId: UUID,
): List<FilmResponse> { ): List<FilmResponse> {
val userLibrary = getFilmLibraryUseCase.getLibrary( val userLibrary = try {
GetFilmLibraryQuery(userId = userId) 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) } return availableFilms.map { FilmResponse.fromDomain(it) }
} }
@@ -27,3 +27,15 @@ data class EditFilmCommand(
interface DeleteFilmUseCase { interface DeleteFilmUseCase {
fun delete(id: UUID) fun delete(id: UUID)
} }
interface GetFilmByIdUseCase {
fun getById(id: UUID): Film
}
interface GetAllFilmsUseCase {
fun getAll(): List<Film>
}
interface SearchFilmByTitleUseCase {
fun searchByTitle(title: String): Film?
}
@@ -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.DeleteFilmUseCase
import com.project.movienight.application.ports.input.EditFilmCommand import com.project.movienight.application.ports.input.EditFilmCommand
import com.project.movienight.application.ports.input.EditFilmUseCase 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.FilmRepositoryPort
import com.project.movienight.application.ports.output.IdGenerator import com.project.movienight.application.ports.output.IdGenerator
import com.project.movienight.config.FilmServiceProperties import com.project.movienight.config.FilmServiceProperties
@@ -21,7 +24,11 @@ class FilmService(
private val filmConfig: FilmServiceProperties, private val filmConfig: FilmServiceProperties,
) : CreateFilmUseCase, ) : CreateFilmUseCase,
EditFilmUseCase, EditFilmUseCase,
DeleteFilmUseCase { DeleteFilmUseCase,
GetFilmByIdUseCase,
GetAllFilmsUseCase,
SearchFilmByTitleUseCase {
override fun create(command: CreateFilmCommand): Film { override fun create(command: CreateFilmCommand): Film {
if (filmConfig.isBlocked(command.title)) { if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "title") throw BlockedValueException(target = "Film", field = "title")
@@ -30,19 +37,15 @@ class FilmService(
throw BlockedValueException(target = "Film", field = "description") throw BlockedValueException(target = "Film", field = "description")
} }
val film = val film = Film(
Film( id = idGenerator.generateId(),
id = idGenerator.generateId(), title = command.title,
title = command.title, description = command.description,
description = command.description, )
)
return filmRepository.save(film) return filmRepository.save(film)
} }
override fun edit( override fun edit(id: UUID, command: EditFilmCommand): Film {
id: UUID,
command: EditFilmCommand,
): Film {
if (filmConfig.isBlocked(command.title)) { if (filmConfig.isBlocked(command.title)) {
throw BlockedValueException(target = "Film", field = "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()) var film = filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString())
film = film.copy(title = command.title, description = command.description) film = film.copy(title = command.title, description = command.description)
return filmRepository.save(film) return filmRepository.save(film)
} }
override fun delete(id: UUID) { override fun delete(id: UUID) {
filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString()) filmRepository.findById(id) ?: throw EntityNotFoundException(entity = "Film", id = id.toString())
filmRepository.deleteById(id) 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<Film> = filmRepository.findAll() override fun getAll(): List<Film> = filmRepository.findAll()
override fun searchByTitle(title: String): Film? = filmRepository.findByTitle(title)
} }
@@ -26,7 +26,7 @@ class FilmControllerTest {
fun `create film should return 201 CREATED`() { fun `create film should return 201 CREATED`() {
val request = CreateFilmRequest( val request = CreateFilmRequest(
title = "The Matrix", 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( mockMvc.perform(
@@ -40,7 +40,6 @@ class FilmControllerTest {
.andExpect(jsonPath("$.id").exists()) .andExpect(jsonPath("$.id").exists())
} }
@Test @Test
fun `edit film should return updated film`() { fun `edit film should return updated film`() {
val createRequest = CreateFilmRequest( val createRequest = CreateFilmRequest(
@@ -58,7 +57,7 @@ class FilmControllerTest {
val editRequest = EditFilmRequest( val editRequest = EditFilmRequest(
title = "New Title", title = "New Title",
description = "New Description" description = "New Description",
) )
mockMvc.perform( mockMvc.perform(
@@ -71,7 +70,6 @@ class FilmControllerTest {
.andExpect(jsonPath("$.description").value("New Description")) .andExpect(jsonPath("$.description").value("New Description"))
} }
@Test @Test
fun `search film by title should return film`() { fun `search film by title should return film`() {
val request = CreateFilmRequest( val request = CreateFilmRequest(
@@ -94,7 +92,6 @@ class FilmControllerTest {
.andExpect(jsonPath("$.description").value("Dream within a dream")) .andExpect(jsonPath("$.description").value("Dream within a dream"))
} }
@Test @Test
fun `search film by non-existent title should return empty`() { fun `search film by non-existent title should return empty`() {
mockMvc.perform( mockMvc.perform(
@@ -105,7 +102,6 @@ class FilmControllerTest {
.andExpect(content().string("")) .andExpect(content().string(""))
} }
@Test @Test
fun `delete film should return 204 NO CONTENT`() { fun `delete film should return 204 NO CONTENT`() {
val request = CreateFilmRequest( val request = CreateFilmRequest(
@@ -9,8 +9,9 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import org.springframework.transaction.annotation.Transactional import org.springframework.transaction.annotation.Transactional
@SpringBootTest @SpringBootTest
@@ -24,46 +25,57 @@ class FilmLibraryControllerTest {
@Autowired @Autowired
private lateinit var objectMapper: ObjectMapper private lateinit var objectMapper: ObjectMapper
@Test @Test
fun `add film to library should work`() { 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( val userResponse = mockMvc.perform(
post("/api/users") post("/api/users")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userRequest)) .content(objectMapper.writeValueAsString(userRequest)),
).andReturn() ).andReturn()
val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() 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( val filmResponse = mockMvc.perform(
post("/api/films") post("/api/films")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(filmRequest)) .content(objectMapper.writeValueAsString(filmRequest)),
).andReturn() ).andReturn()
val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText() val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText()
mockMvc.perform( mockMvc.perform(
post("/api/users/$userId/library/films/$filmId") post("/api/users/$userId/library/films/$filmId"),
) )
.andExpect(status().isCreated) .andExpect(status().isCreated())
} }
@Test @Test
fun `remove film from library should return 204`() { 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( val userResponse = mockMvc.perform(
post("/api/users") post("/api/users")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(userRequest)) .content(objectMapper.writeValueAsString(userRequest)),
).andReturn() ).andReturn()
val userId = objectMapper.readTree(userResponse.response.contentAsString).get("id").asText() 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( val filmResponse = mockMvc.perform(
post("/api/films") post("/api/films")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(filmRequest)) .content(objectMapper.writeValueAsString(filmRequest)),
).andReturn() ).andReturn()
val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText() val filmId = objectMapper.readTree(filmResponse.response.contentAsString).get("id").asText()
@@ -9,8 +9,11 @@ import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMock
import org.springframework.boot.test.context.SpringBootTest import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType import org.springframework.http.MediaType
import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.* import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.* 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 import org.springframework.transaction.annotation.Transactional
@SpringBootTest @SpringBootTest
@@ -28,13 +31,13 @@ class UserControllerTest {
fun `create user should return 201 CREATED`() { fun `create user should return 201 CREATED`() {
val request = CreateUserRequest( val request = CreateUserRequest(
name = "John Doe", name = "John Doe",
email = "john@example.com" email = "john@example.com",
) )
mockMvc.perform( mockMvc.perform(
post("/api/users") post("/api/users")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)) .content(objectMapper.writeValueAsString(request)),
) )
.andExpect(status().isCreated) .andExpect(status().isCreated)
.andExpect(jsonPath("$.name").value("John Doe")) .andExpect(jsonPath("$.name").value("John Doe"))
@@ -42,18 +45,17 @@ class UserControllerTest {
.andExpect(jsonPath("$.id").exists()) .andExpect(jsonPath("$.id").exists())
} }
@Test @Test
fun `edit user should return updated user`() { fun `edit user should return updated user`() {
val createRequest = CreateUserRequest( val createRequest = CreateUserRequest(
name = "Old Name", name = "Old Name",
email = "edit@example.com" email = "edit@example.com",
) )
val response = mockMvc.perform( val response = mockMvc.perform(
post("/api/users") post("/api/users")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(createRequest)) .content(objectMapper.writeValueAsString(createRequest)),
).andReturn() ).andReturn()
val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText() val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText()
@@ -63,7 +65,7 @@ class UserControllerTest {
mockMvc.perform( mockMvc.perform(
patch("/api/users/$userId") patch("/api/users/$userId")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(editRequest)) .content(objectMapper.writeValueAsString(editRequest)),
) )
.andExpect(status().isOk) .andExpect(status().isOk)
.andExpect(jsonPath("$.name").value("New Name")) .andExpect(jsonPath("$.name").value("New Name"))
@@ -74,13 +76,13 @@ class UserControllerTest {
fun `delete user should return 204 NO CONTENT`() { fun `delete user should return 204 NO CONTENT`() {
val request = CreateUserRequest( val request = CreateUserRequest(
name = "User To Delete", name = "User To Delete",
email = "delete@example.com" email = "delete@example.com",
) )
val response = mockMvc.perform( val response = mockMvc.perform(
post("/api/users") post("/api/users")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)) .content(objectMapper.writeValueAsString(request)),
).andReturn() ).andReturn()
val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText() val userId = objectMapper.readTree(response.response.contentAsString).get("id").asText()