From af901770dd7cdfbc038d3ccc673b709338db1b93 Mon Sep 17 00:00:00 2001 From: pricelees Date: Fri, 12 Sep 2025 20:57:45 +0900 Subject: [PATCH] =?UTF-8?q?test:=20=EC=9D=B8=EC=A6=9D=20API=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=B6=94=EA=B0=80(=EC=A0=84=EC=B2=B4=20?= =?UTF-8?q?=EA=B2=BD=EC=9A=B0=20/=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=B4=EB=A0=A5=20=EC=8B=A4=ED=8C=A8=20=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../kotlin/roomescape/auth/AuthApiTest.kt | 229 ++++++++++++++++++ .../auth/FailOnSaveLoginHistoryTest.kt | 64 +++++ 2 files changed, 293 insertions(+) create mode 100644 src/test/kotlin/roomescape/auth/AuthApiTest.kt create mode 100644 src/test/kotlin/roomescape/auth/FailOnSaveLoginHistoryTest.kt diff --git a/src/test/kotlin/roomescape/auth/AuthApiTest.kt b/src/test/kotlin/roomescape/auth/AuthApiTest.kt new file mode 100644 index 00000000..49142f29 --- /dev/null +++ b/src/test/kotlin/roomescape/auth/AuthApiTest.kt @@ -0,0 +1,229 @@ +package roomescape.auth + +import com.ninjasquad.springmockk.SpykBean +import io.kotest.assertions.assertSoftly +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.mockk.every +import io.restassured.response.ValidatableResponse +import org.hamcrest.CoreMatchers.equalTo +import org.springframework.http.HttpStatus +import roomescape.admin.exception.AdminErrorCode +import roomescape.auth.business.CLAIM_PERMISSION_KEY +import roomescape.auth.exception.AuthErrorCode +import roomescape.auth.infrastructure.jwt.JwtUtils +import roomescape.auth.infrastructure.persistence.LoginHistoryRepository +import roomescape.auth.web.LoginRequestV2 +import roomescape.common.dto.PrincipalType +import roomescape.member.exception.UserErrorCode +import roomescape.member.infrastructure.persistence.UserEntity +import roomescape.util.AdminFixture +import roomescape.util.FunSpecSpringbootTest +import roomescape.util.UserFixture +import roomescape.util.runTest + +class AuthApiTest( + @SpykBean private val jwtUtils: JwtUtils, + private val loginHistoryRepository: LoginHistoryRepository +) : FunSpecSpringbootTest() { + + init { + context("로그인을 시도한다.") { + context("성공 응답") { + test("관리자") { + val admin = authUtil.createAdmin(AdminFixture.default) + runLoginSuccessTest( + id = admin.id, + account = admin.account, + password = admin.password, + type = PrincipalType.ADMIN, + ) { + val token: String = it.extract().path("data.accessToken") + jwtUtils.extractSubject(token) shouldBe admin.id.toString() + jwtUtils.extractClaim(token, CLAIM_PERMISSION_KEY) shouldBe admin.permissionLevel.name + } + } + + test("회원") { + val user: UserEntity = authUtil.signup(UserFixture.createRequest) + + runLoginSuccessTest( + id = user.id, + account = user.email, + password = user.password, + type = PrincipalType.USER, + ) { + val token: String = it.extract().path("data.accessToken") + jwtUtils.extractSubject(token) shouldBe user.id.toString() + } + } + } + + context("실패 응답") { + test("비밀번호가 틀린 경우") { + val admin = authUtil.createAdmin(AdminFixture.default) + val request = LoginRequestV2(admin.account, "wrong_password", PrincipalType.ADMIN) + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.UNAUTHORIZED.value()) + body("code", equalTo(AuthErrorCode.LOGIN_FAILED.errorCode)) + } + ).also { + assertSoftly(loginHistoryRepository.findByPrincipalId(admin.id)[0]) { + it.success shouldBe false + it.principalType shouldBe PrincipalType.ADMIN + } + } + } + + test("토큰 생성 과정에서 오류가 발생하는 경우") { + val admin = authUtil.createAdmin(AdminFixture.default) + val request = LoginRequestV2(admin.account, admin.password, PrincipalType.ADMIN) + + every { + jwtUtils.createToken(any(), any()) + } throws RuntimeException("토큰 생성 실패") + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.INTERNAL_SERVER_ERROR.value()) + body("code", equalTo(AuthErrorCode.TEMPORARY_AUTH_ERROR.errorCode)) + } + ).also { + assertSoftly(loginHistoryRepository.findByPrincipalId(admin.id)[0]) { + it.success shouldBe false + it.principalType shouldBe PrincipalType.ADMIN + } + } + } + + context("계정이 일치하지 않으면 로그인 실패 이력을 남기지 않는다.") { + test("회원") { + val user = authUtil.signup(UserFixture.createRequest) + val invalidEmail = "test@email.com".also { + it shouldNotBe user.email + } + + val request = LoginRequestV2(invalidEmail, user.password, PrincipalType.USER) + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.NOT_FOUND.value()) + body("code", equalTo(UserErrorCode.USER_NOT_FOUND.errorCode)) + } + ).also { + loginHistoryRepository.findAll() shouldHaveSize 0 + } + } + + test("관리자") { + val admin = authUtil.createAdmin(AdminFixture.default) + val invalidAccount = "invalid".also { + it shouldNotBe admin.account + } + + val request = LoginRequestV2(invalidAccount, admin.password, PrincipalType.ADMIN) + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.NOT_FOUND.value()) + body("code", equalTo(AdminErrorCode.ADMIN_NOT_FOUND.errorCode)) + } + ).also { + loginHistoryRepository.findAll() shouldHaveSize 0 + } + } + } + } + } + + context("로그인 상태를 확인한다.") { + test("성공 응답") { + val token = authUtil.defaultUserLogin() + + runTest( + token = token, + on = { + get("/auth/login/check") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + val name: String = it.extract().path("data.name") + val type: String = it.extract().path("data.type") + + name.isBlank() shouldBe false + type shouldBe PrincipalType.USER.name + } + } + + test("로그인 상태가 아니면 실패한다.") { + runTest( + on = { + get("/auth/login/check") + }, + expect = { + statusCode(HttpStatus.UNAUTHORIZED.value()) + } + ) + } + } + } + + private fun runLoginSuccessTest( + id: Long, + account: String, + password: String, + type: PrincipalType, + extraAssertions: ((ValidatableResponse) -> Unit)? = null + ) { + val request = LoginRequestV2(account, password, type) + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ).also { + extraAssertions?.invoke(it) + + assertSoftly(loginHistoryRepository.findByPrincipalId(id)) { history -> + history shouldHaveSize (1) + history[0].success shouldBe true + history[0].principalType shouldBe type + } + } + } +} diff --git a/src/test/kotlin/roomescape/auth/FailOnSaveLoginHistoryTest.kt b/src/test/kotlin/roomescape/auth/FailOnSaveLoginHistoryTest.kt new file mode 100644 index 00000000..750a3ee4 --- /dev/null +++ b/src/test/kotlin/roomescape/auth/FailOnSaveLoginHistoryTest.kt @@ -0,0 +1,64 @@ +package roomescape.auth + +import com.ninjasquad.springmockk.MockkBean +import io.mockk.clearMocks +import io.mockk.every +import org.springframework.http.HttpStatus +import roomescape.auth.infrastructure.persistence.LoginHistoryRepository +import roomescape.auth.web.LoginRequestV2 +import roomescape.common.dto.PrincipalType +import roomescape.util.AdminFixture +import roomescape.util.FunSpecSpringbootTest +import roomescape.util.UserFixture +import roomescape.util.runTest + +class FailOnSaveLoginHistoryTest( + @MockkBean private val loginHistoryRepository: LoginHistoryRepository +) : FunSpecSpringbootTest() { + + init { + context("로그인 이력 저장 과정에서 예외가 발생해도 로그인 작업 자체는 정상 처리된다.") { + beforeTest { + clearMocks(loginHistoryRepository) + + every { + loginHistoryRepository.save(any()) + } throws RuntimeException("intended exception") + } + + test("회원") { + val user = authUtil.signup(UserFixture.createRequest) + val request = LoginRequestV2(user.email, user.password, PrincipalType.USER) + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ) + } + + test("관리자") { + val admin = authUtil.createAdmin(AdminFixture.default) + val request = LoginRequestV2(admin.account, admin.password, PrincipalType.ADMIN) + + runTest( + using = { + body(request) + }, + on = { + post("/auth/login") + }, + expect = { + statusCode(HttpStatus.OK.value()) + } + ) + } + } + } +}