diff --git a/build.gradle.kts b/build.gradle.kts index 66af151..64c4db4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -5,6 +5,9 @@ plugins { kotlin("jvm") version "1.9.24" kotlin("plugin.spring") version "1.9.24" kotlin("plugin.jpa") version "1.9.24" //Entity의 기본생성자를 자동으로 만들어줌 + + java + jacoco } group = "org.store" @@ -18,6 +21,7 @@ java { repositories { mavenCentral() + maven(url = "https://jitpack.io") } dependencies { @@ -82,12 +86,75 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-data-redis") //redis testRuntimeOnly("org.junit.platform:junit-platform-launcher") + // 아임포트(결제) 관련 + implementation("com.github.iamport:iamport-rest-client-java:0.2.23") + //vault implementation("org.springframework.cloud:spring-cloud-starter-bootstrap:4.1.4") implementation("org.springframework.cloud:spring-cloud-config-server:4.1.3") implementation("org.springframework.cloud:spring-cloud-starter-vault-config:4.1.3") } +jacoco { + // JaCoCo 버전 + toolVersion = "0.8.8" + +// 테스트결과 리포트를 저장할 경로 변경 +// default는 "${project.reporting.baseDir}/jacoco" +// reportsDir = file("$buildDir/customJacocoReportDir") +} + +// kotlin DSL + +tasks.jacocoTestReport { + reports { + // 원하는 리포트를 켜고 끌 수 있습니다. +// html.isEnabled = true +// xml.isEnabled = false +// csv.isEnabled = false + + //.isEnabled 가 Deprecated 되었습니다 (저는 gradle 7.2 버전에 kotlin DSL 사용하고 있습니다) + html.required.set(true) + xml.required.set(false) + csv.required.set(false) + +// 각 리포트 타입 마다 리포트 저장 경로를 설정할 수 있습니다. +// html.destination = file("$buildDir/jacocoHtml") +// xml.destination = file("$buildDir/jacoco.xml") + } +} + +tasks.jacocoTestCoverageVerification { + violationRules { + rule { + // 룰을 간단히 켜고 끌 수 있다. + enabled = true + + // 룰을 체크할 단위는 클래스 단위 + element = "CLASS" + + // 메서드 커버리지를 최소한 00% 만족시켜야 한다. + limit { + counter = "METHOD" + value = "COVEREDRATIO" + minimum = "0.00".toBigDecimal() + } + + // 라인 커버리지를 최소한 00% 만족시켜야 한다. + limit { + counter = "LINE" + value = "COVEREDRATIO" + minimum = "0.00".toBigDecimal() + } + } + } +} + tasks.withType { useJUnitPlatform() + finalizedBy("jacocoTestReport") +} + +tasks.jacocoTestReport { + finalizedBy("jacocoTestCoverageVerification") } diff --git a/src/main/kotlin/org/store/clothstar/common/config/CustomAuthenticationEntryPoint.kt b/src/main/kotlin/org/store/clothstar/common/config/CustomAuthenticationEntryPoint.kt index d9464ef..b1a4293 100644 --- a/src/main/kotlin/org/store/clothstar/common/config/CustomAuthenticationEntryPoint.kt +++ b/src/main/kotlin/org/store/clothstar/common/config/CustomAuthenticationEntryPoint.kt @@ -1,6 +1,5 @@ package org.store.clothstar.common.config -import com.fasterxml.jackson.databind.ObjectMapper import io.github.oshai.kotlinlogging.KotlinLogging import jakarta.servlet.ServletException import jakarta.servlet.http.HttpServletRequest @@ -8,7 +7,6 @@ import jakarta.servlet.http.HttpServletResponse import org.springframework.security.core.AuthenticationException import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.stereotype.Component -import org.store.clothstar.common.dto.MessageDTO import java.io.IOException @Component @@ -23,15 +21,6 @@ class CustomAuthenticationEntryPoint : AuthenticationEntryPoint { ) { log.error { "인증 실패 로직 실행" } - response.status = HttpServletResponse.SC_UNAUTHORIZED - response.characterEncoding = "UTF-8" - response.contentType = "application/json" - - val messageDTO = MessageDTO( - HttpServletResponse.SC_UNAUTHORIZED, - "권한이 없습니다." - ) - - response.writer.write(ObjectMapper().writeValueAsString(messageDTO)) + response.sendRedirect(request.contextPath + "/login") } } diff --git a/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt b/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt index f777fd3..28a6278 100644 --- a/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt +++ b/src/main/kotlin/org/store/clothstar/common/config/SecurityConfiguration.kt @@ -14,6 +14,7 @@ import org.springframework.security.config.annotation.web.configurers.* import org.springframework.security.config.http.SessionCreationPolicy import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.web.AuthenticationEntryPoint import org.springframework.security.web.SecurityFilterChain import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter import org.store.clothstar.common.config.jwt.JwtAuthenticationFilter @@ -25,6 +26,7 @@ class SecurityConfiguration( private val authenticationConfiguration: AuthenticationConfiguration, private val jwtAuthenticationFilter: JwtAuthenticationFilter, private val jwtUtil: JwtUtil, + private val customAuthenticationEntryPoint: AuthenticationEntryPoint, ) { @Bean fun passwordEncoder(): PasswordEncoder { @@ -52,6 +54,10 @@ class SecurityConfiguration( .csrf { obj: CsrfConfigurer -> obj.disable() } .httpBasic { obj: HttpBasicConfigurer -> obj.disable() } .formLogin { obj: FormLoginConfigurer -> obj.disable() } + .exceptionHandling { exception -> + exception.authenticationEntryPoint(customAuthenticationEntryPoint) + } + http.authorizeHttpRequests( Customizer { auth -> @@ -60,7 +66,7 @@ class SecurityConfiguration( "/", "/login", "/userPage", "/sellerPage", "/adminPage", "/main", "/v1/members/login", "/signup", "/v1/members/email/**", "/v1/access", "/v1/categories/**", "/v1/products/**", "/v1/productLines/**", "/v2/productLines/**", - "/productLinePagingSlice", "/productLinePagingOffset", + "/productPagingSlice", "/productPagingOffset", "/v3/products/**", "v3/sellers/products/**", "/v1/orderdetails", "/v1/orders", "membersPagingOffset", "membersPagingSlice", "/v1/orderdetails", "/v1/orders", "/v2/orders", "/v3/orders", "/v1/orders/list", @@ -76,6 +82,7 @@ class SecurityConfiguration( "/v1/members?signUpType=KAKAO", "/v1/members/**", "/v1/members/auth/**", + "config-service/**", "products/**", "productDetail", "payment/**", "/v1/payments/**" ).permitAll() .requestMatchers(HttpMethod.POST, "/v1/members").permitAll() .requestMatchers(HttpMethod.POST, "/v1/sellers/**").authenticated() diff --git a/src/main/kotlin/org/store/clothstar/member/application/MemberServiceApplication.kt b/src/main/kotlin/org/store/clothstar/member/application/MemberServiceApplication.kt index 0f3a0c0..e93f16e 100644 --- a/src/main/kotlin/org/store/clothstar/member/application/MemberServiceApplication.kt +++ b/src/main/kotlin/org/store/clothstar/member/application/MemberServiceApplication.kt @@ -4,6 +4,7 @@ import org.springframework.data.domain.Page import org.springframework.data.domain.Pageable import org.springframework.data.domain.Slice import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import org.store.clothstar.member.authentication.service.AuthenticationService import org.store.clothstar.member.domain.MemberRole import org.store.clothstar.member.dto.request.CreateMemberRequest @@ -13,6 +14,7 @@ import org.store.clothstar.member.service.AccountService import org.store.clothstar.member.service.MemberService @Service +@Transactional class MemberServiceApplication( private val memberService: MemberService, private val accountService: AccountService, @@ -42,11 +44,13 @@ class MemberServiceApplication( memberService.updatePassword(accountId, password) } + @Transactional fun updateDeleteAt(memberId: Long) { memberService.updateDeleteAt(memberId) accountService.updateDeletedAt(memberId, MemberRole.USER) } + @Transactional fun signUp(createMemberRequest: CreateMemberRequest): Long { //인증번호 확인 authenticationService.verifyEmailCertifyNum(createMemberRequest.email, createMemberRequest.certifyNum) diff --git a/src/main/kotlin/org/store/clothstar/member/service/AccountServiceImpl.kt b/src/main/kotlin/org/store/clothstar/member/service/AccountServiceImpl.kt index 17412d8..9bbf375 100644 --- a/src/main/kotlin/org/store/clothstar/member/service/AccountServiceImpl.kt +++ b/src/main/kotlin/org/store/clothstar/member/service/AccountServiceImpl.kt @@ -1,8 +1,8 @@ package org.store.clothstar.member.service -import jakarta.transaction.Transactional import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional import org.store.clothstar.common.error.ErrorCode import org.store.clothstar.common.error.exception.DuplicatedEmailException import org.store.clothstar.common.error.exception.NotFoundAccountException diff --git a/src/main/kotlin/org/store/clothstar/member/service/MemberServiceImpl.kt b/src/main/kotlin/org/store/clothstar/member/service/MemberServiceImpl.kt index 1fc00d9..d97567e 100644 --- a/src/main/kotlin/org/store/clothstar/member/service/MemberServiceImpl.kt +++ b/src/main/kotlin/org/store/clothstar/member/service/MemberServiceImpl.kt @@ -63,7 +63,7 @@ class MemberServiceImpl( override fun getMemberById(memberId: Long): MemberResponse { log.info { "회원 상세 조회 memberId = ${memberId}" } - return memberRepository.findByMemberId(memberId)?.let { member -> + return memberRepository.findByIdOrNull(memberId)?.let { member -> MemberResponse( memberId = member.memberId!!, name = member.name, diff --git a/src/main/kotlin/org/store/clothstar/payment/application/PaymentServiceApplication.kt b/src/main/kotlin/org/store/clothstar/payment/application/PaymentServiceApplication.kt new file mode 100644 index 0000000..a8aa355 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/application/PaymentServiceApplication.kt @@ -0,0 +1,28 @@ +package org.store.clothstar.payment.application + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.store.clothstar.payment.dto.request.SavePaymentRequest +import org.store.clothstar.payment.service.PaymentService +import org.store.clothstar.product.service.ItemService + +@Service +class PaymentServiceApplication( + private val itemService: ItemService, + private val paymentService: PaymentService, +) { + private val log = KotlinLogging.logger {} + + @Transactional + fun savePayment(savePaymentRequest: SavePaymentRequest) { + //재고차감 + log.info { "재고차감 로직 실행" } + val item = itemService.getItemById(savePaymentRequest.itemId) + itemService.deductItemStock(item, savePaymentRequest.buyQuantity) + + //구매이력 저장 + log.info { "구매이력 저장 실행" } + paymentService.savePayment(savePaymentRequest) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/payment/controller/PaymentController.kt b/src/main/kotlin/org/store/clothstar/payment/controller/PaymentController.kt new file mode 100644 index 0000000..c324da4 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/controller/PaymentController.kt @@ -0,0 +1,32 @@ +package org.store.clothstar.payment.controller + +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.http.HttpStatus +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import org.store.clothstar.common.dto.MessageDTO +import org.store.clothstar.payment.application.PaymentServiceApplication +import org.store.clothstar.payment.dto.request.SavePaymentRequest + +@RestController +class PaymentController( + private val paymentServiceApplication: PaymentServiceApplication, +) { + private val log = KotlinLogging.logger {} + + @PostMapping("/v1/payments") + fun savePayment(@RequestBody savePaymentRequest: SavePaymentRequest): ResponseEntity { + log.info { "/v1/payment post 요청 실행" } + + paymentServiceApplication.savePayment(savePaymentRequest) + + val messageDTO = MessageDTO( + HttpStatus.OK.value(), + "상품을 구매 완료 하였습니다." + ) + + return ResponseEntity(messageDTO, HttpStatus.OK) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/payment/controller/PaymentViewController.kt b/src/main/kotlin/org/store/clothstar/payment/controller/PaymentViewController.kt new file mode 100644 index 0000000..1004931 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/controller/PaymentViewController.kt @@ -0,0 +1,17 @@ +package org.store.clothstar.payment.controller + +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable + + +@Controller +class PaymentViewController { + @GetMapping("/payment/{productId}/{itemId}") + fun paymentPage(@PathVariable itemId: Long, @PathVariable productId: Long, model: Model): String { + model.addAttribute("productId", productId) + model.addAttribute("itemId", itemId) + return "payment" + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/payment/domain/Payment.kt b/src/main/kotlin/org/store/clothstar/payment/domain/Payment.kt new file mode 100644 index 0000000..a62a884 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/domain/Payment.kt @@ -0,0 +1,28 @@ +package org.store.clothstar.payment.domain + +import jakarta.persistence.Entity +import jakarta.persistence.GeneratedValue +import jakarta.persistence.GenerationType +import jakarta.persistence.Id + +@Entity +class Payment( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + val paymentId: Long? = null, + + var productId: Long, + var itemId: Long, + var impUid: String, + var merchantUid: String, + var itemName: String, + var itemOption: String, + var paidAmount: Int, + var buyQuantity: Int, + var buyerName: String, + var buyerEmail: String, + var buyerTelNo: String, + var buyerAddr: String, + var buyerPostCode: String, +) { +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/payment/dto/request/SavePaymentRequest.kt b/src/main/kotlin/org/store/clothstar/payment/dto/request/SavePaymentRequest.kt new file mode 100644 index 0000000..22d3d9f --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/dto/request/SavePaymentRequest.kt @@ -0,0 +1,63 @@ +package org.store.clothstar.payment.dto.request + +import jakarta.validation.constraints.NotBlank +import org.store.clothstar.payment.domain.Payment + +class SavePaymentRequest( + @field: NotBlank + var productId: Long, + + @field: NotBlank + var itemId: Long, + + @field: NotBlank + var impUid: String, + + @field: NotBlank + var merchantUid: String, + + @field: NotBlank + var itemName: String, + + @field: NotBlank + var itemOption: String, + + @field: NotBlank + var paidAmount: Int, + + @field: NotBlank + var buyQuantity: Int, + + @field: NotBlank + var buyerName: String, + + @field: NotBlank + var buyerEmail: String, + + @field: NotBlank + var buyerTelNo: String, + + @field: NotBlank + var buyerAddr: String, + + @field: NotBlank + var buyerPostCode: String, +) { + fun toPayment(): Payment { + return Payment( + productId = productId, + itemId = itemId, + impUid = impUid, + merchantUid = merchantUid, + itemName = itemName, + itemOption = itemOption, + paidAmount = paidAmount, + buyQuantity = buyQuantity, + buyerName = buyerName, + buyerEmail = buyerEmail, + buyerTelNo = buyerTelNo, + buyerAddr = buyerAddr, + buyerPostCode = buyerPostCode, + ) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/payment/repository/PaymentRepository.kt b/src/main/kotlin/org/store/clothstar/payment/repository/PaymentRepository.kt new file mode 100644 index 0000000..a2fc823 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/repository/PaymentRepository.kt @@ -0,0 +1,7 @@ +package org.store.clothstar.payment.repository + +import org.springframework.data.jpa.repository.JpaRepository +import org.store.clothstar.payment.domain.Payment + +interface PaymentRepository : JpaRepository { +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/payment/service/PaymentService.kt b/src/main/kotlin/org/store/clothstar/payment/service/PaymentService.kt new file mode 100644 index 0000000..e9ce449 --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/payment/service/PaymentService.kt @@ -0,0 +1,17 @@ +package org.store.clothstar.payment.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.store.clothstar.payment.dto.request.SavePaymentRequest +import org.store.clothstar.payment.repository.PaymentRepository + +@Service +class PaymentService( + val paymentRepository: PaymentRepository +) { + @Transactional + fun savePayment(savePaymentRequest: SavePaymentRequest) { + val payment = savePaymentRequest.toPayment() + paymentRepository.save(payment) + } +} \ No newline at end of file diff --git a/src/main/kotlin/org/store/clothstar/product/controller/ProductViewController.kt b/src/main/kotlin/org/store/clothstar/product/controller/ProductViewController.kt new file mode 100644 index 0000000..a455adf --- /dev/null +++ b/src/main/kotlin/org/store/clothstar/product/controller/ProductViewController.kt @@ -0,0 +1,20 @@ +package org.store.clothstar.product.controller + +import org.springframework.stereotype.Controller +import org.springframework.ui.Model +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PathVariable + +@Controller +class ProductViewController { + @GetMapping("/productPagingOffset") + fun productLinePagingOffset(): String { + return "productOffsetList" + } + + @GetMapping("/products/{productId}") + fun productDetail(@PathVariable productId: Long, model: Model): String { + model.addAttribute("productId", productId) + return "productDetail" + } +} \ No newline at end of file diff --git a/src/main/resources/templates/payment.html b/src/main/resources/templates/payment.html new file mode 100644 index 0000000..e497fa1 --- /dev/null +++ b/src/main/resources/templates/payment.html @@ -0,0 +1,304 @@ + + + + + 결제 화면 + + + + + + + + + + +
+
+

주문 상세

+
+ +
+ + +
+
+ +
+
+
+ +
+
+ +
+
+ + +
+
+ + +
+ +
+

구매자 정보

+
+
+ + +
+
+ +
+
+ +
+ +
+
+ +
+

배송지

+
+ +
+ +
+ + + + + +
+ +
+ +
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/paymentList.html b/src/main/resources/templates/paymentList.html new file mode 100644 index 0000000..e69de29 diff --git a/src/main/resources/templates/productDetail.html b/src/main/resources/templates/productDetail.html new file mode 100644 index 0000000..f7288c1 --- /dev/null +++ b/src/main/resources/templates/productDetail.html @@ -0,0 +1,99 @@ + + + + + + 상품 상세 + + + + +
+ + + +

+ + + + + + + + + + + +
ImageNamedescriptionPrice
+
+ +
+ + + + + + + + + + + + + +
itemIdNamePriceStock구매
+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/productLineOffsetList.html b/src/main/resources/templates/productOffsetList.html similarity index 81% rename from src/main/resources/templates/productLineOffsetList.html rename to src/main/resources/templates/productOffsetList.html index 2ea2893..71d9306 100644 --- a/src/main/resources/templates/productLineOffsetList.html +++ b/src/main/resources/templates/productOffsetList.html @@ -8,6 +8,11 @@ justify-content: center; margin-top: 20px; } + + tr:hover { + cursor: pointer; + background-color: #f0f0f0; /* 마우스를 올렸을 때 배경색 변경 */ + } @@ -39,11 +44,8 @@

Product Lines List (Offset Paging)

Name Content Price - Total Stock Status - Created At - Brand Names - Option Names + Brand Name @@ -76,27 +78,22 @@

Product Lines List (Offset Paging)

function loadProductLines(page = 0) { const searchKeyword = document.getElementById('search-input').value; const sortOption = document.getElementById('sort-select').value; - const url = `/v1/productLines/offset?page=${page}&keyword=${searchKeyword}&sort=${sortOption}`; + const url = `/v3/products/offset?page=${page}&keyword=${searchKeyword}&sort=${sortOption}`; fetch(url) .then(response => response.json()) .then(data => { const tableBody = document.getElementById('product-lines-table-body'); tableBody.innerHTML = ''; // 기존 내용을 지웁니다. - data.content.forEach((productLine) => { - const brandNames = productLine.productList.map(product => product.brandName).join(', '); - const optionNames = productLine.productList.map(product => product.name).join(', '); + data.content.forEach((products) => { const row = ` - - ${productLine.productLineId} - ${productLine.name} - ${productLine.content} - ${productLine.price} - ${productLine.totalStock} - ${productLine.status} - ${productLine.createdAt} - ${brandNames} - ${optionNames} + + ${products.productId} + ${products.name} + ${products.content} + ${products.price} + ${products.saleStatus} + ${products.seller.brandName} `; tableBody.innerHTML += row; diff --git a/src/test/kotlin/org/store/clothstar/common/config/VaultDataTest.kt b/src/test/kotlin/org/store/clothstar/common/config/VaultDataTest.kt new file mode 100644 index 0000000..a886413 --- /dev/null +++ b/src/test/kotlin/org/store/clothstar/common/config/VaultDataTest.kt @@ -0,0 +1,16 @@ +package org.store.clothstar.common.config + +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class VaultDataTest { + @Value("\${email.send}") + lateinit var email: String + + @Test + fun demoTest() { + println("email : ${email}") + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/store/clothstar/member/application/MemberServiceApplicationTransactionTest.kt b/src/test/kotlin/org/store/clothstar/member/application/MemberServiceApplicationTransactionTest.kt new file mode 100644 index 0000000..04d0831 --- /dev/null +++ b/src/test/kotlin/org/store/clothstar/member/application/MemberServiceApplicationTransactionTest.kt @@ -0,0 +1,142 @@ +package org.store.clothstar.member.application + +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.junit5.MockKExtension +import jakarta.persistence.EntityManager +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.mockito.Mockito.doThrow +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.MockBean +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.data.repository.findByIdOrNull +import org.springframework.test.context.ActiveProfiles +import org.springframework.transaction.PlatformTransactionManager +import org.springframework.transaction.annotation.Transactional +import org.springframework.transaction.support.TransactionTemplate +import org.store.clothstar.common.error.ErrorCode +import org.store.clothstar.common.error.exception.NotFoundMemberException +import org.store.clothstar.member.authentication.service.AuthenticationService +import org.store.clothstar.member.domain.Account +import org.store.clothstar.member.domain.Member +import org.store.clothstar.member.domain.MemberRole +import org.store.clothstar.member.repository.AccountRepository +import org.store.clothstar.member.repository.MemberRepository +import org.store.clothstar.member.service.AccountService +import org.store.clothstar.member.service.MemberService +import org.store.clothstar.member.util.CreateObject + + +@SpringBootTest +@ActiveProfiles("local") +@ExtendWith(MockKExtension::class) +@Transactional +class MemberServiceApplicationTransactionTest( + @Autowired + private val transactionManager: PlatformTransactionManager, +) { + @MockBean + lateinit var authenticationService: AuthenticationService + + @Autowired + lateinit var em: EntityManager + + @SpyBean + lateinit var accountService: AccountService + + @Autowired + lateinit var memberService: MemberService + + @InjectMockKs + lateinit var memberServiceApplication: MemberServiceApplication + + @Autowired + lateinit var memberRepository: MemberRepository + + @Autowired + lateinit var accountRepository: AccountRepository + + private lateinit var member: Member + private lateinit var account: Account + private var memberId: Long = 0L + private var accountId: Long = 0L + private val transactionTemplate = TransactionTemplate(transactionManager) + + @BeforeEach + fun saveMemberAndAccount() { + member = memberRepository.save(CreateObject.getMember()) + memberId = member.memberId!! + account = accountRepository.save(CreateObject.getAccount(memberId)) + accountId = account.accountId!! + } + + @DisplayName("회원을 삭제하면 member, account updatedAt 필드에 값이 update 되는지 확인한다.") + @Test + fun deleteTransactionTest() { + //when + memberServiceApplication.updateDeleteAt(memberId) + + val account = accountRepository.findByIdOrNull(accountId) + ?: throw NotFoundMemberException(ErrorCode.NOT_FOUND_ACCOUNT) + + val member = memberRepository.findByIdOrNull(memberId) + ?: throw NotFoundMemberException(ErrorCode.NOT_FOUND_MEMBER) + + //then + assertThat(account.deletedAt).isNotNull() + assertThat(member.deletedAt).isNotNull() + } + + @DisplayName("회원을 삭제하다가 에러나면 전부 롤백되는지 확인한다.") + @Test + fun deleteFail_TransactionTest() { + //발생할수 있는 이셉션으로 변경 + doThrow(RuntimeException()).`when`(accountService).updateDeletedAt(memberId, MemberRole.USER) +// assertThrows { memberServiceApplication.updateDeleteAt(memberId) } + try { + println("Before account.deletedAt = ${account.deletedAt}") + println("Before member.deletedAt = ${member.deletedAt}") + + memberServiceApplication.updateDeleteAt(memberId) + } catch (ex: RuntimeException) { + println("ex = ${ex}") + val account = accountRepository.findByIdOrNull(accountId) + ?: throw NotFoundMemberException(ErrorCode.NOT_FOUND_ACCOUNT) + + val member = memberRepository.findByIdOrNull(memberId) + ?: throw NotFoundMemberException(ErrorCode.NOT_FOUND_MEMBER) + + //then + println("After account.deletedAt = ${account.deletedAt}") + println("After member.deletedAt = ${member.deletedAt}") + assertThat(account.deletedAt).isNull() + //assertThat(member.deletedAt).isNull() + } + //영속성컨텍스트 안에 있는 member, account 엔티티를 관리하지 않도록 한다. +// em.detach(member) +// em.detach(account) + + +// // Given: accountService가 memberId를 업데이트할 때 RuntimeException을 던지도록 설정 +// doThrow(java.lang.RuntimeException::class.java).`when`(accountService) +// .updateDeletedAt(memberId, MemberRole.USER) +// +// +// // When: memberServiceApplication.updateDeleteAt을 호출할 때 RuntimeException이 발생할 것으로 기대 +// assertThrows(java.lang.RuntimeException::class.java) { memberServiceApplication.updateDeleteAt(memberId) } +// +// +// // Then: 트랜잭션이 롤백되었는지 확인하기 위해 member와 account를 다시 조회 +// val account = accountRepository.findByIdOrNull(accountId) +// assertNotNull(account, "Account should exist") +// assertNull(account!!.deletedAt, "Account deletedAt should be null") +// +// val member = memberRepository.findByIdOrNull(memberId) +// assertNotNull(member, "Member should exist") +// assertNull(member.getDeletedAt(), "Member deletedAt should be null") + } +} \ No newline at end of file diff --git a/src/test/kotlin/org/store/clothstar/member/authentication/service/AuthenticationServiceKotest.kt b/src/test/kotlin/org/store/clothstar/member/authentication/service/AuthenticationServiceKotest.kt new file mode 100644 index 0000000..f09684f --- /dev/null +++ b/src/test/kotlin/org/store/clothstar/member/authentication/service/AuthenticationServiceKotest.kt @@ -0,0 +1,37 @@ +package org.store.clothstar.member.authentication.service + +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.BehaviorSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import org.store.clothstar.common.config.mail.MailContentBuilder +import org.store.clothstar.common.config.mail.MailService +import org.store.clothstar.common.config.redis.RedisUtil +import org.store.clothstar.common.error.ErrorCode +import org.store.clothstar.common.error.exception.SignupCertifyNumAuthFailedException + +class AuthenticationServiceKotest : BehaviorSpec({ + val mailContentBuilder: MailContentBuilder = mockk() + val mailService: MailService = mockk() + val redisUtil: RedisUtil = mockk() + + val authenticationService = AuthenticationService(mailContentBuilder, mailService, redisUtil) + + Given("회원가입을 할 때") { + val certifyNum = "hi" + val redisCertifyNum = "hello" + every { redisUtil.getData(any()) } returns certifyNum + + When("인증번호가 틀리면") { + val ex = shouldThrow { + authenticationService.verifyEmailCertifyNum("test@naver.com", redisCertifyNum) + } + + Then("인증번호가 잘못 되었습니다. 메시지를 응답한다.") { + ex.message shouldBe ErrorCode.INVALID_AUTH_CERTIFY_NUM.message + ex.message shouldBe "인증번호가 잘못 되었습니다." + } + } + } +}) diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index e07421d..5b74cb3 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -37,6 +37,7 @@ spring: logging.level: org.hibernate.SQL: debug + org.springframework.transaction.interceptor: trace jwt: secret_key: Y2xvdGhzaG9wcGluZ21hbGxjbG90aHN0YXJjbG90aHNob3BwaW5nbWFsbGNsb3Roc3RhcmNsb3Roc2hvcHBpbmdtYWxsY2xvdGhzdGFyY2xvdGhzaG9wcGluZ21hbGxjbG90aHN0YXIK