Skip to content

Commit

Permalink
뉴스 레터 구독 API 연동 (#333)
Browse files Browse the repository at this point in the history
* feat: impl stibee subscription api

* feat: add test case

* feat: add properties

* fix: add mock for spring web test

* fix: test

* test: add compatibility test case

* refactor: remove task executor

* feat: add secret after resolving conflict
  • Loading branch information
jyoo0515 authored Jul 6, 2024
1 parent 5d1d0b3 commit 9eae493
Show file tree
Hide file tree
Showing 18 changed files with 430 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,8 @@ paths:
type: array
items:
$ref: '#/components/schemas/UserMobilityToolDto'
isNewsLetterSubscriptionAgreed:
type: boolean
required:
- nickname
- email
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package club.staircrusher.user.application.port.`in`

import club.staircrusher.stdlib.clock.SccClock
import club.staircrusher.stdlib.di.annotation.Component
import club.staircrusher.stdlib.domain.SccDomainException
import club.staircrusher.stdlib.domain.entity.EntityIdGenerator
Expand All @@ -9,11 +10,12 @@ import club.staircrusher.stdlib.validation.email.EmailValidator
import club.staircrusher.user.application.port.out.persistence.UserAuthInfoRepository
import club.staircrusher.user.domain.model.AuthTokens
import club.staircrusher.user.application.port.out.persistence.UserRepository
import club.staircrusher.user.application.port.out.web.subscription.StibeeSubscriptionService
import club.staircrusher.user.domain.model.User
import club.staircrusher.user.domain.model.UserMobilityTool
import club.staircrusher.user.domain.service.PasswordEncryptor
import club.staircrusher.user.domain.service.UserAuthService
import java.time.Clock
import kotlinx.coroutines.runBlocking

@Component
class UserApplicationService(
Expand All @@ -22,8 +24,9 @@ class UserApplicationService(
private val userAuthService: UserAuthService,
private val passwordEncryptor: PasswordEncryptor,
private val userAuthInfoRepository: UserAuthInfoRepository,
private val clock: Clock,
private val stibeeSubscriptionService: StibeeSubscriptionService,
) {

@Deprecated("닉네임 로그인은 사라질 예정")
fun signUpWithNicknameAndPassword(
nickname: String,
Expand Down Expand Up @@ -61,7 +64,7 @@ class UserApplicationService(
instagramId = params.instagramId?.trim()?.takeIf { it.isNotEmpty() },
email = params.email,
mobilityTools = mutableListOf(),
createdAt = clock.instant(),
createdAt = SccClock.instant(),
)
)
}
Expand All @@ -88,6 +91,7 @@ class UserApplicationService(
instagramId: String?,
email: String,
mobilityTools: List<UserMobilityTool>,
isNewsLetterSubscriptionAgreed: Boolean,
): User = transactionManager.doInTransaction(TransactionIsolationLevel.REPEATABLE_READ) {
val user = userRepository.findById(userId)
user.nickname = run {
Expand Down Expand Up @@ -126,13 +130,21 @@ class UserApplicationService(
user.mobilityTools.clear()
user.mobilityTools.addAll(mobilityTools)
userRepository.save(user)

if (isNewsLetterSubscriptionAgreed) {
transactionManager.doAfterCommit {
user.email?.let { subscribeToNewsLetter(it, user.nickname) }
}
}

return@doInTransaction user
}

fun deleteUser(
userId: String,
) = transactionManager.doInTransaction (TransactionIsolationLevel.REPEATABLE_READ) {
val user = userRepository.findById(userId)
user.delete(clock.instant())
user.delete(SccClock.instant())
userRepository.save(user)

userAuthInfoRepository.removeByUserId(userId)
Expand All @@ -149,4 +161,15 @@ class UserApplicationService(
fun getAllUsers(): List<User> {
return userRepository.findAll()
}

private fun subscribeToNewsLetter(email: String, name: String) {
runBlocking {
stibeeSubscriptionService.registerSubscriber(
email = email,
name = name,
// 일단 false 로 두지만 나중에 동의 버튼이 추가될 수도 있다
isMarketingPushAgreed = false,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package club.staircrusher.user.application.port.out.web.subscription

interface StibeeSubscriptionService {
suspend fun registerSubscriber(email: String, name: String, isMarketingPushAgreed: Boolean): Boolean
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
id("org.springframework.boot")
id("io.spring.dependency-management")
kotlin("plugin.serialization")
}

dependencies {
Expand All @@ -19,4 +20,5 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinxSerializationVersion")

integrationTestImplementation(projects.crossCuttingConcern.test.springIt)
testImplementation("org.mockito.kotlin:mockito-kotlin:5.1.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,25 @@ import club.staircrusher.api.spec.dto.ApiErrorResponse
import club.staircrusher.api.spec.dto.UpdateUserInfoPost200Response
import club.staircrusher.api.spec.dto.UpdateUserInfoPostRequest
import club.staircrusher.stdlib.testing.SccRandom
import club.staircrusher.user.application.port.out.web.subscription.StibeeSubscriptionService
import club.staircrusher.user.domain.model.UserMobilityTool
import club.staircrusher.user.infra.adapter.`in`.controller.base.UserITBase
import club.staircrusher.user.infra.adapter.`in`.converter.toDTO
import club.staircrusher.user.infra.adapter.`in`.converter.toModel
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotEquals
import org.junit.jupiter.api.Test
import java.util.UUID
import org.mockito.kotlin.any
import org.mockito.kotlin.atLeastOnce
import org.mockito.kotlin.eq
import org.mockito.kotlin.never
import org.mockito.kotlin.verifyBlocking
import org.springframework.boot.test.mock.mockito.MockBean

class UpdateUserInfoTest : UserITBase() {
@MockBean
lateinit var stibeeSubscriptionService: StibeeSubscriptionService

@Test
fun updateUserInfoTest() {
val user = transactionManager.doInTransaction {
Expand Down Expand Up @@ -155,4 +164,107 @@ class UpdateUserInfoTest : UserITBase() {
assertEquals(ApiErrorResponse.Code.INVALID_EMAIL, result.code)
}
}

@Test
fun `뉴스레터 수신에 동의하면 stibee 에 연동한다`() {
val user = transactionManager.doInTransaction {
testDataGenerator.createUser()
}

val changedNickname = SccRandom.string(32)
val changedInstagramId = SccRandom.string(32)
val changedEmail = "${SccRandom.string(32)}@staircrusher.club"
val changedMobilityTools = listOf(
UserMobilityTool.ELECTRIC_WHEELCHAIR,
UserMobilityTool.WALKING_ASSISTANCE_DEVICE,
)

val params = UpdateUserInfoPostRequest(
nickname = changedNickname,
instagramId = changedInstagramId,
email = changedEmail,
mobilityTools = changedMobilityTools.map { it.toDTO() },
isNewsLetterSubscriptionAgreed = true,
)

mvc
.sccRequest("/updateUserInfo", params, user = user)
.andExpect {
status {
isOk()
}
}
.apply {
verifyBlocking(stibeeSubscriptionService, atLeastOnce()) { registerSubscriber(eq(changedEmail), eq(changedNickname), any()) }
}
}

@Test
fun `뉴스레터 수신에 동의하지 않으면 stibee 에 연동하지 않는다`() {
val user = transactionManager.doInTransaction {
testDataGenerator.createUser()
}

val changedNickname = SccRandom.string(32)
val changedInstagramId = SccRandom.string(32)
val changedEmail = "${SccRandom.string(32)}@staircrusher.club"
val changedMobilityTools = listOf(
UserMobilityTool.ELECTRIC_WHEELCHAIR,
UserMobilityTool.WALKING_ASSISTANCE_DEVICE,
)

val params = UpdateUserInfoPostRequest(
nickname = changedNickname,
instagramId = changedInstagramId,
email = changedEmail,
mobilityTools = changedMobilityTools.map { it.toDTO() },
isNewsLetterSubscriptionAgreed = false,
)

mvc
.sccRequest("/updateUserInfo", params, user = user)
.andExpect {
status {
isOk()
}
}
.apply {
verifyBlocking(stibeeSubscriptionService, never()) { registerSubscriber(eq(changedEmail), eq(changedNickname), any()) }
}
}

@Test
fun `명시적인 동의 없이는 stibee 에 연동하지 않는다`() {
val user = transactionManager.doInTransaction {
testDataGenerator.createUser()
}

val changedNickname = SccRandom.string(32)
val changedInstagramId = SccRandom.string(32)
val changedEmail = "${SccRandom.string(32)}@staircrusher.club"
val changedMobilityTools = listOf(
UserMobilityTool.ELECTRIC_WHEELCHAIR,
UserMobilityTool.WALKING_ASSISTANCE_DEVICE,
)

val params = UpdateUserInfoPostRequest(
nickname = changedNickname,
instagramId = changedInstagramId,
email = changedEmail,
mobilityTools = changedMobilityTools.map { it.toDTO() },
// 하위 호환성
isNewsLetterSubscriptionAgreed = null,
)

mvc
.sccRequest("/updateUserInfo", params, user = user)
.andExpect {
status {
isOk()
}
}
.apply {
verifyBlocking(stibeeSubscriptionService, never()) { registerSubscriber(eq(changedEmail), eq(changedNickname), any()) }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class UserController(
instagramId = request.instagramId,
email = request.email,
mobilityTools = request.mobilityTools.map { it.toModel() },
isNewsLetterSubscriptionAgreed = request.isNewsLetterSubscriptionAgreed ?: false,
)
return UpdateUserInfoPost200Response(
user = updatedUser.toDTO(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ internal open class AppleLoginApiClientConfiguration {
it.addHandlerLast(ReadTimeoutHandler(READ_TIMEOUT.toSeconds(), TimeUnit.SECONDS))
it.addHandlerLast(WriteTimeoutHandler(WRITE_TIMEOUT.toSeconds(), TimeUnit.SECONDS))
}
.responseTimeout(Duration.ofSeconds(2))
.responseTimeout(RESPONSE_TIMEOUT)

val decoder = KotlinSerializationJsonDecoder(Json { ignoreUnknownKeys = true })

Expand All @@ -54,5 +54,6 @@ internal open class AppleLoginApiClientConfiguration {
private val CONNECT_TIMEOUT = Duration.ofSeconds(10)
private val READ_TIMEOUT = Duration.ofSeconds(10)
private val WRITE_TIMEOUT = Duration.ofSeconds(10)
private val RESPONSE_TIMEOUT = Duration.ofSeconds(2)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package club.staircrusher.user.infra.adapter.out.web.subscription

import org.springframework.boot.context.properties.ConfigurationProperties

@ConfigurationProperties("scc.stibee")
data class StibeeProperties(
/**
* 스티비에서 생성한 API 키입니다.
*/
val apiKey: String,
/**
* 주소록에 할당된 고유의 아이디입니다.
* 주소록 목록에서 주소록 이름을 클릭하여 "주소록 대시보드"로 이동한 뒤, 브라우저에 표시되는 URL에서 확인할 수 있습니다.
* ref: https://api.stibee.com/docs/
*/
val listId: String,
)
Loading

0 comments on commit 9eae493

Please sign in to comment.