From 4124416a8ed973906705c578239a98d9505ed96a Mon Sep 17 00:00:00 2001 From: Jonghyun Lee Date: Wed, 25 Dec 2024 15:25:00 +0900 Subject: [PATCH] feat: Add pretotype and basic testing --- build.gradle.kts | 3 ++ .../waffletoy/team1server/DomainException.kt | 17 +++++++++ .../GlobalControllerExceptionHandler.kt | 15 ++++++++ .../PretotypeEmailConflictException.kt | 6 +++ .../pretotype/controller/Pretotype.kt | 20 ++++++++++ .../controller/PretotypeController.kt | 32 ++++++++++++++++ .../pretotype/persistence/PretotypeEntity.kt | 21 ++++++++++ .../persistence/PretotypeRepository.kt | 7 ++++ .../pretotype/service/PretotypeService.kt | 38 +++++++++++++++++++ .../waffletoy/team1server/ApplicationTests.kt | 13 +++++++ 10 files changed, 172 insertions(+) create mode 100644 src/main/kotlin/com/waffletoy/team1server/DomainException.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/GlobalControllerExceptionHandler.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/pretotype/PretotypeEmailConflictException.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/pretotype/controller/Pretotype.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/pretotype/controller/PretotypeController.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeEntity.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeRepository.kt create mode 100644 src/main/kotlin/com/waffletoy/team1server/pretotype/service/PretotypeService.kt diff --git a/build.gradle.kts b/build.gradle.kts index eeaef33..33c39b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,9 +23,12 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter-web") implementation("com.fasterxml.jackson.module:jackson-module-kotlin") implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + developmentOnly("org.springframework.boot:spring-boot-devtools") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("org.jetbrains.kotlin:kotlin-test-junit5") + testImplementation("com.h2database:h2:2.2.220") testRuntimeOnly("org.junit.platform:junit-platform-launcher") implementation("org.hibernate.validator:hibernate-validator:8.0.0.Final") diff --git a/src/main/kotlin/com/waffletoy/team1server/DomainException.kt b/src/main/kotlin/com/waffletoy/team1server/DomainException.kt new file mode 100644 index 0000000..8e906cc --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/DomainException.kt @@ -0,0 +1,17 @@ +package com.waffletoy.team1server + +import org.springframework.http.HttpStatus +import org.springframework.http.HttpStatusCode + +open class DomainException( + // client 와 약속된 Application Error 에 대한 코드 필요 시 Enum 으로 관리하자. + val errorCode: Int, + // HTTP Status Code, 비어있다면 500 이다. + val httpErrorCode: HttpStatusCode = HttpStatus.INTERNAL_SERVER_ERROR, + val msg: String, + cause: Throwable? = null, +) : RuntimeException(msg, cause) { + override fun toString(): String { + return "com.waffletoy.team1server.DomainException(msg='$msg', errorCode=$errorCode, httpErrorCode=$httpErrorCode)" + } +} diff --git a/src/main/kotlin/com/waffletoy/team1server/GlobalControllerExceptionHandler.kt b/src/main/kotlin/com/waffletoy/team1server/GlobalControllerExceptionHandler.kt new file mode 100644 index 0000000..b322478 --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/GlobalControllerExceptionHandler.kt @@ -0,0 +1,15 @@ +package com.waffletoy.team1server + +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler + +@ControllerAdvice +class GlobalControllerExceptionHandler { + @ExceptionHandler(DomainException::class) + fun handle(exception: DomainException): ResponseEntity> { + return ResponseEntity + .status(exception.httpErrorCode) + .body(mapOf("error" to exception.msg, "errorCode" to exception.errorCode)) + } +} diff --git a/src/main/kotlin/com/waffletoy/team1server/pretotype/PretotypeEmailConflictException.kt b/src/main/kotlin/com/waffletoy/team1server/pretotype/PretotypeEmailConflictException.kt new file mode 100644 index 0000000..b34052f --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/pretotype/PretotypeEmailConflictException.kt @@ -0,0 +1,6 @@ +package com.waffletoy.team1server.pretotype + +import com.waffletoy.team1server.DomainException +import org.springframework.http.HttpStatus + +class PretotypeEmailConflictException: DomainException(0, HttpStatus.CONFLICT, "Email already exists") \ No newline at end of file diff --git a/src/main/kotlin/com/waffletoy/team1server/pretotype/controller/Pretotype.kt b/src/main/kotlin/com/waffletoy/team1server/pretotype/controller/Pretotype.kt new file mode 100644 index 0000000..36b05ed --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/pretotype/controller/Pretotype.kt @@ -0,0 +1,20 @@ +package com.waffletoy.team1server.pretotype.controller + +import com.waffletoy.team1server.pretotype.persistence.PretotypeEntity +import java.time.Instant + +class Pretotype ( + val email: String, + val isSubscribed: Boolean, + val createdAt: Instant, +) { + companion object { + fun fromEntity(entity: PretotypeEntity): Pretotype { + return Pretotype( + email = entity.email, + isSubscribed = entity.isSubscribed, + createdAt = entity.createdAt + ) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/waffletoy/team1server/pretotype/controller/PretotypeController.kt b/src/main/kotlin/com/waffletoy/team1server/pretotype/controller/PretotypeController.kt new file mode 100644 index 0000000..7e3479f --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/pretotype/controller/PretotypeController.kt @@ -0,0 +1,32 @@ +package com.waffletoy.team1server.pretotype.controller + +import com.waffletoy.team1server.pretotype.service.PretotypeService +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RestController +class TestResponseController ( + private val pretotypeService: PretotypeService, +){ + @PostMapping("/api/pretotype") + fun createPretotype( + @RequestBody request: PretotypeRequest + ): ResponseEntity { + val pretotype = pretotypeService.createPretotype(request.email, request.isSubscribed) + return ResponseEntity.ok(pretotype) + } + + @GetMapping("/api/pretotype/list") + fun listPretotypes(): ResponseEntity> { + val pretotypes = pretotypeService.listPretotypes() + return ResponseEntity.ok(pretotypes) + } +} + +data class PretotypeRequest( + val email: String, + val isSubscribed: Boolean +) diff --git a/src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeEntity.kt b/src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeEntity.kt new file mode 100644 index 0000000..2faa89f --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeEntity.kt @@ -0,0 +1,21 @@ +package com.waffletoy.team1server.pretotype.persistence + +import jakarta.persistence.* +import java.time.Instant + +@Entity +@Table(name = "pretotypes") +class PretotypeEntity( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + @Column(unique = true) + var email: String = "", + @Column(name = "is_subscribed") + var isSubscribed: Boolean = false, + @Column(name = "created_at") + var createdAt: Instant = Instant.now() +) { + // No-args constructor for Hibernate + constructor() : this(null, "", false, Instant.now()) +} \ No newline at end of file diff --git a/src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeRepository.kt b/src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeRepository.kt new file mode 100644 index 0000000..36c0dd0 --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/pretotype/persistence/PretotypeRepository.kt @@ -0,0 +1,7 @@ +package com.waffletoy.team1server.pretotype.persistence + +import org.springframework.data.jpa.repository.JpaRepository + +interface PretotypeRepository: JpaRepository { + fun findByEmail(email: String): PretotypeEntity? +} \ No newline at end of file diff --git a/src/main/kotlin/com/waffletoy/team1server/pretotype/service/PretotypeService.kt b/src/main/kotlin/com/waffletoy/team1server/pretotype/service/PretotypeService.kt new file mode 100644 index 0000000..4f581f0 --- /dev/null +++ b/src/main/kotlin/com/waffletoy/team1server/pretotype/service/PretotypeService.kt @@ -0,0 +1,38 @@ +package com.waffletoy.team1server.pretotype.service + +import com.waffletoy.team1server.pretotype.PretotypeEmailConflictException +import com.waffletoy.team1server.pretotype.controller.Pretotype +import com.waffletoy.team1server.pretotype.persistence.PretotypeEntity +import com.waffletoy.team1server.pretotype.persistence.PretotypeRepository +import jakarta.transaction.Transactional +import org.springframework.stereotype.Service +import java.time.Instant + +@Service +class PretotypeService ( + private val pretotypeRepository: PretotypeRepository +){ + @Transactional + fun createPretotype( + email: String, + isSubscribed: Boolean, + ): Pretotype { + pretotypeRepository.findByEmail(email) ?.let { + throw PretotypeEmailConflictException() + } ?: run { + val pretotypeEntity = PretotypeEntity( + email = email, + isSubscribed = isSubscribed, + createdAt = Instant.now() + ) + pretotypeRepository.save(pretotypeEntity) + return Pretotype.fromEntity(pretotypeEntity) + } + } + + fun listPretotypes(): List { + return pretotypeRepository.findAll().map { + Pretotype.fromEntity(it) + } + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/waffletoy/team1server/ApplicationTests.kt b/src/test/kotlin/com/waffletoy/team1server/ApplicationTests.kt index c47699e..3623bc9 100644 --- a/src/test/kotlin/com/waffletoy/team1server/ApplicationTests.kt +++ b/src/test/kotlin/com/waffletoy/team1server/ApplicationTests.kt @@ -1,11 +1,24 @@ package com.waffletoy.team1server +import com.waffletoy.team1server.pretotype.service.PretotypeService +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.hasSize import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.context.SpringBootTest @SpringBootTest class ApplicationTests { + @Autowired + private val pretotypeService: PretotypeService? = null + @Test fun contextLoads() { } + + @Test + fun whenPretotypeAdded_thenOneItemInList() { + pretotypeService!!.createPretotype("test@waffle.com", isSubscribed = false) + assertThat(pretotypeService.listPretotypes(), hasSize(1)) + } }