From 8b6b4b3783374c0d73a97496dab29da155b34c62 Mon Sep 17 00:00:00 2001 From: "siwakon.dev" Date: Sun, 18 Feb 2024 20:54:13 +0700 Subject: [PATCH 1/2] create lottery service --- .gitignore | 1 + posttest/.gitignore | 1 + posttest/Dockerfile | 12 ++ .../KBTG_ASSESSMENT.postman_collection.json | 172 ++++++++++++++++++ posttest/README.md | 3 + posttest/build.gradle | 38 ++-- posttest/docker-compose.yml | 39 ++++ .../bootcamp/posttest/IndexController.java | 15 ++ .../posttest/PosttestApplication.java | 7 +- .../admin/controller/AdminController.java | 52 ++++++ .../posttest/config/SecurityConfig.java | 69 +++++++ .../bootcamp/posttest/entity/Lottery.java | 34 ++++ .../kbtg/bootcamp/posttest/entity/User.java | 38 ++++ .../bootcamp/posttest/entity/UserTicket.java | 35 ++++ .../posttest/exception/ConflictException.java | 8 + .../posttest/exception/NotFoundException.java | 8 + .../exception/ValidationExceptionHandler.java | 29 +++ .../posttest/helper/HttpResponse.java | 19 ++ .../posttest/helper/ResponseHandler.java | 19 ++ .../lottery/controller/LotteryController.java | 39 ++++ .../lottery/repository/LotteryRepository.java | 14 ++ .../lottery/request/CreateLotteryRequest.java | 22 +++ .../response/CreateLotteryResponse.java | 15 ++ .../response/GetAllTicketResponse.java | 18 ++ .../lottery/response/TicketResponse.java | 16 ++ .../lottery/service/LotteryService.java | 16 ++ .../service/impl/LotteryServiceImpl.java | 52 ++++++ .../user/controller/UserController.java | 89 +++++++++ .../user/repository/UserRepository.java | 10 + .../user/repository/UserTicketRepository.java | 20 ++ .../user/response/BuyTicketResponse.java | 15 ++ .../user/response/RefundTicketResponse.java | 15 ++ .../posttest/user/service/UserService.java | 16 ++ .../user/service/impl/UserServiceImpl.java | 86 +++++++++ .../src/main/resources/application.properties | 6 - posttest/src/main/resources/application.yml | 76 ++++++++ .../db/changelog/db.changelog-master.yaml | 3 + .../resources/db/changelog/v1/data/user.csv | 3 + .../db/changelog/v1/db.changelog-1.0.0.yaml | 101 ++++++++++ .../db/changelog/v1/db.loaddata-1.0.0.yaml | 41 +++++ 40 files changed, 1247 insertions(+), 25 deletions(-) create mode 100644 .gitignore create mode 100644 posttest/Dockerfile create mode 100644 posttest/KBTG_ASSESSMENT.postman_collection.json create mode 100644 posttest/README.md create mode 100644 posttest/docker-compose.yml create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/IndexController.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/admin/controller/AdminController.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/config/SecurityConfig.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/User.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/UserTicket.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ConflictException.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/NotFoundException.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ValidationExceptionHandler.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/HttpResponse.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/ResponseHandler.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/controller/LotteryController.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/repository/LotteryRepository.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/request/CreateLotteryRequest.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/CreateLotteryResponse.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/GetAllTicketResponse.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/TicketResponse.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/controller/UserController.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserRepository.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserTicketRepository.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/BuyTicketResponse.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/RefundTicketResponse.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/UserService.java create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java delete mode 100644 posttest/src/main/resources/application.properties create mode 100644 posttest/src/main/resources/application.yml create mode 100644 posttest/src/main/resources/db/changelog/db.changelog-master.yaml create mode 100644 posttest/src/main/resources/db/changelog/v1/data/user.csv create mode 100644 posttest/src/main/resources/db/changelog/v1/db.changelog-1.0.0.yaml create mode 100644 posttest/src/main/resources/db/changelog/v1/db.loaddata-1.0.0.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..723ef36f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea \ No newline at end of file diff --git a/posttest/.gitignore b/posttest/.gitignore index c2065bc2..55295d9c 100644 --- a/posttest/.gitignore +++ b/posttest/.gitignore @@ -35,3 +35,4 @@ out/ ### VS Code ### .vscode/ +.idea \ No newline at end of file diff --git a/posttest/Dockerfile b/posttest/Dockerfile new file mode 100644 index 00000000..db478d49 --- /dev/null +++ b/posttest/Dockerfile @@ -0,0 +1,12 @@ +# stage-1 build artifact +FROM amazoncorretto:21.0.2-alpine3.19 as builder +WORKDIR /app +ADD . . +RUN ["./gradlew","bootJar"] + +# stage-2 running image +FROM gcr.io/distroless/java21-debian12:latest +WORKDIR /app +COPY --from=builder /app/build/libs/posttest-0.0.1-SNAPSHOT.jar posttest-0.0.1-SNAPSHOT.jar +EXPOSE 8888 +ENTRYPOINT ["java", "-jar", "posttest-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/posttest/KBTG_ASSESSMENT.postman_collection.json b/posttest/KBTG_ASSESSMENT.postman_collection.json new file mode 100644 index 00000000..c952cd22 --- /dev/null +++ b/posttest/KBTG_ASSESSMENT.postman_collection.json @@ -0,0 +1,172 @@ +{ + "info": { + "_postman_id": "bc7f7b7b-726d-481e-92ee-6d0477e5c721", + "name": "KBTG_ASSESSMENT", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", + "_exporter_id": "32476344" + }, + "item": [ + { + "name": "admin", + "item": [ + { + "name": "create lottery", + "request": { + "auth": { + "type": "basic", + "basic": [ + { + "key": "password", + "value": "password", + "type": "string" + }, + { + "key": "username", + "value": "admin", + "type": "string" + } + ] + }, + "method": "POST", + "header": [], + "body": { + "mode": "raw", + "raw": "{\n \"ticket\": \"111111\",\n \"price\": 80,\n \"amount\": 1\n}", + "options": { + "raw": { + "language": "json" + } + } + }, + "url": { + "raw": "http://localhost:8888/admin/lotteries", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "admin", + "lotteries" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "lottery", + "item": [ + { + "name": "get all tickets", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8888/lotteries", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "lotteries" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "user", + "item": [ + { + "name": "buy tickets", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "http://localhost:8888/users/2/lotteries/222222", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "users", + "2", + "lotteries", + "222222" + ] + } + }, + "response": [] + }, + { + "name": "my tickets", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8888/users/1/lotteries", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "users", + "1", + "lotteries" + ] + } + }, + "response": [] + }, + { + "name": "delete user ticket", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "http://localhost:8888/users/1/lotteries/345345", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "users", + "1", + "lotteries", + "345345" + ] + } + }, + "response": [] + } + ] + }, + { + "name": "New Request", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "http://localhost:8888/", + "protocol": "http", + "host": [ + "localhost" + ], + "port": "8888", + "path": [ + "" + ] + } + }, + "response": [] + } + ] +} \ No newline at end of file diff --git a/posttest/README.md b/posttest/README.md new file mode 100644 index 00000000..54a9dcef --- /dev/null +++ b/posttest/README.md @@ -0,0 +1,3 @@ +# Postman Collection + +[KBTG_ASSESSMENT.postman_collection.json](KBTG_ASSESSMENT.postman_collection.json) \ No newline at end of file diff --git a/posttest/build.gradle b/posttest/build.gradle index 6d9dbbe2..c7494d4e 100644 --- a/posttest/build.gradle +++ b/posttest/build.gradle @@ -1,35 +1,43 @@ plugins { - id 'java' - id 'org.springframework.boot' version '3.2.2' - id 'io.spring.dependency-management' version '1.1.4' + id 'java' + id 'org.springframework.boot' version '3.2.2' + id 'io.spring.dependency-management' version '1.1.4' } group = 'com.kbtg.bootcamp' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '17' + sourceCompatibility = '21' } configurations { - compileOnly { - extendsFrom annotationProcessor - } + compileOnly { + extendsFrom annotationProcessor + } } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.postgresql:postgresql' - compileOnly 'org.projectlombok:lombok' - annotationProcessor 'org.projectlombok:lombok' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.liquibase:liquibase-core' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:2.2.0' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + implementation 'io.jsonwebtoken:jjwt-api:0.12.4' + runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.4' + runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.4' + runtimeOnly 'org.postgresql:postgresql' + compileOnly 'org.projectlombok:lombok:1.18.30' + annotationProcessor 'org.projectlombok:lombok:1.18.30' + testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { - useJUnitPlatform() + useJUnitPlatform() } diff --git a/posttest/docker-compose.yml b/posttest/docker-compose.yml new file mode 100644 index 00000000..c4993d78 --- /dev/null +++ b/posttest/docker-compose.yml @@ -0,0 +1,39 @@ +version: '3.8' + +services: + db: + image: postgres + ports: + - "25061:5432" + restart: always + environment: + POSTGRES_DB: assessment + POSTGRES_USER: root + POSTGRES_PASSWORD: 12345678 + volumes: + - postgres-data:/var/lib/postgresql/data + networks: + - lotteryservice-network + lottery-service: + build: + context: . + dockerfile: Dockerfile + restart: always + ports: + - "8888:8888" + environment: + SPRING_PROFILES_ACTIVE: "local" + SPRING_DATASOURCE_URL: jdbc:postgresql://db:5432/assessment?searchpath=public&createDatabaseIfNotExist=true&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=Asia/Bangkok&prepareThreshold=0 + SPRING_DATASOURCE_USERNAME: root + SPRING_DATASOURCE_PASSWORD: 12345678 + depends_on: + - db + networks: + - lotteryservice-network + +volumes: + postgres-data: + +networks: + lotteryservice-network: + driver: bridge \ No newline at end of file diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/IndexController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/IndexController.java new file mode 100644 index 00000000..4e7ca1f7 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/IndexController.java @@ -0,0 +1,15 @@ +package com.kbtg.bootcamp.posttest; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/") +public class IndexController { + + @GetMapping + public String index() { + return "Hello World!"; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/PosttestApplication.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/PosttestApplication.java index 630c3b8d..c0e904f1 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/PosttestApplication.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/PosttestApplication.java @@ -6,8 +6,7 @@ @SpringBootApplication public class PosttestApplication { - public static void main(String[] args) { - SpringApplication.run(PosttestApplication.class, args); - } - + public static void main(String[] args) { + SpringApplication.run(PosttestApplication.class, args); + } } diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/admin/controller/AdminController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/admin/controller/AdminController.java new file mode 100644 index 00000000..f3d737b9 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/admin/controller/AdminController.java @@ -0,0 +1,52 @@ +package com.kbtg.bootcamp.posttest.admin.controller; + +import com.kbtg.bootcamp.posttest.helper.ResponseHandler; +import com.kbtg.bootcamp.posttest.lottery.request.CreateLotteryRequest; +import com.kbtg.bootcamp.posttest.lottery.response.CreateLotteryResponse; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/admin") +public class AdminController { + + private final LotteryService lotteryService; + + @Autowired + public AdminController(LotteryService lotteryService) { + this.lotteryService = lotteryService; + } + + @PreAuthorize("hasRole('ROLE_ADMIN')") + @PostMapping("/lotteries") + public ResponseEntity createLottery( + @Valid @RequestBody CreateLotteryRequest createLotteryRequest + ) { + try { + if (!lotteryService.checkExistTicket(createLotteryRequest.getTicket())) { + String ticket = lotteryService.createLottery(createLotteryRequest); + return ResponseHandler.generateResponse( + "create lottery Success", + HttpStatus.CREATED, + new CreateLotteryResponse(ticket)); + } else { + return ResponseHandler.generateResponse( + "ticket" + createLotteryRequest.getTicket() + " already exists", + HttpStatus.CONFLICT, + null); + } + } catch (Exception e) { + return ResponseHandler.generateResponse(e.getLocalizedMessage(), + HttpStatus.INTERNAL_SERVER_ERROR, + null); + } + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/config/SecurityConfig.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/config/SecurityConfig.java new file mode 100644 index 00000000..24c192fa --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/config/SecurityConfig.java @@ -0,0 +1,69 @@ +package com.kbtg.bootcamp.posttest.config; + +import static org.springframework.security.config.Customizer.withDefaults; + +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.cors(cors -> cors.configurationSource(corsConfigurationSource())); + http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.disable()); + http.formLogin(withDefaults()); + http.httpBasic(withDefaults()); + http.authorizeHttpRequests(authorize -> + authorize + .requestMatchers(HttpMethod.OPTIONS).permitAll() + .requestMatchers("/").permitAll() + .requestMatchers("/users/**").permitAll() + .requestMatchers("/lotteries/**").permitAll() + .anyRequest().authenticated() + ); + return http.build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User.builder() + .username("admin") + .password(bCryptPasswordEncoder().encode("password")) + .roles("ADMIN") + .build(); + return new InMemoryUserDetailsManager(user); + } + + private CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(List.of("*")); + configuration.setAllowedMethods(List.of("*")); + configuration.setAllowedHeaders(List.of("*")); + configuration.setExposedHeaders(List.of("*")); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/*/**", configuration); + source.registerCorsConfiguration("/api/**", configuration); + + return source; + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java new file mode 100644 index 00000000..c253e34b --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java @@ -0,0 +1,34 @@ +package com.kbtg.bootcamp.posttest.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "lottery") +@Getter +@Setter +@NoArgsConstructor +public class Lottery { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "ticket", unique = true, nullable = false, length = 6) + private String ticket; + + @Column(name = "price", nullable = false) + private Integer price; + + @Column(name = "amount", nullable = false) + private Integer amount; + +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/User.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/User.java new file mode 100644 index 00000000..ffc5668a --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/User.java @@ -0,0 +1,38 @@ +package com.kbtg.bootcamp.posttest.entity; + +import com.fasterxml.jackson.annotation.JsonFormat; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import java.util.Date; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "user") +@Getter +@Setter +@NoArgsConstructor +public class User { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column(name = "username") + private String username; + + @Column(name = "password") + private String password; + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private Date createdDate = new Date(); + + @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX") + private Date modifiedDate = new Date(); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/UserTicket.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/UserTicket.java new file mode 100644 index 00000000..f24eec9c --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/UserTicket.java @@ -0,0 +1,35 @@ +package com.kbtg.bootcamp.posttest.entity; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Table(name = "user_ticket") +@Getter +@Setter +@NoArgsConstructor +public class UserTicket { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id") + private User user; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "lottery_id") + private Lottery lottery; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ConflictException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ConflictException.java new file mode 100644 index 00000000..2712a320 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ConflictException.java @@ -0,0 +1,8 @@ +package com.kbtg.bootcamp.posttest.exception; + +public class ConflictException extends Exception { + + public ConflictException(String message) { + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/NotFoundException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/NotFoundException.java new file mode 100644 index 00000000..fa60574b --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/NotFoundException.java @@ -0,0 +1,8 @@ +package com.kbtg.bootcamp.posttest.exception; + +public class NotFoundException extends Exception { + + public NotFoundException(String message) { + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ValidationExceptionHandler.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ValidationExceptionHandler.java new file mode 100644 index 00000000..61386ca4 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/ValidationExceptionHandler.java @@ -0,0 +1,29 @@ +package com.kbtg.bootcamp.posttest.exception; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; + +@ControllerAdvice +public class ValidationExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity notValid(MethodArgumentNotValidException ex, + HttpServletRequest request) { + List errors = new ArrayList<>(); + + ex.getAllErrors().forEach(err -> errors.add(err.getDefaultMessage())); + + Map> result = new HashMap<>(); + result.put("errors", errors); + + return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/HttpResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/HttpResponse.java new file mode 100644 index 00000000..fbee3b63 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/HttpResponse.java @@ -0,0 +1,19 @@ +package com.kbtg.bootcamp.posttest.helper; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +public class HttpResponse { + + private Boolean status; + private String message; + private Object data = null; + + public HttpResponse() { + + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/ResponseHandler.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/ResponseHandler.java new file mode 100644 index 00000000..9cc37b5d --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/helper/ResponseHandler.java @@ -0,0 +1,19 @@ +package com.kbtg.bootcamp.posttest.helper; + +import java.util.HashMap; +import java.util.Map; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; + +public class ResponseHandler { + + public static ResponseEntity generateResponse(String message, HttpStatus status, + Object responseObj) { + Map map = new HashMap(); + map.put("message", message); + map.put("status", status.value()); + map.put("data", responseObj); + + return new ResponseEntity(map, status); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/controller/LotteryController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/controller/LotteryController.java new file mode 100644 index 00000000..4089ced0 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/controller/LotteryController.java @@ -0,0 +1,39 @@ +package com.kbtg.bootcamp.posttest.lottery.controller; + +import com.kbtg.bootcamp.posttest.helper.ResponseHandler; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/lotteries") +public class LotteryController { + + private final LotteryService lotteryService; + + @Autowired + public LotteryController(LotteryService lotteryService) { + this.lotteryService = lotteryService; + } + + @GetMapping + public ResponseEntity getAllTickets() { + try { + return ResponseHandler.generateResponse( + "get all tickets success", + HttpStatus.OK, + lotteryService.getAllTickets() + ); + } catch (Exception e) { + return ResponseHandler.generateResponse( + e.getMessage(), + HttpStatus.INTERNAL_SERVER_ERROR, + null + ); + } + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/repository/LotteryRepository.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/repository/LotteryRepository.java new file mode 100644 index 00000000..a14c6803 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/repository/LotteryRepository.java @@ -0,0 +1,14 @@ +package com.kbtg.bootcamp.posttest.lottery.repository; + +import com.kbtg.bootcamp.posttest.entity.Lottery; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface LotteryRepository extends JpaRepository { + + Optional findByTicket(String ticket); + + Boolean existsByTicket(String ticket); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/request/CreateLotteryRequest.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/request/CreateLotteryRequest.java new file mode 100644 index 00000000..a168ea69 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/request/CreateLotteryRequest.java @@ -0,0 +1,22 @@ +package com.kbtg.bootcamp.posttest.lottery.request; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class CreateLotteryRequest { + + @NotEmpty(message = "ticket is required") + @Size(min = 6, max = 6, message = "ticket must be 6 digits") + private String ticket; + + @NotNull(message = "price is required") + private Integer price; + + @NotNull(message = "amount is required") + private Integer amount; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/CreateLotteryResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/CreateLotteryResponse.java new file mode 100644 index 00000000..86118edb --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/CreateLotteryResponse.java @@ -0,0 +1,15 @@ +package com.kbtg.bootcamp.posttest.lottery.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class CreateLotteryResponse { + + private String ticket; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/GetAllTicketResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/GetAllTicketResponse.java new file mode 100644 index 00000000..b8efc402 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/GetAllTicketResponse.java @@ -0,0 +1,18 @@ +package com.kbtg.bootcamp.posttest.lottery.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class GetAllTicketResponse { + + private List tickets; + private int count; + private int cost; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/TicketResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/TicketResponse.java new file mode 100644 index 00000000..249ddc78 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/response/TicketResponse.java @@ -0,0 +1,16 @@ +package com.kbtg.bootcamp.posttest.lottery.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@AllArgsConstructor +@NoArgsConstructor +public class TicketResponse { + + private List tickets; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java new file mode 100644 index 00000000..993f88f0 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java @@ -0,0 +1,16 @@ +package com.kbtg.bootcamp.posttest.lottery.service; + +import com.kbtg.bootcamp.posttest.exception.NotFoundException; +import com.kbtg.bootcamp.posttest.lottery.request.CreateLotteryRequest; +import com.kbtg.bootcamp.posttest.lottery.response.TicketResponse; + +public interface LotteryService { + + String createLottery(CreateLotteryRequest createLotteryRequest); + + TicketResponse getAllTickets(); + + Boolean checkAmountLottery(String ticket) throws NotFoundException; + + Boolean checkExistTicket(String ticket); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java new file mode 100644 index 00000000..4a631552 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java @@ -0,0 +1,52 @@ +package com.kbtg.bootcamp.posttest.lottery.service.impl; + +import com.kbtg.bootcamp.posttest.entity.Lottery; +import com.kbtg.bootcamp.posttest.exception.NotFoundException; +import com.kbtg.bootcamp.posttest.lottery.repository.LotteryRepository; +import com.kbtg.bootcamp.posttest.lottery.request.CreateLotteryRequest; +import com.kbtg.bootcamp.posttest.lottery.response.TicketResponse; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class LotteryServiceImpl implements LotteryService { + + private final LotteryRepository lotteryRepository; + + @Autowired + public LotteryServiceImpl(LotteryRepository lotteryRepository) { + this.lotteryRepository = lotteryRepository; + } + + @Override + public String createLottery(CreateLotteryRequest createLotteryRequest) { + Lottery lottery = new Lottery(); + lottery.setTicket(createLotteryRequest.getTicket()); + lottery.setPrice(createLotteryRequest.getPrice()); + lottery.setAmount(createLotteryRequest.getAmount()); + Lottery lotteryCreated = lotteryRepository.save(lottery); + return lotteryCreated.getTicket(); + } + + @Override + public TicketResponse getAllTickets() { + List lotteries = lotteryRepository.findAll(); + TicketResponse ticketResponse = new TicketResponse(); + ticketResponse.setTickets(lotteries.stream().map(Lottery::getTicket).toList()); + return ticketResponse; + } + + @Override + public Boolean checkAmountLottery(String ticket) throws NotFoundException { + Lottery lottery = lotteryRepository.findByTicket(ticket) + .orElseThrow(() -> new NotFoundException("Lottery not found with ticket: " + ticket)); + return lottery.getAmount() > 0; + } + + @Override + public Boolean checkExistTicket(String ticket) { + return lotteryRepository.existsByTicket(ticket); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/controller/UserController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/controller/UserController.java new file mode 100644 index 00000000..aecdb93a --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/controller/UserController.java @@ -0,0 +1,89 @@ +package com.kbtg.bootcamp.posttest.user.controller; + +import com.kbtg.bootcamp.posttest.exception.NotFoundException; +import com.kbtg.bootcamp.posttest.helper.ResponseHandler; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +import com.kbtg.bootcamp.posttest.user.response.BuyTicketResponse; +import com.kbtg.bootcamp.posttest.user.response.RefundTicketResponse; +import com.kbtg.bootcamp.posttest.user.service.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/users") +public class UserController { + + private final UserService userService; + private final LotteryService lotteryService; + + @Autowired + public UserController(UserService userService, LotteryService lotteryService) { + this.userService = userService; + this.lotteryService = lotteryService; + } + + @PostMapping("/{userId}/lotteries/{ticket}") + public ResponseEntity buyTicketByUserId( + @PathVariable Long userId, + @PathVariable String ticket + ) { + try { + if (lotteryService.checkAmountLottery(ticket)) { + Long userTicketId = userService.buyTicketByUserId(userId, ticket); + return ResponseHandler.generateResponse("buy ticket success", HttpStatus.OK, + new BuyTicketResponse(userTicketId)); + } else { + return ResponseHandler.generateResponse("ticket " + ticket + " is out of stock", + HttpStatus.BAD_REQUEST, + null); + } + } catch (NotFoundException e) { + return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.NOT_FOUND, + null); + } catch (Exception e) { + return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, + null); + } + } + + @GetMapping("/{userId}/lotteries") + public ResponseEntity getTicketByUser(@PathVariable Long userId) { + try { + return ResponseHandler.generateResponse("get my tickets success", HttpStatus.OK, + userService.getAllTicketsByUserId(userId)); + } catch (NotFoundException e) { + return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.NOT_FOUND, + null); + } catch (Exception e) { + return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, + null); + } + } + + @DeleteMapping("/{userId}/lotteries/{ticket}") + public ResponseEntity createLottery( + @PathVariable Long userId, + @PathVariable String ticket + ) { + try { + String result = userService.deleteUserTicket(userId, ticket); + return ResponseHandler.generateResponse( + "sell lottery ticket " + ticket + " success", + HttpStatus.OK, + new RefundTicketResponse(result)); + } catch (NotFoundException e) { + return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.NOT_FOUND, + null); + } catch (Exception e) { + return ResponseHandler.generateResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR, + null); + } + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserRepository.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserRepository.java new file mode 100644 index 00000000..bc2cb930 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserRepository.java @@ -0,0 +1,10 @@ +package com.kbtg.bootcamp.posttest.user.repository; + +import com.kbtg.bootcamp.posttest.entity.User; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface UserRepository extends JpaRepository { + +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserTicketRepository.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserTicketRepository.java new file mode 100644 index 00000000..7bf1a9f2 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/repository/UserTicketRepository.java @@ -0,0 +1,20 @@ +package com.kbtg.bootcamp.posttest.user.repository; + +import com.kbtg.bootcamp.posttest.entity.Lottery; +import com.kbtg.bootcamp.posttest.entity.User; +import com.kbtg.bootcamp.posttest.entity.UserTicket; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@Repository +public interface UserTicketRepository extends JpaRepository { + + List findAllByUser(User user); + + @Modifying + @Transactional + void deleteByUserAndLottery(User user, Lottery lottery); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/BuyTicketResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/BuyTicketResponse.java new file mode 100644 index 00000000..6af301d2 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/BuyTicketResponse.java @@ -0,0 +1,15 @@ +package com.kbtg.bootcamp.posttest.user.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class BuyTicketResponse { + + private Long id; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/RefundTicketResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/RefundTicketResponse.java new file mode 100644 index 00000000..3c8f11de --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/response/RefundTicketResponse.java @@ -0,0 +1,15 @@ +package com.kbtg.bootcamp.posttest.user.response; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class RefundTicketResponse { + + private String ticket; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/UserService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/UserService.java new file mode 100644 index 00000000..bef23c05 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/UserService.java @@ -0,0 +1,16 @@ +package com.kbtg.bootcamp.posttest.user.service; + +import com.kbtg.bootcamp.posttest.entity.User; +import com.kbtg.bootcamp.posttest.exception.NotFoundException; +import com.kbtg.bootcamp.posttest.lottery.response.GetAllTicketResponse; + +public interface UserService { + + User validateUser(Long userId) throws NotFoundException; + + Long buyTicketByUserId(Long userId, String ticket) throws Exception; + + String deleteUserTicket(Long userId, String ticket) throws Exception; + + GetAllTicketResponse getAllTicketsByUserId(Long userId) throws Exception; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java new file mode 100644 index 00000000..e79520fe --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java @@ -0,0 +1,86 @@ +package com.kbtg.bootcamp.posttest.user.service.impl; + +import com.kbtg.bootcamp.posttest.entity.Lottery; +import com.kbtg.bootcamp.posttest.entity.User; +import com.kbtg.bootcamp.posttest.entity.UserTicket; +import com.kbtg.bootcamp.posttest.exception.NotFoundException; +import com.kbtg.bootcamp.posttest.lottery.repository.LotteryRepository; +import com.kbtg.bootcamp.posttest.lottery.response.GetAllTicketResponse; +import com.kbtg.bootcamp.posttest.user.repository.UserRepository; +import com.kbtg.bootcamp.posttest.user.repository.UserTicketRepository; +import com.kbtg.bootcamp.posttest.user.service.UserService; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserServiceImpl implements UserService { + + private final LotteryRepository lotteryRepository; + private final UserRepository userRepository; + private final UserTicketRepository userTicketRepository; + + @Autowired + public UserServiceImpl(LotteryRepository lotteryRepository, UserRepository userRepository, + UserTicketRepository userTicketRepository) { + this.lotteryRepository = lotteryRepository; + this.userRepository = userRepository; + this.userTicketRepository = userTicketRepository; + } + + @Override + public User validateUser(Long userId) throws NotFoundException { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException("User not found with id: " + userId)); + } + + @Override + @Transactional + public Long buyTicketByUserId(Long userId, String ticket) throws Exception { + User user = validateUser(userId); + + Lottery lottery = lotteryRepository.findByTicket(ticket) + .orElseThrow(() -> new NotFoundException("Ticket not found with ticket: " + ticket)); + lottery.setAmount(0); + + UserTicket userTicket = new UserTicket(); + userTicket.setUser(user); + userTicket.setLottery(lottery); + + lotteryRepository.save(lottery); + UserTicket userTicketCreated = userTicketRepository.save(userTicket); + + return userTicketCreated.getId(); + } + + @Override + @Transactional + public String deleteUserTicket(Long userId, String ticket) throws Exception { + User user = validateUser(userId); + Lottery lottery = lotteryRepository.findByTicket(ticket) + .orElseThrow(() -> new NotFoundException("Ticket not found with ticket: " + ticket)); + lottery.setAmount(1); + + userTicketRepository.deleteByUserAndLottery(user, lottery); + Lottery lotteryUpdated = lotteryRepository.save(lottery); + + return lotteryUpdated.getTicket(); + } + + @Override + public GetAllTicketResponse getAllTicketsByUserId(Long userId) throws Exception { + User user = validateUser(userId); + + List userTickets = userTicketRepository.findAllByUser(user); + GetAllTicketResponse getAllTicketResponse = new GetAllTicketResponse(); + + List tickets = userTickets.stream().map(UserTicket::getLottery).map(Lottery::getTicket) + .toList(); + int cost = userTickets.stream().map(UserTicket::getLottery).map(Lottery::getPrice) + .reduce(0, Integer::sum); + int count = userTickets.size(); + + return new GetAllTicketResponse(tickets, count, cost); + } +} diff --git a/posttest/src/main/resources/application.properties b/posttest/src/main/resources/application.properties deleted file mode 100644 index 8d3eb44e..00000000 --- a/posttest/src/main/resources/application.properties +++ /dev/null @@ -1,6 +0,0 @@ -server.port=8081 -spring.datasource.url=jdbc:postgresql://localhost:5432/ -spring.datasource.username= -spring.datasource.password= -spring.jpa.hibernate.ddl-auto= -spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect \ No newline at end of file diff --git a/posttest/src/main/resources/application.yml b/posttest/src/main/resources/application.yml new file mode 100644 index 00000000..c7d1069b --- /dev/null +++ b/posttest/src/main/resources/application.yml @@ -0,0 +1,76 @@ +server: + shutdown: graceful + port: 8888 +spring: + profiles: + active: "local" + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + generate-ddl: false + hibernate: + transaction: + jta: + platform: org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform + default_schema: public + naming: + implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl + physical-strategy: org.hibernate.boot.model.naming.CamelCaseToUnderscoresNamingStrategy + ddl-auto: none + properties: + hibernate: + globally_quoted_identifiers: true + dialect: org.hibernate.dialect.PostgreSQLDialect + connection: + characterEncoding: utf-8 + CharSet: utf-8 + useUnicode: true + format_sql: true + show-sql: false + liquibase: + enabled: true + mvc: + pathmatch: + matching-strategy: ANT_PATH_MATCHER + main: + allow-circular-references: false + +management: + endpoint: + health: + probes: + enabled: true + health: + livenessState: + enabled: true + readinessstate: + enabled: true + +logging: + level: + root: INFO + org.springframework.web: INFO + org.springframework.security: INFO + +springdoc: + api-docs: + path: /api-docs + enable-native-support: true + swagger-ui: + tags-sorter: alpha + operations-sorter: method + path: /swagger-ui +--- +spring: + config: + activate: + on-profile: local + datasource: + url: jdbc:postgresql://127.0.0.1:5432/assessment?searchpath=public&createDatabaseIfNotExist=true&useUnicode=yes&characterEncoding=UTF-8&serverTimezone=Asia/Bangkok&prepareThreshold=0 + username: root + password: 12345678 + hikari: + schema: public + connectionTimeout: 30000 + leakDetectionThreshold: 120000 +--- + diff --git a/posttest/src/main/resources/db/changelog/db.changelog-master.yaml b/posttest/src/main/resources/db/changelog/db.changelog-master.yaml new file mode 100644 index 00000000..a539be5b --- /dev/null +++ b/posttest/src/main/resources/db/changelog/db.changelog-master.yaml @@ -0,0 +1,3 @@ +databaseChangeLog: + - includeAll: + path: classpath:db/changelog/v1 \ No newline at end of file diff --git a/posttest/src/main/resources/db/changelog/v1/data/user.csv b/posttest/src/main/resources/db/changelog/v1/data/user.csv new file mode 100644 index 00000000..18fdd2db --- /dev/null +++ b/posttest/src/main/resources/db/changelog/v1/data/user.csv @@ -0,0 +1,3 @@ +name,username,password,created_date,modified_date +user1,user1,$2a$12$w0SsL0t9u90pEeG4aL1caOAH6qGLPdECjv1zD87L36gw2AC.Bzz8u,2024-02-18 00:00:00,2024-02-18 00:00:00 +user2,user2,$2a$12$w0SsL0t9u90pEeG4aL1caOAH6qGLPdECjv1zD87L36gw2AC.Bzz8u,2024-02-18 00:00:00,2024-02-18 00:00:00 \ No newline at end of file diff --git a/posttest/src/main/resources/db/changelog/v1/db.changelog-1.0.0.yaml b/posttest/src/main/resources/db/changelog/v1/db.changelog-1.0.0.yaml new file mode 100644 index 00000000..16bb514b --- /dev/null +++ b/posttest/src/main/resources/db/changelog/v1/db.changelog-1.0.0.yaml @@ -0,0 +1,101 @@ +databaseChangeLog: + - changeSet: + id: create user table + author: siwakon.dev + changes: + - createTable: + tableName: user + columns: + - column: + name: id + type: int + autoIncrement: true + incrementBy: 1 + constraints: + primaryKey: true + nullable: false + - column: + name: name + type: varchar(255) + constraints: + nullable: false + - column: + name: username + type: varchar + constraints: + unique: true + uniqueConstraintName: username_unique + nullable: false + - column: + name: password + type: varchar(255) + constraints: + nullable: true + - column: + name: created_date + type: datetime + - column: + name: modified_date + type: timestamp + - changeSet: + id: create lottery table + author: siwakon.dev + changes: + - createTable: + tableName: lottery + columns: + - column: + name: id + type: int + autoIncrement: true + incrementBy: 1 + constraints: + primaryKey: true + nullable: false + - column: + name: ticket + type: varchar(6) + constraints: + unique: true + uniqueConstraintName: ticket_unique + nullable: false + - column: + name: price + type: int + constraints: + nullable: false + - column: + name: amount + type: int + constraints: + nullable: false + - changeSet: + id: create user_ticket table + author: siwakon.dev + changes: + - createTable: + tableName: user_ticket + columns: + - column: + name: id + type: int + autoIncrement: true + incrementBy: 1 + constraints: + primaryKey: true + nullable: false + - column: + name: user_id + type: int + constraints: + nullable: false + foreignKeyName: user_ticket_user_id_fk + references: user(id) + - column: + name: lottery_id + type: int + constraints: + nullable: false + foreignKeyName: user_ticket_lottery_id_fk + references: lottery(id) + diff --git a/posttest/src/main/resources/db/changelog/v1/db.loaddata-1.0.0.yaml b/posttest/src/main/resources/db/changelog/v1/db.loaddata-1.0.0.yaml new file mode 100644 index 00000000..43507aea --- /dev/null +++ b/posttest/src/main/resources/db/changelog/v1/db.loaddata-1.0.0.yaml @@ -0,0 +1,41 @@ +databaseChangeLog: + - changeSet: + id: load user data + author: siwakon.dev + changes: + - loadData: + catalogName: user + columns: + - column: + header: id + name: id + type: NUMERIC + - column: + header: name + name: name + type: varchar + - column: + header: username + name: username + type: varchar + - column: + header: password + name: password + type: varchar + - column: + header: created_date + name: created_date + type: datetime + - column: + header: modified_date + name: modified_date + type: datetime + commentLineStartsWith: // + encoding: UTF-8 + file: data/user.csv + quotchar: '''' + relativeToChangelogFile: true + schemaName: public + separator: ',' + tableName: user + usePreparedStatements: true From a325b68fa7c53bb05ea873528d12109a0fb25482 Mon Sep 17 00:00:00 2001 From: "siwakon.dev" Date: Fri, 23 Feb 2024 22:31:32 +0700 Subject: [PATCH 2/2] add unit test --- posttest/build.gradle | 8 +- posttest/settings.gradle | 8 ++ .../bootcamp/posttest/entity/Lottery.java | 2 + .../InternalServerErrorException.java | 8 ++ .../lottery/service/LotteryService.java | 5 +- .../service/impl/LotteryServiceImpl.java | 33 +++-- .../user/service/impl/UserServiceImpl.java | 1 - .../posttest/PosttestApplicationTests.java | 10 +- .../posttest/admin/AdminControllerTest.java | 100 +++++++++++++++ .../lotttery/LotteryControllerTest.java | 64 ++++++++++ .../posttest/user/UserControllerTest.java | 116 ++++++++++++++++++ 11 files changed, 335 insertions(+), 20 deletions(-) create mode 100644 posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/InternalServerErrorException.java create mode 100644 posttest/src/test/java/com/kbtg/bootcamp/posttest/admin/AdminControllerTest.java create mode 100644 posttest/src/test/java/com/kbtg/bootcamp/posttest/lotttery/LotteryControllerTest.java create mode 100644 posttest/src/test/java/com/kbtg/bootcamp/posttest/user/UserControllerTest.java diff --git a/posttest/build.gradle b/posttest/build.gradle index c7494d4e..b03bf41c 100644 --- a/posttest/build.gradle +++ b/posttest/build.gradle @@ -2,13 +2,13 @@ plugins { id 'java' id 'org.springframework.boot' version '3.2.2' id 'io.spring.dependency-management' version '1.1.4' + id 'org.jetbrains.kotlin.jvm' } group = 'com.kbtg.bootcamp' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = '21' } configurations { @@ -36,8 +36,14 @@ dependencies { compileOnly 'org.projectlombok:lombok:1.18.30' annotationProcessor 'org.projectlombok:lombok:1.18.30' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.testcontainers:junit-jupiter' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8" +// testImplementation 'com.h2database:h2:2.2.224' } tasks.named('test') { useJUnitPlatform() } +kotlin { + jvmToolchain(21) +} diff --git a/posttest/settings.gradle b/posttest/settings.gradle index 5fb54ad4..228303f5 100644 --- a/posttest/settings.gradle +++ b/posttest/settings.gradle @@ -1 +1,9 @@ +pluginManagement { + plugins { + id 'org.jetbrains.kotlin.jvm' version '1.9.22' + } +} +plugins { + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.5.0' +} rootProject.name = 'posttest' diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java index c253e34b..536c462b 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entity/Lottery.java @@ -6,6 +6,7 @@ import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; +import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -14,6 +15,7 @@ @Table(name = "lottery") @Getter @Setter +@AllArgsConstructor @NoArgsConstructor public class Lottery { diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/InternalServerErrorException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/InternalServerErrorException.java new file mode 100644 index 00000000..ba4ea765 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exception/InternalServerErrorException.java @@ -0,0 +1,8 @@ +package com.kbtg.bootcamp.posttest.exception; + +public class InternalServerErrorException extends Exception { + + public InternalServerErrorException(String message) { + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java index 993f88f0..0230f3fb 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/LotteryService.java @@ -1,14 +1,15 @@ package com.kbtg.bootcamp.posttest.lottery.service; +import com.kbtg.bootcamp.posttest.exception.InternalServerErrorException; import com.kbtg.bootcamp.posttest.exception.NotFoundException; import com.kbtg.bootcamp.posttest.lottery.request.CreateLotteryRequest; import com.kbtg.bootcamp.posttest.lottery.response.TicketResponse; public interface LotteryService { - String createLottery(CreateLotteryRequest createLotteryRequest); + String createLottery(CreateLotteryRequest createLotteryRequest) throws Exception; - TicketResponse getAllTickets(); + TicketResponse getAllTickets() throws InternalServerErrorException; Boolean checkAmountLottery(String ticket) throws NotFoundException; diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java index 4a631552..978de023 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/lottery/service/impl/LotteryServiceImpl.java @@ -1,6 +1,7 @@ package com.kbtg.bootcamp.posttest.lottery.service.impl; import com.kbtg.bootcamp.posttest.entity.Lottery; +import com.kbtg.bootcamp.posttest.exception.InternalServerErrorException; import com.kbtg.bootcamp.posttest.exception.NotFoundException; import com.kbtg.bootcamp.posttest.lottery.repository.LotteryRepository; import com.kbtg.bootcamp.posttest.lottery.request.CreateLotteryRequest; @@ -21,21 +22,29 @@ public LotteryServiceImpl(LotteryRepository lotteryRepository) { } @Override - public String createLottery(CreateLotteryRequest createLotteryRequest) { - Lottery lottery = new Lottery(); - lottery.setTicket(createLotteryRequest.getTicket()); - lottery.setPrice(createLotteryRequest.getPrice()); - lottery.setAmount(createLotteryRequest.getAmount()); - Lottery lotteryCreated = lotteryRepository.save(lottery); - return lotteryCreated.getTicket(); + public String createLottery(CreateLotteryRequest createLotteryRequest) throws Exception { + try { + Lottery lottery = new Lottery(); + lottery.setTicket(createLotteryRequest.getTicket()); + lottery.setPrice(createLotteryRequest.getPrice()); + lottery.setAmount(createLotteryRequest.getAmount()); + Lottery lotteryCreated = lotteryRepository.save(lottery); + return lotteryCreated.getTicket(); + } catch (Exception e) { + throw new InternalServerErrorException("Create lottery failed"); + } } @Override - public TicketResponse getAllTickets() { - List lotteries = lotteryRepository.findAll(); - TicketResponse ticketResponse = new TicketResponse(); - ticketResponse.setTickets(lotteries.stream().map(Lottery::getTicket).toList()); - return ticketResponse; + public TicketResponse getAllTickets() throws InternalServerErrorException { + try { + List lotteries = lotteryRepository.findAll(); + TicketResponse ticketResponse = new TicketResponse(); + ticketResponse.setTickets(lotteries.stream().map(Lottery::getTicket).toList()); + return ticketResponse; + } catch (Exception e) { + throw new InternalServerErrorException("Get all tickets failed"); + } } @Override diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java index e79520fe..eda9f2c3 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/user/service/impl/UserServiceImpl.java @@ -73,7 +73,6 @@ public GetAllTicketResponse getAllTicketsByUserId(Long userId) throws Exception User user = validateUser(userId); List userTickets = userTicketRepository.findAllByUser(user); - GetAllTicketResponse getAllTicketResponse = new GetAllTicketResponse(); List tickets = userTickets.stream().map(UserTicket::getLottery).map(Lottery::getTicket) .toList(); diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/PosttestApplicationTests.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/PosttestApplicationTests.java index 8349fc7a..b5158330 100644 --- a/posttest/src/test/java/com/kbtg/bootcamp/posttest/PosttestApplicationTests.java +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/PosttestApplicationTests.java @@ -1,13 +1,15 @@ package com.kbtg.bootcamp.posttest; -import org.junit.jupiter.api.Test; +import org.springframework.boot.SpringApplication; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.TestConfiguration; +@TestConfiguration(proxyBeanMethods = false) @SpringBootTest class PosttestApplicationTests { - @Test - void contextLoads() { - } + public static void main(String[] args) { + SpringApplication.from(PosttestApplication::main).with(PosttestApplication.class).run(args); + } } diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/admin/AdminControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/admin/AdminControllerTest.java new file mode 100644 index 00000000..edb787fd --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/admin/AdminControllerTest.java @@ -0,0 +1,100 @@ +package com.kbtg.bootcamp.posttest.admin; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kbtg.bootcamp.posttest.admin.controller.AdminController; +import com.kbtg.bootcamp.posttest.lottery.request.CreateLotteryRequest; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ExtendWith(MockitoExtension.class) +class AdminControllerTest { + + MockMvc mockMvc; + + @Mock + private LotteryService lotteryService; + + @BeforeEach + public void setUp() { + AdminController adminController = new AdminController(lotteryService); + mockMvc = MockMvcBuilders.standaloneSetup(adminController) + .alwaysDo(print()) + .build(); + } + + @Test + @DisplayName("when post /admin/lotteries with valid request body then return 201") + void createLottery_Success() throws Exception { + CreateLotteryRequest createLotteryRequest = new CreateLotteryRequest(); + createLotteryRequest.setTicket("123456"); + createLotteryRequest.setAmount(1); + createLotteryRequest.setPrice(80); + + when(lotteryService.createLottery(createLotteryRequest)).thenReturn("123456"); + + mockMvc.perform(post("/admin/lotteries") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createLotteryRequest))) + .andExpect(status().isCreated()); + } + + @Test + @DisplayName("when post /admin/lotteries with invalid request body then return 400") + void createLottery_Fail() throws Exception { + CreateLotteryRequest createLotteryRequest = new CreateLotteryRequest(); + createLotteryRequest.setTicket("1234567"); + createLotteryRequest.setAmount(1); + + mockMvc.perform(post("/admin/lotteries") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createLotteryRequest))) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("when post /admin/lotteries with existing ticket then return 409") + void createLottery_Exist() throws Exception { + CreateLotteryRequest createLotteryRequest = new CreateLotteryRequest(); + createLotteryRequest.setTicket("123456"); + createLotteryRequest.setAmount(1); + createLotteryRequest.setPrice(80); + + when(lotteryService.checkExistTicket(createLotteryRequest.getTicket())).thenReturn(true); + + mockMvc.perform(post("/admin/lotteries") + .contentType(MediaType.APPLICATION_JSON) + .content(new ObjectMapper().writeValueAsString(createLotteryRequest))) + .andExpect(status().isConflict()); + } + +// @Test +// @DisplayName("when post /admin/lotteries with exception then return 500") +// void createLottery_Exception() throws Exception { +// CreateLotteryRequest createLotteryRequest = new CreateLotteryRequest(); +// createLotteryRequest.setTicket("123456789"); +// createLotteryRequest.setAmount(1); +// createLotteryRequest.setPrice(80); +// +// when(lotteryService.createLottery(createLotteryRequest)).thenThrow( +// new Exception("Internal Server Error") +// ); +// +// mockMvc.perform(post("/admin/lotteries") +// .contentType(MediaType.APPLICATION_JSON) +// .content(new ObjectMapper().writeValueAsString(createLotteryRequest))) +// .andExpect(status().isInternalServerError()); +// } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/lotttery/LotteryControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/lotttery/LotteryControllerTest.java new file mode 100644 index 00000000..3be41729 --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/lotttery/LotteryControllerTest.java @@ -0,0 +1,64 @@ +package com.kbtg.bootcamp.posttest.lotttery; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.kbtg.bootcamp.posttest.exception.InternalServerErrorException; +import com.kbtg.bootcamp.posttest.lottery.controller.LotteryController; +import com.kbtg.bootcamp.posttest.lottery.response.TicketResponse; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +import java.util.List; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ExtendWith(MockitoExtension.class) +class LotteryControllerTest { + + MockMvc mockMvc; + + @Mock + private LotteryService lotteryService; + + @BeforeEach + public void setUp() { + LotteryController lotteryController = new LotteryController(lotteryService); + mockMvc = MockMvcBuilders.standaloneSetup(lotteryController) + .alwaysDo(print()) + .build(); + } + + @Test + @DisplayName("when get /lotteries then return 200") + void getAllTickets() throws Exception { + mockMvc.perform(get("/lotteries")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("when get /lotteries then return data") + void getAllTickets_Success() throws Exception { + List tickets = List.of("123456", "123457"); + + when(lotteryService.getAllTickets()).thenReturn(new TicketResponse(tickets)); + + mockMvc.perform(get("/lotteries")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("when get /lotteries then return 500") + void getAllTickets_Fail() throws Exception { + when(lotteryService.getAllTickets()).thenThrow( + new InternalServerErrorException("Internal Server Error")); + mockMvc.perform(get("/lotteries")) + .andExpect(status().isInternalServerError()); + } +} \ No newline at end of file diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/user/UserControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/user/UserControllerTest.java new file mode 100644 index 00000000..9e6a2d6b --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/user/UserControllerTest.java @@ -0,0 +1,116 @@ +package com.kbtg.bootcamp.posttest.user; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.kbtg.bootcamp.posttest.entity.Lottery; +import com.kbtg.bootcamp.posttest.exception.NotFoundException; +import com.kbtg.bootcamp.posttest.lottery.response.GetAllTicketResponse; +import com.kbtg.bootcamp.posttest.lottery.service.LotteryService; +import com.kbtg.bootcamp.posttest.user.controller.UserController; +import com.kbtg.bootcamp.posttest.user.service.UserService; +import java.util.List; +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.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +@ExtendWith(MockitoExtension.class) +public class UserControllerTest { + + MockMvc mockMvc; + + @Mock + private LotteryService lotteryService; + + @Mock + private UserService userService; + + @BeforeEach + public void setUp() { + UserController userController = new UserController(userService, lotteryService); + mockMvc = MockMvcBuilders.standaloneSetup(userController) + .alwaysDo(print()) + .build(); + } + + @Test + @DisplayName("when buy ticket by user id then return 200") + void buyTicketByUserId() throws Exception { + when(lotteryService.checkAmountLottery("123456")).thenReturn(true); + when(userService.buyTicketByUserId(1L, "123456")).thenReturn(1L); + + mockMvc.perform(post("/users/1/lotteries/123456")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("when buy ticket out of stock by user id then return 400") + void buyTicketByUserId_Fail() throws Exception { + String ticket = "123456"; + + when(lotteryService.checkAmountLottery(ticket)).thenReturn(false); + + mockMvc.perform(post("/users/1/lotteries/" + ticket) + .contentType("application/json") + .content("ticket" + ticket + " is out of stock")) + .andExpect(status().isBadRequest()); + } + + @Test + @DisplayName("when get ticket by user id then return 200") + void getTicketByUser() throws Exception { + List lotteries = List.of( + new Lottery(1L, "123123", 1, 80), + new Lottery(2L, "234234", 1, 80) + ); + GetAllTicketResponse ticketResponse = new GetAllTicketResponse(); + List tickets = lotteries.stream().map(lottery -> lottery.getTicket()).toList(); + int cost = lotteries.stream().map(Lottery::getPrice).reduce(0, Integer::sum); + int amount = lotteries.size(); + ticketResponse.setTickets(tickets); + ticketResponse.setCost(cost); + ticketResponse.setCount(amount); + when(userService.getAllTicketsByUserId(1L)).thenReturn(ticketResponse); + + mockMvc.perform(get("/users/1/lotteries")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("when get ticket by user id then return 404") + void getTicketByUser_Fail() throws Exception { + when(userService.getAllTicketsByUserId(1L)).thenThrow( + new NotFoundException("User not found with id: 1")); + + mockMvc.perform(get("/users/1/lotteries")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("when delete ticket by user id then return 200") + void deleteUserTicket() throws Exception { + when(userService.deleteUserTicket(1L, "123456")).thenReturn("123456"); + + mockMvc.perform(delete("/users/1/lotteries/123456")) + .andExpect(status().isOk()); + } + + @Test + @DisplayName("when delete ticket by user id then return 404") + void deleteUserTicket_Fail() throws Exception { + when(userService.deleteUserTicket(1L, "123456")).thenThrow( + new NotFoundException("Ticket not found with ticket: 123456")); + + mockMvc.perform(delete("/users/1/lotteries/123456")) + .andExpect(status().isNotFound()); + } +}