diff --git a/20230107/shopping-mall/.gitignore b/20230107/shopping-mall/.gitignore new file mode 100644 index 0000000..c2065bc --- /dev/null +++ b/20230107/shopping-mall/.gitignore @@ -0,0 +1,37 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/20230107/shopping-mall/README.md b/20230107/shopping-mall/README.md new file mode 100644 index 0000000..05895a0 --- /dev/null +++ b/20230107/shopping-mall/README.md @@ -0,0 +1,42 @@ +# 쇼핑몰 + +## 기능 + +### 주문 + +- [ ] 상품 주문 + +### 상품 + +- [x] 상품 목록 +- [x] 상품 상세 + +### 회원 + +- [ ] 회원 가입 +- [ ] 로그인 + +## 관리자 서버 + +- 상품 등록 +- 상품 수정 +- 상품 삭제 + +## 회원가입 + +### 회원 정보 입력 + +- 이름, 이메일, 주소, 비밀번호, 전화번호 + +### 도메인 + +- 이름, 이메일, 주소, 비밀번호(해시), 전화번호 + +### 테이블 + +- 이름, 이메일, 도시, 상세주소, 비밀번호(해시), 전화번호 + +### 해시함수 + +argon2를 활용해서 해보기. +https://github.com/phxql/argon2-jvm diff --git a/20230107/shopping-mall/build.gradle.kts b/20230107/shopping-mall/build.gradle.kts new file mode 100644 index 0000000..fc1a3f9 --- /dev/null +++ b/20230107/shopping-mall/build.gradle.kts @@ -0,0 +1,50 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.springframework.boot") version "2.7.7" + id("io.spring.dependency-management") version "1.0.15.RELEASE" + kotlin("jvm") version "1.6.21" + kotlin("plugin.spring") version "1.6.21" +} + +group = "com.gringrape" +version = "0.0.1-SNAPSHOT" +java.sourceCompatibility = JavaVersion.VERSION_17 + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-web") + developmentOnly("org.springframework.boot:spring-boot-devtools") + + implementation("org.jetbrains.exposed:exposed-kotlin-datetime:0.41.1") + implementation("de.mkammerer:argon2-jvm:2.11") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + + implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + + runtimeOnly("com.h2database:h2") + implementation("org.jetbrains.exposed:exposed-spring-boot-starter:0.41.1") + + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("io.kotest:kotest-runner-junit5:5.5.4") + testImplementation("io.kotest:kotest-assertions-core:5.5.4") + testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2") + testImplementation("io.mockk:mockk:1.13.3") + testImplementation("com.ninja-squad:springmockk:4.0.0") +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = "17" + } +} + +tasks.withType { + useJUnitPlatform() +} diff --git a/20230107/shopping-mall/gradle/wrapper/gradle-wrapper.jar b/20230107/shopping-mall/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/20230107/shopping-mall/gradle/wrapper/gradle-wrapper.jar differ diff --git a/20230107/shopping-mall/gradle/wrapper/gradle-wrapper.properties b/20230107/shopping-mall/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..070cb70 --- /dev/null +++ b/20230107/shopping-mall/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/20230107/shopping-mall/gradlew b/20230107/shopping-mall/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/20230107/shopping-mall/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/20230107/shopping-mall/gradlew.bat b/20230107/shopping-mall/gradlew.bat new file mode 100644 index 0000000..53a6b23 --- /dev/null +++ b/20230107/shopping-mall/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/20230107/shopping-mall/settings.gradle.kts b/20230107/shopping-mall/settings.gradle.kts new file mode 100644 index 0000000..0fa0011 --- /dev/null +++ b/20230107/shopping-mall/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "shoppingMall" diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/OrderController.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/OrderController.kt new file mode 100644 index 0000000..70097e5 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/OrderController.kt @@ -0,0 +1,32 @@ +package com.gringrape.shoppingMall + +import com.gringrape.shoppingMall.dtos.CreateOrderDto +import com.gringrape.shoppingMall.dtos.OrderDto +import com.gringrape.shoppingMall.dtos.OrderItemDto +import com.gringrape.shoppingMall.service.OrderService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/orders") +class OrderController( + val orderService: OrderService +) { + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun create(@RequestBody source: CreateOrderDto): OrderDto { + val order = orderService.order(source) + return OrderDto( + id = order.id, + userId = order.customer.id, + items = order.items.map { + OrderItemDto( + productId = it.id, + quantity = it.count + ) + } + ) + } + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/ProductController.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/ProductController.kt new file mode 100644 index 0000000..aee2863 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/ProductController.kt @@ -0,0 +1,45 @@ +package com.gringrape.shoppingMall + +import com.gringrape.shoppingMall.domain.Product +import com.gringrape.shoppingMall.dtos.ProductDto +import com.gringrape.shoppingMall.dtos.ProductListDto +import com.gringrape.shoppingMall.exceptions.ProductNotFoundException +import com.gringrape.shoppingMall.service.FindProductService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/products") +class ProductController( + val findProductService: FindProductService +) { + + @GetMapping + fun list(): ProductListDto { + val products = findProductService.list() + return ProductListDto( + products.map { makeDto(it) } + ) + } + + @GetMapping("{id}") + fun detail(@PathVariable id: Long): ProductDto { + val product = findProductService.findOne(id) + return makeDto(product) + } + + private fun makeDto(product: Product): ProductDto { + return ProductDto( + id = product.id, + name = product.name, + price = product.price + ) + } + + @ExceptionHandler + @ResponseStatus(HttpStatus.BAD_REQUEST) + fun handleProductNotFound(exception: ProductNotFoundException): String { + return "Product not found" + } + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/ShoppingMallApplication.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/ShoppingMallApplication.kt new file mode 100644 index 0000000..b769c19 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/ShoppingMallApplication.kt @@ -0,0 +1,11 @@ +package com.gringrape.shoppingMall + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class ShoppingMallApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/UserController.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/UserController.kt new file mode 100644 index 0000000..f0abb54 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/UserController.kt @@ -0,0 +1,27 @@ +package com.gringrape.shoppingMall + +import com.gringrape.shoppingMall.dtos.CreateUserDto +import com.gringrape.shoppingMall.dtos.CreateUserResponseDto +import com.gringrape.shoppingMall.service.CreateUserService +import org.springframework.http.HttpStatus +import org.springframework.web.bind.annotation.* +import javax.validation.Valid + +@RestController +@RequestMapping("users") +class UserController( + val createUserService: CreateUserService +) { + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + fun create(@Valid @RequestBody source: CreateUserDto): CreateUserResponseDto { + val user = createUserService.create(source) + return CreateUserResponseDto( + id = user.id, + name = user.name, + email = user.email + ) + } + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/Order.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/Order.kt new file mode 100644 index 0000000..0a447d0 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/Order.kt @@ -0,0 +1,57 @@ +package com.gringrape.shoppingMall.domain + +import com.gringrape.shoppingMall.exceptions.NotEnoughStockException +import kotlinx.datetime.Clock +import kotlinx.datetime.LocalDateTime +import kotlinx.datetime.TimeZone +import kotlinx.datetime.toLocalDateTime + +// TODO 초기 아이디로 생성되지 않도록 하기 +class Order( + id: Long = INITIAL_ID, + val customer: User, + val items: List, + val date: LocalDateTime, +) { + var id: Long = id + private set + + val customerId: Long + get() = this.customer.id + + fun register(id: Long) { + this.id = id + } + + fun setItemIds(orderItemIds: List) { + orderItemIds.forEach { + + } + } + + companion object { + fun fake(): Order { + return Order( + id = 1L, + customer = User.fake(), + items = listOf(OrderItem.fake()), + date = Clock.System.now().toLocalDateTime(TimeZone.UTC) + ) + } + + fun create(user: User, orderItems: List): Order { + if (!checkEnoughStock(orderItems)) { + throw NotEnoughStockException() + } + return Order( + customer = user, + items = orderItems, + date = Clock.System.now().toLocalDateTime(TimeZone.UTC) + ) + } + + private fun checkEnoughStock(orderItems: List): Boolean { + return orderItems.any { it.checkStock() } + } + } +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/OrderItem.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/OrderItem.kt new file mode 100644 index 0000000..4dfb7c7 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/OrderItem.kt @@ -0,0 +1,30 @@ +package com.gringrape.shoppingMall.domain + +class OrderItem( + id: Long = INITIAL_ID, + orderId: Long = INITIAL_ID, + val product: Product, + val count: Int +) { + + var id = id + val orderId = orderId + + val productId: Long + get() = product.id + + fun checkStock(): Boolean { + return count <= product.stockQuantity + } + + companion object { + fun fake(): OrderItem { + return OrderItem( + id = 1L, + orderId = 1L, + product = Product.fake(), + count = 1 + ) + } + } +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/OrderRepository.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/OrderRepository.kt new file mode 100644 index 0000000..4b851bb --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/OrderRepository.kt @@ -0,0 +1,7 @@ +package com.gringrape.shoppingMall.domain + +interface OrderRepository { + + fun save(order: Order): Order + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/Product.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/Product.kt new file mode 100644 index 0000000..7cbd3be --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/Product.kt @@ -0,0 +1,19 @@ +package com.gringrape.shoppingMall.domain + +class Product( + val id: Long, + val name: String, + val price: Long, + val stockQuantity: Int +) { + companion object { + fun fake(): Product { + return Product( + id = 1L, + name = "dummy", + price = 100_000L, + stockQuantity = 5 + ) + } + } +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/ProductRepository.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/ProductRepository.kt new file mode 100644 index 0000000..4d66d05 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/ProductRepository.kt @@ -0,0 +1,8 @@ +package com.gringrape.shoppingMall.domain + +interface ProductRepository { + + fun findAll(): List + fun findById(id: Long): Product? + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/User.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/User.kt new file mode 100644 index 0000000..7fee75e --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/User.kt @@ -0,0 +1,44 @@ +package com.gringrape.shoppingMall.domain + +import com.gringrape.shoppingMall.dtos.CreateUserDto +import de.mkammerer.argon2.Argon2Factory + +const val INITIAL_ID: Long = 0L + +class User( + id: Long = INITIAL_ID, + val name: String, + val email: String, + val encodedPassword: String +) { + var id: Long = id + private set + + fun setId(id: Long) { + if (this.id != INITIAL_ID) { + return + } + this.id = id + } + + companion object { + private val argon2 = Argon2Factory.create() + + fun fake(): User { + return User( + id = 1L, + name = "Jin", + email = "kingkong@hello.world", + encodedPassword = "" + ) + } + + fun create(source: CreateUserDto): User { + return User( + name = source.name, + email = source.email, + encodedPassword = argon2.hash(10, 65536, 1, source.password.toCharArray()) + ) + } + } +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/UserRepository.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/UserRepository.kt new file mode 100644 index 0000000..6fede16 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/domain/UserRepository.kt @@ -0,0 +1,6 @@ +package com.gringrape.shoppingMall.domain + +interface UserRepository { + fun save(user: User) + fun findById(userId: Long): User? +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateOrderDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateOrderDto.kt new file mode 100644 index 0000000..b5e1a27 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateOrderDto.kt @@ -0,0 +1,6 @@ +package com.gringrape.shoppingMall.dtos + +data class CreateOrderDto( + val userId: Long, + val items: List +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateUserDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateUserDto.kt new file mode 100644 index 0000000..180b7ad --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateUserDto.kt @@ -0,0 +1,11 @@ +package com.gringrape.shoppingMall.dtos + +import javax.validation.constraints.NotBlank + +data class CreateUserDto( + @field:NotBlank val name: String, + @field:NotBlank val address: String, + @field:NotBlank val email: String, + @field:NotBlank val password: String, + @field:NotBlank val phone: String +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateUserResponseDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateUserResponseDto.kt new file mode 100644 index 0000000..069bd82 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/CreateUserResponseDto.kt @@ -0,0 +1,7 @@ +package com.gringrape.shoppingMall.dtos + +data class CreateUserResponseDto( + val id: Long, + val name: String, + val email: String +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/OrderDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/OrderDto.kt new file mode 100644 index 0000000..e513deb --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/OrderDto.kt @@ -0,0 +1,7 @@ +package com.gringrape.shoppingMall.dtos + +data class OrderDto( + val id: Long, + val userId: Long, + val items: List, +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/OrderItemDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/OrderItemDto.kt new file mode 100644 index 0000000..8c84925 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/OrderItemDto.kt @@ -0,0 +1,6 @@ +package com.gringrape.shoppingMall.dtos + +data class OrderItemDto( + val productId: Long, + val quantity: Int +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/ProductDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/ProductDto.kt new file mode 100644 index 0000000..06c2c69 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/ProductDto.kt @@ -0,0 +1,7 @@ +package com.gringrape.shoppingMall.dtos + +data class ProductDto( + val id: Long, + val name: String, + val price: Long +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/ProductListDto.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/ProductListDto.kt new file mode 100644 index 0000000..3046306 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/dtos/ProductListDto.kt @@ -0,0 +1,5 @@ +package com.gringrape.shoppingMall.dtos + +data class ProductListDto( + val products: List +) diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/NotEnoughStockException.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/NotEnoughStockException.kt new file mode 100644 index 0000000..80681a8 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/NotEnoughStockException.kt @@ -0,0 +1,5 @@ +package com.gringrape.shoppingMall.exceptions + +class NotEnoughStockException : Throwable() { + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/ProductNotFoundException.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/ProductNotFoundException.kt new file mode 100644 index 0000000..be17235 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/ProductNotFoundException.kt @@ -0,0 +1,3 @@ +package com.gringrape.shoppingMall.exceptions + +class ProductNotFoundException(id: Long) : Throwable("Product does not exists, ID:$id") diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/UserNotFoundException.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/UserNotFoundException.kt new file mode 100644 index 0000000..44ef7c7 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/exceptions/UserNotFoundException.kt @@ -0,0 +1,3 @@ +package com.gringrape.shoppingMall.exceptions + +class UserNotFoundException(userId: Long) : Throwable("User Not Found, User: $userId") diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedOrderRepository.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedOrderRepository.kt new file mode 100644 index 0000000..7dd0607 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedOrderRepository.kt @@ -0,0 +1,31 @@ +package com.gringrape.shoppingMall.infrastructure + +import com.gringrape.shoppingMall.domain.Order +import com.gringrape.shoppingMall.domain.OrderRepository +import org.jetbrains.exposed.sql.batchInsert +import org.jetbrains.exposed.sql.insert +import org.springframework.stereotype.Repository + +@Repository +class ExposedOrderRepository : OrderRepository { + override fun save(order: Order): Order { + // 1. order 에 아이디 부여하기 + // - 테이블 구조를 생각해야 함. => Orders 테이블 부터 설계 => 결국 구현 이전에 설계가 대부분 결정되어야 한다는 논리. + // 2. order items 에 아이디 부여하기 + // 추가고려 - order + val id = Orders.insert { + it[customerId] = order.customerId + it[date] = order.date + } get Orders.id + + order.register(id) + + OrderItems.batchInsert(order.items) { + this[OrderItems.count] = it.count + this[OrderItems.orderId] = id + this[OrderItems.productId] = it.productId + } + + return order + } +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedProductRepository.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedProductRepository.kt new file mode 100644 index 0000000..d4a3e8a --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedProductRepository.kt @@ -0,0 +1,31 @@ +package com.gringrape.shoppingMall.infrastructure + +import com.gringrape.shoppingMall.domain.Product +import com.gringrape.shoppingMall.domain.ProductRepository +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.select +import org.jetbrains.exposed.sql.selectAll +import org.springframework.stereotype.Repository + +@Repository +class ExposedProductRepository : ProductRepository { + + override fun findAll(): List { + return Products.selectAll().map { toProduct(it) } + } + + override fun findById(id: Long): Product? { + return Products.select { Products.id eq id } + .map { toProduct(it) }.firstOrNull() + } + + fun toProduct(row: ResultRow): Product { + return Product( + id = row[Products.id], + name = row[Products.name], + price = row[Products.price], + stockQuantity = row[Products.stockQuantity] + ) + } + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedUserRepository.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedUserRepository.kt new file mode 100644 index 0000000..bee561d --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/ExposedUserRepository.kt @@ -0,0 +1,32 @@ +package com.gringrape.shoppingMall.infrastructure + +import com.gringrape.shoppingMall.domain.User +import com.gringrape.shoppingMall.domain.UserRepository +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select +import org.springframework.stereotype.Repository + +@Repository +class ExposedUserRepository : UserRepository { + override fun save(user: User) { + val id = Users.insert { + it[email] = user.email + it[name] = user.name + it[encodedPassword] = user.encodedPassword + } get Users.id + + user.setId(id) + } + + override fun findById(userId: Long): User? { + return Users.select { Users.id eq userId } + .map { + User( + id = it[Users.id], + name = it[Users.name], + email = it[Users.email], + encodedPassword = it[Users.encodedPassword], + ) + }.firstOrNull() + } +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/OrderItems.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/OrderItems.kt new file mode 100644 index 0000000..6b489a2 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/OrderItems.kt @@ -0,0 +1,12 @@ +package com.gringrape.shoppingMall.infrastructure + +import org.jetbrains.exposed.sql.Table + +object OrderItems : Table() { + val id = long("id").index().autoIncrement() + val orderId = long("order_id").index() + val productId = long("product_id").index() + val count = integer("count") + + override val primaryKey = PrimaryKey(id) +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Orders.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Orders.kt new file mode 100644 index 0000000..cc3adc6 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Orders.kt @@ -0,0 +1,12 @@ +package com.gringrape.shoppingMall.infrastructure + +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.kotlin.datetime.datetime + +object Orders : Table() { + val id = long("id").autoIncrement().index() + val customerId = long("customer_id").index() + val date = datetime("created_at").index() + + override val primaryKey = PrimaryKey(id) +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Products.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Products.kt new file mode 100644 index 0000000..e742048 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Products.kt @@ -0,0 +1,12 @@ +package com.gringrape.shoppingMall.infrastructure + +import org.jetbrains.exposed.sql.Table + +object Products : Table() { + val id = long("id").index().autoIncrement() + val name = varchar("name", 30).index() + val price = long("price") + val stockQuantity = integer("stock_quantity") + + override val primaryKey = PrimaryKey(id) +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Users.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Users.kt new file mode 100644 index 0000000..205c733 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/infrastructure/Users.kt @@ -0,0 +1,14 @@ +package com.gringrape.shoppingMall.infrastructure + +import org.jetbrains.exposed.sql.Table + +object Users : Table() { + val id = long("id").index().autoIncrement() + val name = varchar("name", 20).index() + val email = varchar("email", 100).index() + val encodedPassword = text("encoded_password") +// val address = varchar("address", 100) +// val phone = varchar("phone", 20).index() + + override val primaryKey = PrimaryKey(id) +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/CreateUserService.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/CreateUserService.kt new file mode 100644 index 0000000..5ce34c8 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/CreateUserService.kt @@ -0,0 +1,23 @@ +package com.gringrape.shoppingMall.service + +import com.gringrape.shoppingMall.domain.User +import com.gringrape.shoppingMall.domain.UserRepository +import com.gringrape.shoppingMall.dtos.CreateUserDto +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class CreateUserService( + val userRepository: UserRepository +) { + + fun create(source: CreateUserDto): User { + val user = User.create(source) + + userRepository.save(user) + + return user + } + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/FindProductService.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/FindProductService.kt new file mode 100644 index 0000000..891f3a0 --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/FindProductService.kt @@ -0,0 +1,24 @@ +package com.gringrape.shoppingMall.service + +import com.gringrape.shoppingMall.domain.Product +import com.gringrape.shoppingMall.domain.ProductRepository +import com.gringrape.shoppingMall.exceptions.ProductNotFoundException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class FindProductService( + val productRepository: ProductRepository +) { + + fun list(): List { + return productRepository.findAll() + } + + fun findOne(id: Long): Product { + return productRepository.findById(id) + ?: throw ProductNotFoundException(id) + } + +} diff --git a/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/OrderService.kt b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/OrderService.kt new file mode 100644 index 0000000..0a7ee0d --- /dev/null +++ b/20230107/shopping-mall/src/main/kotlin/com/gringrape/shoppingMall/service/OrderService.kt @@ -0,0 +1,36 @@ +package com.gringrape.shoppingMall.service + +import com.gringrape.shoppingMall.domain.* +import com.gringrape.shoppingMall.dtos.CreateOrderDto +import com.gringrape.shoppingMall.exceptions.ProductNotFoundException +import com.gringrape.shoppingMall.exceptions.UserNotFoundException +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional +class OrderService( + val orderRepository: OrderRepository, + val userRepository: UserRepository, + val productRepository: ProductRepository +) { + + fun order(source: CreateOrderDto): Order { + val user = userRepository.findById(source.userId) + ?: throw UserNotFoundException(source.userId) + + val orderItems = source.items.map { + val product: Product = productRepository.findById(it.productId) + ?: throw ProductNotFoundException(it.productId) + OrderItem( + product = product, + count = it.quantity + ) + } + + val order = Order.create(user, orderItems) + + return orderRepository.save(order) + } + +} diff --git a/20230107/shopping-mall/src/main/resources/application.yml b/20230107/shopping-mall/src/main/resources/application.yml new file mode 100644 index 0000000..953a33e --- /dev/null +++ b/20230107/shopping-mall/src/main/resources/application.yml @@ -0,0 +1,10 @@ +spring: + datasource: + url: jdbc:h2:mem:testdb + driverClassName: org.h2.Driver + username: sa + password: + generate-ddl: true + exposed: + generate-ddl: true + diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/OrderControllerTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/OrderControllerTest.kt new file mode 100644 index 0000000..f048cad --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/OrderControllerTest.kt @@ -0,0 +1,48 @@ +package com.gringrape.shoppingMall + +import com.gringrape.shoppingMall.domain.Order +import com.gringrape.shoppingMall.service.OrderService +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import org.hamcrest.core.StringContains.containsString +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(controllers = [OrderController::class]) +class OrderControllerTest : DescribeSpec() { + + @Autowired + lateinit var mockMvc: MockMvc + + @MockkBean + lateinit var orderService: OrderService + + init { + describe("POST /orders") { + val userId = 1L + val productId = 1L + val quantity = 3 + + beforeEach { + every { orderService.order(any()) } returns Order.fake() + } + + it("responds with created order") { + mockMvc.perform( + post("/orders") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"userId\":${userId},\"items\":[{\"productId\":$productId,\"quantity\":$quantity}]}") + ) + .andExpect(status().isCreated) + .andExpect(content().string(containsString("\"userId\":${userId}"))) + } + } + } + +} diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ProductControllerTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ProductControllerTest.kt new file mode 100644 index 0000000..4eb1e1c --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ProductControllerTest.kt @@ -0,0 +1,64 @@ +package com.gringrape.shoppingMall + +import com.gringrape.shoppingMall.domain.Product +import com.gringrape.shoppingMall.exceptions.ProductNotFoundException +import com.gringrape.shoppingMall.service.FindProductService +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import org.hamcrest.core.StringContains.containsString +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(controllers = [ProductController::class]) +class ProductControllerTest : DescribeSpec() { + @Autowired + lateinit var mockMvc: MockMvc + + @MockkBean + lateinit var findProductService: FindProductService + + init { + describe("GET /products") { + beforeEach { + every { findProductService.list() } returns listOf(Product.fake()) + } + + it("responds with list of products") { + mockMvc.perform(get("/products")) + .andExpect(status().isOk) + .andExpect(content().string(containsString("[{\"id\""))) + } + } + + describe("GET /products/{id}") { + val id = 1L + + context("when product exists") { + beforeEach { + every { findProductService.findOne(id) } returns Product.fake() + } + + it("responds with ok status") { + mockMvc.perform(get("/products/$id")) + .andExpect(status().isOk) + } + } + + context("when product not found") { + beforeEach { + every { findProductService.findOne(id) } throws ProductNotFoundException(id) + } + + it("responds with BAD REQUEST status") { + mockMvc.perform(get("/products/$id")) + .andExpect(status().isBadRequest) + } + } + } + } +} diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ProjectConfig.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ProjectConfig.kt new file mode 100644 index 0000000..cda4627 --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ProjectConfig.kt @@ -0,0 +1,8 @@ +package com.gringrape.shoppingMall + +import io.kotest.core.config.AbstractProjectConfig +import io.kotest.extensions.spring.SpringExtension + +class ProjectConfig : AbstractProjectConfig() { + override fun extensions() = listOf(SpringExtension) +} diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ShoppingMallApplicationTests.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ShoppingMallApplicationTests.kt new file mode 100644 index 0000000..f30f9de --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/ShoppingMallApplicationTests.kt @@ -0,0 +1,13 @@ +package com.gringrape.shoppingMall + +import org.junit.jupiter.api.Test +import org.springframework.boot.test.context.SpringBootTest + +@SpringBootTest +class ShoppingMallApplicationTests { + + @Test + fun contextLoads() { + } + +} diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/UserControllerTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/UserControllerTest.kt new file mode 100644 index 0000000..2427041 --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/UserControllerTest.kt @@ -0,0 +1,59 @@ +package com.gringrape.shoppingMall + +import com.gringrape.shoppingMall.domain.User +import com.gringrape.shoppingMall.service.CreateUserService +import com.ninjasquad.springmockk.MockkBean +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import org.hamcrest.core.StringContains.containsString +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.MockMvc +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content +import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status + +@WebMvcTest(controllers = [UserController::class]) +class UserControllerTest : DescribeSpec() { + @Autowired + lateinit var mockMvc: MockMvc + + @MockkBean + lateinit var createUserService: CreateUserService + + init { + describe("POST /users") { + val name = "Jin" + val password = "PASS" + val address = "서울시 강남구 삼성동" + val email = "kingkong@kingkong.com" + val phone = "010-1234-1234" + + beforeEach { + every { createUserService.create(any()) } returns User.fake() + } + + it("responds with created user") { + mockMvc.perform( + post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"$name\",\"password\":\"$password\",\"address\":\"$address\",\"email\":\"$email\",\"phone\":\"$phone\"}") + ) + .andExpect(status().isCreated) + .andExpect(content().string(containsString("\"name\":"))) + } + + context("with blank information") { + it("responds with bad request status") { + mockMvc.perform( + post("/users") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"name\":\"\",\"password\":\"$password\",\"address\":\"$address\",\"email\":\"$email\",\"phone\":\"$phone\"}") + ) + .andExpect(status().isBadRequest) + } + } + } + } +} diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/domain/OrderItemTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/domain/OrderItemTest.kt new file mode 100644 index 0000000..d616ab4 --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/domain/OrderItemTest.kt @@ -0,0 +1,19 @@ +package com.gringrape.shoppingMall.domain + +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe + +class OrderItemTest : DescribeSpec({ + describe("checkStock") { + it("returns whether stock quantity is enough") { + val stockQuantity = 100 + val count = 5 + + val orderItem = OrderItem( + count = count, + product = Product(stockQuantity = stockQuantity, id = 1L, name = "ANY", price = 100) + ) + orderItem.checkStock() shouldBe true + } + } +}) diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/CreateUserServiceTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/CreateUserServiceTest.kt new file mode 100644 index 0000000..e8e8acd --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/CreateUserServiceTest.kt @@ -0,0 +1,46 @@ +package com.gringrape.shoppingMall.service + +import com.gringrape.shoppingMall.domain.User +import com.gringrape.shoppingMall.domain.UserRepository +import com.gringrape.shoppingMall.dtos.CreateUserDto +import io.kotest.core.spec.style.DescribeSpec +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkObject +import io.mockk.verify + +class CreateUserServiceTest : DescribeSpec({ + + lateinit var createUserService: CreateUserService + lateinit var userRepository: UserRepository + + beforeEach { + userRepository = mockk() + createUserService = CreateUserService(userRepository) + } + + describe("create") { + val source = CreateUserDto( + name = "Jin", + address = "서울시 강남구 삼성동", + password = "PASS", + email = "kingkong@kingkong.com", + phone = "010-1234-1234" + ) + + val user = User.fake() + + beforeEach { + mockkObject(User.Companion) + every { User.create(any()) } returns user + + every { userRepository.save(any()) } returns Unit + } + + it("creates user") { + createUserService.create(source) + verify { userRepository.save(user) } + } + } + +}) diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/FindProductServiceTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/FindProductServiceTest.kt new file mode 100644 index 0000000..74b8b09 --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/FindProductServiceTest.kt @@ -0,0 +1,61 @@ +package com.gringrape.shoppingMall.service + +import com.gringrape.shoppingMall.domain.Product +import com.gringrape.shoppingMall.domain.ProductRepository +import com.gringrape.shoppingMall.exceptions.ProductNotFoundException +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk + +class FindProductServiceTest : DescribeSpec({ + + lateinit var findProductService: FindProductService + lateinit var productRepository: ProductRepository + + beforeEach { + productRepository = mockk() + findProductService = FindProductService(productRepository) + } + + describe("list") { + val products = listOf() + + beforeEach { + every { productRepository.findAll() } returns products + } + + it("returns products") { + findProductService.list() shouldBe products + } + } + + describe("findOne") { + val id = 1L + val product = Product.fake() + + context("when product found") { + beforeEach { + every { productRepository.findById(id) } returns product + } + + it("returns product") { + findProductService.findOne(id) shouldBe product + } + } + + context("when product not found") { + beforeEach { + every { productRepository.findById(id) } returns null + } + + it("should throw exception") { + shouldThrow { + findProductService.findOne(id) + } + } + } + } + +}) diff --git a/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/OrderServiceTest.kt b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/OrderServiceTest.kt new file mode 100644 index 0000000..026a30b --- /dev/null +++ b/20230107/shopping-mall/src/test/kotlin/com/gringrape/shoppingMall/service/OrderServiceTest.kt @@ -0,0 +1,81 @@ +package com.gringrape.shoppingMall.service + +import com.gringrape.shoppingMall.domain.* +import com.gringrape.shoppingMall.dtos.CreateOrderDto +import com.gringrape.shoppingMall.dtos.OrderItemDto +import com.gringrape.shoppingMall.exceptions.NotEnoughStockException +import io.kotest.assertions.throwables.shouldThrow +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.matchers.shouldBe +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify + + +class OrderServiceTest : DescribeSpec({ + + lateinit var orderRepository: OrderRepository + lateinit var orderService: OrderService + lateinit var userRepository: UserRepository + lateinit var productRepository: ProductRepository + + beforeEach { + userRepository = mockk() + productRepository = mockk() + orderRepository = mockk() + + orderService = OrderService( + orderRepository = orderRepository, + userRepository = userRepository, + productRepository = productRepository + ) + } + + describe("create") { + val productId = 2L + val quantity = 3 + + val source = CreateOrderDto( + userId = 1L, + items = listOf( + OrderItemDto( + productId = productId, + quantity = quantity + ) + ) + ) + + val user = mockk() + val product = Product.fake() + val order = mockk() + + beforeEach { + every { userRepository.findById(source.userId) } returns user + every { productRepository.findById(productId) } returns product + every { orderRepository.save(any()) } returns order + } + + it("creates order") { + orderService.order(source) shouldBe order + verify { userRepository.findById(source.userId) } + verify { productRepository.findById(source.items[0].productId) } + } + + context("with not enough stock") { + val item = source.items[0] + + beforeEach { + every { + productRepository.findById(item.productId) + } returns Product(stockQuantity = 0, id = 100L, name = "ANY", price = 1000) + } + + it("throws exception") { + shouldThrow { + orderService.order(source) + } + } + } + } + +})