feat(member): 가입과 로그인 응답에 신규 회원 여부를 추가한다
This commit is contained in:
@@ -77,7 +77,7 @@ class MemberController(
|
||||
)
|
||||
|
||||
val message = messageSource.getMessage("member.signup.success", langContext.lang)
|
||||
return ApiResponse.ok(message = message, data = response.loginResponse)
|
||||
return ApiResponse.ok(message = message, data = response.loginResponse.copy(isNew = true))
|
||||
}
|
||||
|
||||
@PostMapping("/signup")
|
||||
@@ -96,7 +96,7 @@ class MemberController(
|
||||
}
|
||||
|
||||
val message = messageSource.getMessage("member.signup.success", langContext.lang)
|
||||
return ApiResponse.ok(message = message, data = response.loginResponse)
|
||||
return ApiResponse.ok(message = message, data = response.loginResponse.copy(isNew = true))
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
@@ -440,7 +440,7 @@ class MemberController(
|
||||
}
|
||||
|
||||
val message = messageSource.getMessage("member.signup.success", langContext.lang)
|
||||
return ApiResponse.ok(message = message, data = response.loginResponse)
|
||||
return ApiResponse.ok(message = message, data = response.loginResponse.copy(isNew = response.isNew))
|
||||
}
|
||||
|
||||
private fun extractBearerToken(authHeader: String, provider: MemberProvider): String {
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
package kr.co.vividnext.sodalive.member.login
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class LoginResponse(
|
||||
val userId: Long,
|
||||
val token: String,
|
||||
val nickname: String,
|
||||
val email: String,
|
||||
val profileImage: String
|
||||
val profileImage: String,
|
||||
@get:JsonProperty("isNew")
|
||||
@param:JsonProperty("isNew")
|
||||
val isNew: Boolean = false
|
||||
)
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
package kr.co.vividnext.sodalive.member
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.common.ApiResponse
|
||||
import kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberService
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingHistoryType
|
||||
import kr.co.vividnext.sodalive.marketing.AdTrackingService
|
||||
import kr.co.vividnext.sodalive.member.login.LoginRequest
|
||||
import kr.co.vividnext.sodalive.member.login.LoginResponse
|
||||
import kr.co.vividnext.sodalive.member.signUp.SignUpRequestV2
|
||||
import kr.co.vividnext.sodalive.member.signUp.SignUpResponse
|
||||
import kr.co.vividnext.sodalive.member.social.SocialAuthService
|
||||
import kr.co.vividnext.sodalive.member.social.SocialAuthServiceResolver
|
||||
import kr.co.vividnext.sodalive.member.social.SocialLoginResponse
|
||||
import kr.co.vividnext.sodalive.support.EmbeddedRedisInitializer
|
||||
import kr.co.vividnext.sodalive.useraction.ActionType
|
||||
import kr.co.vividnext.sodalive.useraction.UserActionService
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.DisplayName
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.params.ParameterizedTest
|
||||
import org.junit.jupiter.params.provider.CsvSource
|
||||
import org.junit.jupiter.params.provider.ValueSource
|
||||
import org.mockito.Mockito
|
||||
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.boot.test.mock.mockito.MockBean
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.mock.web.MockMultipartFile
|
||||
import org.springframework.test.context.ContextConfiguration
|
||||
import org.springframework.test.web.servlet.MockMvc
|
||||
import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.multipart
|
||||
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
|
||||
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ContextConfiguration(initializers = [EmbeddedRedisInitializer::class])
|
||||
class MemberLoginResponseContractTest @Autowired constructor(
|
||||
private val mockMvc: MockMvc,
|
||||
private val objectMapper: ObjectMapper
|
||||
) {
|
||||
@MockBean
|
||||
private lateinit var memberService: MemberService
|
||||
|
||||
@MockBean
|
||||
private lateinit var creatorAdminMemberService: CreatorAdminMemberService
|
||||
|
||||
@MockBean
|
||||
private lateinit var socialAuthServiceResolver: SocialAuthServiceResolver
|
||||
|
||||
@MockBean
|
||||
private lateinit var trackingService: AdTrackingService
|
||||
|
||||
@MockBean
|
||||
private lateinit var userActionService: UserActionService
|
||||
|
||||
private val loginResponse = LoginResponse(42L, "issued-token", "member", "", "profile-url")
|
||||
|
||||
@ParameterizedTest(name = "{0} isNew={1}")
|
||||
@CsvSource(
|
||||
"GOOGLE, false",
|
||||
"GOOGLE, true",
|
||||
"KAKAO, false",
|
||||
"KAKAO, true",
|
||||
"APPLE, false",
|
||||
"APPLE, true",
|
||||
"LINE, false",
|
||||
"LINE, true"
|
||||
)
|
||||
@DisplayName("모든 소셜 로그인은 내부 가입 여부와 기존 JSON 계약 및 부수 동작을 유지한다")
|
||||
fun shouldExposeSocialIsNewAsBooleanForEveryProvider(provider: MemberProvider, isNew: Boolean) {
|
||||
val authService = Mockito.mock(SocialAuthService::class.java)
|
||||
val nonce = if (provider == MemberProvider.APPLE || provider == MemberProvider.LINE) "login-nonce" else null
|
||||
Mockito.`when`(socialAuthServiceResolver.resolve(provider)).thenReturn(authService)
|
||||
Mockito.`when`(authService.authenticate("provider-token", "web", "campaign-pid", "push-token", nonce))
|
||||
.thenReturn(SocialLoginResponse(42L, "campaign-pid", loginResponse, isNew))
|
||||
|
||||
val request = post("/member/login/${provider.name.lowercase()}")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content(
|
||||
"""{
|
||||
"container":"web", "marketingPid":"campaign-pid", "pushToken":"push-token",
|
||||
"identityToken":"provider-token", "nonce":"login-nonce"
|
||||
}"""
|
||||
)
|
||||
if (nonce == null) request.header("Authorization", "Bearer provider-token")
|
||||
|
||||
val response = perform(request)
|
||||
|
||||
Mockito.verify(authService).authenticate("provider-token", "web", "campaign-pid", "push-token", nonce)
|
||||
Mockito.verify(trackingService).saveTrackingHistory("campaign-pid", AdTrackingHistoryType.SIGNUP, 42L)
|
||||
Mockito.verify(userActionService, Mockito.times(if (isNew) 1 else 0))
|
||||
.recordAction(42L, false, ActionType.SIGN_UP)
|
||||
Mockito.verifyNoMoreInteractions(trackingService, userActionService)
|
||||
assertEnvelope(response, "Congratulations on your sign up.")
|
||||
assertLoginData(response.path("data"), isNew)
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = ["/member/signup", "/member/signup/v2"])
|
||||
@DisplayName("두 이메일 가입 응답은 서비스 DTO 기본값과 무관하게 신규 회원 여부를 true로 반환한다")
|
||||
fun shouldReturnNewForBothEmailSignupEndpoints(path: String) {
|
||||
val signupResponse = SignUpResponse(42L, "campaign-pid", loginResponse)
|
||||
val request = if (path == "/member/signup") {
|
||||
val body = """{
|
||||
"email":"member@test.com", "password":"test-password", "nickname":"member", "gender":"NONE",
|
||||
"marketingPid":"campaign-pid", "isAgreeTermsOfService":true, "isAgreePrivacyPolicy":true, "container":"web"
|
||||
}"""
|
||||
Mockito.`when`(memberService.signUp(null, body)).thenReturn(signupResponse)
|
||||
multipart(path).file(MockMultipartFile("request", "", MediaType.TEXT_PLAIN_VALUE, body.toByteArray()))
|
||||
} else {
|
||||
Mockito.`when`(
|
||||
memberService.signUpV2(
|
||||
SignUpRequestV2(
|
||||
email = "member@test.com",
|
||||
password = "test-password",
|
||||
marketingPid = "campaign-pid",
|
||||
isAgreeTermsOfService = true,
|
||||
isAgreePrivacyPolicy = true,
|
||||
container = "web"
|
||||
)
|
||||
)
|
||||
).thenReturn(signupResponse)
|
||||
post(path).contentType(MediaType.APPLICATION_JSON).content(
|
||||
"""{
|
||||
"email":"member@test.com", "password":"test-password", "marketingPid":"campaign-pid",
|
||||
"isAgreeTermsOfService":true, "isAgreePrivacyPolicy":true, "container":"web"
|
||||
}"""
|
||||
)
|
||||
}
|
||||
|
||||
val response = perform(request)
|
||||
|
||||
Mockito.verify(trackingService).saveTrackingHistory("campaign-pid", AdTrackingHistoryType.SIGNUP, 42L)
|
||||
Mockito.verify(userActionService, Mockito.times(if (path == "/member/signup/v2") 1 else 0))
|
||||
.recordAction(42L, false, ActionType.SIGN_UP)
|
||||
Mockito.verifyNoMoreInteractions(trackingService, userActionService)
|
||||
assertEnvelope(response, "Congratulations on your sign up.")
|
||||
assertLoginData(response.path("data"), true)
|
||||
}
|
||||
|
||||
@ParameterizedTest(name = "isAdmin={0}, isCreator={1}")
|
||||
@CsvSource("false, false", "true, false", "false, true")
|
||||
@DisplayName("일반 및 관리자 및 크리에이터 회원 로그인은 신규 회원 여부를 false로 반환한다")
|
||||
fun shouldReturnFalseForEveryMemberLoginBranch(isAdmin: Boolean, isCreator: Boolean) {
|
||||
val request = LoginRequest("member@test.com", "test-password", isAdmin, isCreator)
|
||||
Mockito.`when`(memberService.login(request))
|
||||
.thenReturn(ApiResponse.ok(data = loginResponse, message = "You are logged in."))
|
||||
|
||||
val response = perform(
|
||||
post("/member/login").contentType(MediaType.APPLICATION_JSON).content(
|
||||
"""{"email":"member@test.com","password":"test-password","isAdmin":$isAdmin,"isCreator":$isCreator}"""
|
||||
)
|
||||
)
|
||||
|
||||
Mockito.verify(memberService).login(request)
|
||||
assertEnvelope(response, "You are logged in.")
|
||||
assertLoginData(response.path("data"), false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("크리에이터 관리자 로그인은 공유 응답의 신규 회원 여부를 false로 반환한다")
|
||||
fun shouldReturnFalseForCreatorAdminLogin() {
|
||||
val request = LoginRequest("member@test.com", "test-password")
|
||||
Mockito.`when`(creatorAdminMemberService.login(request))
|
||||
.thenReturn(ApiResponse.ok(data = loginResponse, message = "Logged in."))
|
||||
|
||||
val response = perform(
|
||||
post("/creator-admin/member/login").contentType(MediaType.APPLICATION_JSON)
|
||||
.content("""{"email":"member@test.com","password":"test-password"}""")
|
||||
)
|
||||
|
||||
Mockito.verify(creatorAdminMemberService).login(request)
|
||||
assertEnvelope(response, "Logged in.")
|
||||
assertLoginData(response.path("data"), false)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("애플리케이션 Jackson은 기본 LoginResponse의 isNew false를 생략하지 않는다")
|
||||
fun shouldSerializeDefaultLoginResponseWithIsNewFalse() {
|
||||
assertLoginData(objectMapper.readTree(objectMapper.writeValueAsString(loginResponse)), false)
|
||||
}
|
||||
|
||||
private fun perform(request: MockHttpServletRequestBuilder): JsonNode {
|
||||
val response = mockMvc.perform(request.header("Accept-Language", "en"))
|
||||
.andExpect(status().isOk)
|
||||
.andReturn().response
|
||||
return objectMapper.readTree(response.contentAsByteArray)
|
||||
}
|
||||
|
||||
private fun assertEnvelope(response: JsonNode, message: String) {
|
||||
assertEquals(setOf("success", "message", "data", "errorProperty"), response.fieldNames().asSequence().toSet())
|
||||
assertTrue(response.path("success").isBoolean)
|
||||
assertTrue(response.path("success").booleanValue())
|
||||
assertEquals(message, response.path("message").textValue())
|
||||
assertTrue(response.path("errorProperty").isNull)
|
||||
}
|
||||
|
||||
private fun assertLoginData(data: JsonNode, isNew: Boolean) {
|
||||
assertTrue(data.path("isNew").isBoolean, "isNew must be a present JSON boolean: $data")
|
||||
assertEquals(isNew, data.path("isNew").booleanValue())
|
||||
assertFalse(data.has("new"))
|
||||
assertEquals(
|
||||
setOf("userId", "token", "nickname", "email", "profileImage", "isNew"),
|
||||
data.fieldNames().asSequence().toSet()
|
||||
)
|
||||
assertEquals(42L, data.path("userId").longValue())
|
||||
assertEquals("issued-token", data.path("token").textValue())
|
||||
assertEquals("member", data.path("nickname").textValue())
|
||||
assertEquals("", data.path("email").textValue())
|
||||
assertEquals("profile-url", data.path("profileImage").textValue())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user