Skip to content

Commit

Permalink
Merge pull request #2 from prgrms-web-devcourse-final-project/QUZ-43-…
Browse files Browse the repository at this point in the history
…user

[Quz-43][FEATURE] user service 작성
  • Loading branch information
HMWG authored Nov 17, 2024
2 parents c269521 + ea12df0 commit 96ed3f6
Show file tree
Hide file tree
Showing 23 changed files with 720 additions and 2 deletions.
3 changes: 3 additions & 0 deletions user-service/user-domain/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
dependencies {
api(project(":common"))
implementation("org.springframework:spring-context")


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.grepp.quizy.domain.user

enum class AuthProvider {
GOOGLE,
KAKAO
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.grepp.quizy.domain.user

data class ProviderType(
val provider: AuthProvider,
val providerId: String
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.grepp.quizy.domain.user

enum class Role(
val key: String,
val title: String
) {
USER("ROLE_USER", "일반 사용자"),
ADMIN("ROLE_ADMIN", "관리자"),
GUEST("ROLE_GUEST", "손님"),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.grepp.quizy.domain.user

import java.time.LocalDateTime


class User (
private val id: UserId,
private val userProfile: UserProfile,
private val role: Role = Role.USER,
private val provider: ProviderType,
) {
fun getId(): UserId = id

fun getUserProfile(): UserProfile = userProfile

fun getRole(): Role = role

fun getProvider(): ProviderType = provider
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.grepp.quizy.domain.user

import org.springframework.stereotype.Component

@Component
class UserAppender (
private val userRepository: UserRepository
) {
fun append(user: User): User =
userRepository.save(user)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.grepp.quizy.domain.user

import com.grepp.quizy.common.domain.BaseId

class UserId(
id: Long = 0
) : BaseId<Long>(id)
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.grepp.quizy.domain.user

data class UserProfile(
val name: String,
val email: String,
val profileImageUrl: String = "https://cdn.pixabay.com/photo/2015/10/05/22/37/blank-profile-picture-973460_1280.png" // TODO : S3 기본 썸네일 등록하기
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.grepp.quizy.domain.user

import org.springframework.stereotype.Component

@Component
class UserReader (
private val userRepository: UserRepository
) {
fun read(userId: Long): User =
userRepository.findById(userId)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.grepp.quizy.domain.user

import org.springframework.stereotype.Component

@Component
class UserRemover (
private val userRepository: UserRepository
) {
fun remove(user: User) =
userRepository.delete(user)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.grepp.quizy.domain.user

interface UserRepository {
fun findById(id: Long): User
fun existsByEmail(email: String): Boolean
fun save(user: User): User
fun delete(user: User)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.grepp.quizy.domain.user

import org.springframework.stereotype.Service

@Service
class UserService (
private val userAppender: UserAppender,
private val userReader: UserReader,
private val userRemover: UserRemover
) {
fun appendUser(user: User): User {
return userAppender.append(user)
}

fun getUser(userId: Long): User {
return userReader.read(userId)
}

fun removeUser(user: User) {
userRemover.remove(user)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.grepp.quizy.domain.user.exception

import com.grepp.quizy.common.exception.BaseErrorCode
import com.grepp.quizy.common.exception.ErrorReason

enum class UserErrorCode (
private val status: Int,
private val errorCode: String,
private val message: String
) : BaseErrorCode {
USER_NOT_FOUND(404, "U001", "해당 유저를 찾지 못했습니다."),
USER_ALREADY_EXISTS(409, "U002", "이미 존재하는 유저입니다."),
;

override val errorReason: ErrorReason
get() = ErrorReason(status, errorCode, message)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package com.grepp.quizy.damin.user

import com.grepp.quizy.domain.user.*
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.shouldBe
import io.mockk.*

class UserServiceTest : DescribeSpec({
// Mocks
val userAppender = mockk<UserAppender>()
val userReader = mockk<UserReader>()
val userRemover = mockk<UserRemover>()

// System Under Test
val userService = UserService(userAppender, userReader, userRemover)

// Test Data
val userId = UserId(1)
val email = "[email protected]"
val userProfile = UserProfile(
name = "Test User",
email = email,
profileImageUrl = "http://example.com/test.jpg"
)
val providerVO = ProviderType(
provider = AuthProvider.KAKAO,
providerId = "kakaoId"
)
val user = User(
id = userId,
userProfile = userProfile,
provider = providerVO,
role = Role.USER
)

beforeSpec {
// MockK 초기화
clearAllMocks()
}

afterSpec {
// 테스트 종료 후 정리
clearAllMocks()
}

describe("append 에서") {
context("새로운 유저가 주어졌을 때") {
// Context
every { userAppender.append(user) } returns user

it("userAppender를 통해 유저를 추가한다") {
// Execute
val newUser = userService.appendUser(user)

// Verify
newUser shouldBe user
verify(exactly = 1) { userAppender.append(user) }
}
}
}

describe("getUser 에서") {
context("존재하는 ID의 유저가 주어졌을 때") {
// Context
every { userReader.read(userId.value) } returns user

it("userReader를 통해 유저를 조회하여 반환한다") {
// Execute
val result = userService.getUser(userId.value)

// Verify
result shouldBe user
verify(exactly = 1) { userReader.read(userId.value) }
}
}

context("존재하지 않는 ID의 유저가 주어졌을 때") {
// Context
val nonExistentId = 999L
every { userReader.read(nonExistentId) } throws RuntimeException()

it("UserNotFoundException을 발생시킨다") {
// Execute & Verify
shouldThrow<RuntimeException> {
userService.getUser(nonExistentId)
}
verify(exactly = 1) { userReader.read(nonExistentId) }
}
}
}

describe("remove 에서") {
context("삭제할 유저가 주어졌을 때") {
// Context
every { userRemover.remove(user) } just runs

it("userRemover를 통해 유저를 삭제한다") {
// Execute
userService.removeUser(user)

// Verify
verify(exactly = 1) { userRemover.remove(user) }
}
}
}
}) {
}
44 changes: 42 additions & 2 deletions user-service/user-infra/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,43 @@
plugins {
id("io.kotest.multiplatform") version "5.0.2"
kotlin("plugin.jpa") version "1.8.22"
}

allOpen {
annotation("jakarta.persistence.Entity")
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}

dependencies {
implementation(project(":common"))
}
api(project(":infrastructure:kafka"))
api(project(":common:common-jpa"))

implementation(project(":user-service:user-domain"))

implementation("org.springframework.boot:spring-boot-starter-data-jpa")
implementation("mysql:mysql-connector-java:8.0.28")

// Redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")
implementation("io.lettuce:lettuce-core")

// JWT
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.5")

// Spring Security
implementation("org.springframework.boot:spring-boot-starter-security")
testImplementation("org.springframework.security:spring-security-test")

// OAuth2
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")

// Feign
//implementation("org.springframework.cloud:spring-cloud-starter-openfeign")

// H2 Database
runtimeOnly("com.h2database:h2")
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.grepp.quizy.infra.user.entity

import com.grepp.quizy.domain.user.AuthProvider
import com.grepp.quizy.domain.user.ProviderType
import jakarta.persistence.Column
import jakarta.persistence.Embeddable
import jakarta.persistence.EnumType
import jakarta.persistence.Enumerated

@Embeddable
data class ProviderTypeVO (
@Enumerated(EnumType.STRING)
@Column(nullable = false)
val provider: AuthProvider,

@Column(nullable = false)
val providerId: String
) {
fun toDomain(): ProviderType {
return ProviderType(provider, providerId)
}

companion object {
fun from(providerType: ProviderType): ProviderTypeVO {
return ProviderTypeVO(providerType.provider, providerType.providerId)
}
}
}
Loading

0 comments on commit 96ed3f6

Please sign in to comment.