test: write tests for controllers #24
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
`org.springframework.web.bind.annotation.*` is a wildcard import and is redundant with the explicit annotation imports above; ktlint (enabled in this repo) will fail on wildcard/redundant imports. Replace the wildcard import with only the specific annotations you use (and remove any duplicates).
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
|
`searchByTitle` returns `null` when a film isn't found, which produces a 200 with an empty body. Other not-found scenarios in this API return 404 via `EntityNotFoundException`/`ApiExceptionHandler`; consider returning a 404 (or 204) explicitly (e.g., `ResponseEntity.notFound()`), to keep error semantics consistent for clients.
|
||||
|
||||
@@ -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<FilmResponse> =
|
||||
getAllFilmsUseCase.getAll().map { FilmResponse.fromDomain(it) }
|
||||
|
||||
@GetMapping("/search")
|
||||
fun searchByTitle(
|
||||
@RequestParam title: String,
|
||||
): FilmResponse? =
|
||||
filmService.findByTitle(title)?.let { FilmResponse.fromDomain(it) }
|
||||
): ResponseEntity<FilmResponse> {
|
||||
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.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<FilmResponse> {
|
||||
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<FilmResponse> {
|
||||
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) }
|
||||
}
|
||||
|
||||
@@ -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<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.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<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`() {
|
||||
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(
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user
There are multiple consecutive blank lines after the
packagedeclaration here; ktlint will flag consecutive blank lines. Collapse this to a single blank line before the imports.