package roomescape.theme.web import com.ninjasquad.springmockk.MockkBean import com.ninjasquad.springmockk.SpykBean import io.kotest.assertions.assertSoftly import io.kotest.matchers.collections.shouldContainAll import io.kotest.matchers.shouldBe import io.mockk.every import io.mockk.just import io.mockk.runs import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest import org.springframework.http.MediaType import org.springframework.test.web.servlet.MockMvc import roomescape.theme.business.ThemeService import roomescape.theme.infrastructure.persistence.ThemeRepository import roomescape.util.RoomescapeApiTest import roomescape.util.ThemeFixture @WebMvcTest(ThemeController::class) class ThemeControllerTest(mockMvc: MockMvc) : RoomescapeApiTest() { @SpykBean private lateinit var themeService: ThemeService @MockkBean private lateinit var themeRepository: ThemeRepository init { Given("모든 테마를 조회할 때") { val endpoint = "/themes" When("로그인 상태가 아니라면") { doNotLogin() Then("로그인 페이지로 이동한다.") { runGetTest( mockMvc = mockMvc, endpoint = endpoint, log = true ) { status { is3xxRedirection() } header { string("Location", "/login") } } } } When("로그인 상태라면") { loginAsUser() Then("조회에 성공한다.") { every { themeRepository.findAll() } returns listOf( ThemeFixture.create(id = 1, name = "theme1"), ThemeFixture.create(id = 2, name = "theme2"), ThemeFixture.create(id = 3, name = "theme3") ) val response: ThemesResponse = runGetTest( mockMvc = mockMvc, endpoint = endpoint, log = true ) { status { isOk() } content { contentType(MediaType.APPLICATION_JSON) } }.andReturn().readValue(ThemesResponse::class.java) assertSoftly(response.themes) { it.size shouldBe 3 it.map { m -> m.name } shouldContainAll listOf("theme1", "theme2", "theme3") } } } } Given("테마를 추가할 때") { val endpoint = "/themes" val request = ThemeRequest( name = "theme1", description = "description1", thumbnail = "http://example.com/thumbnail1.jpg" ) When("로그인 상태가 아니라면") { doNotLogin() Then("로그인 페이지로 이동한다.") { runPostTest( mockMvc = mockMvc, endpoint = endpoint, body = request, log = true ) { status { is3xxRedirection() } header { string("Location", "/login") } } } } When("관리자가 아닌 회원은") { loginAsUser() Then("로그인 페이지로 이동한다.") { runPostTest( mockMvc = mockMvc, endpoint = endpoint, body = request, log = true ) { status { is3xxRedirection() } jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } } } } When("동일한 이름의 테마가 있으면") { loginAsAdmin() Then("409 에러를 응답한다.") { every { themeRepository.existsByName(request.name) } returns true runPostTest( mockMvc = mockMvc, endpoint = endpoint, body = request, log = true ) { status { isConflict() } jsonPath("$.errorType") { value("THEME_DUPLICATED") } } } } When("값이 잘못 입력되면 400 에러를 응답한다") { beforeTest { loginAsAdmin() } val request = ThemeRequest( name = "theme1", description = "description1", thumbnail = "http://example.com/thumbnail1.jpg" ) fun runTest(request: ThemeRequest) { runPostTest( mockMvc = mockMvc, endpoint = endpoint, body = request, log = true ) { status { isBadRequest() } } } Then("이름이 공백인 경우") { val invalidRequest = request.copy(name = " ") runTest(invalidRequest) } Then("이름이 20글자를 초과하는 경우") { val invalidRequest = request.copy(name = "a".repeat(21)) runTest(invalidRequest) } Then("설명이 공백인 경우") { val invalidRequest = request.copy(description = " ") runTest(invalidRequest) } Then("설명이 100글자를 초과하는 경우") { val invalidRequest = request.copy(description = "a".repeat(101)) runTest(invalidRequest) } Then("썸네일이 공백인 경우") { val invalidRequest = request.copy(thumbnail = " ") runTest(invalidRequest) } Then("썸네일이 URL 형식이 아닌 경우") { val invalidRequest = request.copy(thumbnail = "invalid-url") runTest(invalidRequest) } } When("저장에 성공하면") { loginAsAdmin() val theme = ThemeFixture.create( id = 1, name = request.name, description = request.description, thumbnail = request.thumbnail ) every { themeService.create(request) } returns ThemeResponse( id = theme.id!!, name = theme.name, description = theme.description, thumbnail = theme.thumbnail ) Then("201 응답을 받는다.") { runPostTest( mockMvc = mockMvc, endpoint = endpoint, body = request, log = true ) { status { isCreated() } header { string("Location", "/themes/${theme.id}") } jsonPath("$.data.id") { value(theme.id) } jsonPath("$.data.name") { value(theme.name) } jsonPath("$.data.description") { value(theme.description) } jsonPath("$.data.thumbnail") { value(theme.thumbnail) } } } } } Given("테마를 제거할 때") { val themeId = 1L val endpoint = "/themes/$themeId" When("로그인 상태가 아니라면") { doNotLogin() Then("로그인 페이지로 이동한다.") { runDeleteTest( mockMvc = mockMvc, endpoint = endpoint, log = true ) { status { is3xxRedirection() } header { string("Location", "/login") } } } } When("관리자가 아닌 회원은") { loginAsUser() Then("로그인 페이지로 이동한다.") { runDeleteTest( mockMvc = mockMvc, endpoint = endpoint, log = true ) { status { is3xxRedirection() } jsonPath("$.errorType") { value("PERMISSION_DOES_NOT_EXIST") } } } } When("입력된 ID에 해당하는 테마가 없으면") { loginAsAdmin() Then("409 에러를 응답한다.") { every { themeRepository.isReservedTheme(themeId) } returns true runDeleteTest( mockMvc = mockMvc, endpoint = endpoint, log = true ) { status { isConflict() } jsonPath("$.errorType") { value("THEME_IS_USED_CONFLICT") } } } } When("정상적으로 제거되면") { loginAsAdmin() every { themeRepository.isReservedTheme(themeId) } returns false every { themeRepository.deleteById(themeId) } just runs Then("204 응답을 받는다.") { runDeleteTest( mockMvc = mockMvc, endpoint = endpoint, log = true ) { status { isNoContent() } } } } } } }