diff --git a/.env b/.env new file mode 100644 index 00000000..c609c38e --- /dev/null +++ b/.env @@ -0,0 +1,5 @@ +DATABASE_URL=jdbc:postgresql://db:5432/lottery + +POSTGRES_DB=lottery +POSTGRES_USER=lottery_service +POSTGRES_PASSWORD=lottery_p@ssw0d \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 00000000..3cdde62c --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,22 @@ +version: '3' + +networks: + lottery: + +volumes: + db_data: + +services: + db: + image: postgres:16 + ports: + - "5432:5432" + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - db_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - lottery \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..2113ca9e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: '3' + +networks: + lottery: + +volumes: + db_data: + +services: + api: + build: + context: ./posttest + ports: + - "8888:8888" + depends_on: + - db + environment: + SPRING_DATASOURCE_URL: ${DATABASE_URL} + SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} + SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} + networks: + - lottery + + db: + image: postgres:16 + ports: + - "5432:5432" + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - db_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + networks: + - lottery \ No newline at end of file diff --git a/init.sql b/init.sql new file mode 100644 index 00000000..1ff829b6 --- /dev/null +++ b/init.sql @@ -0,0 +1,34 @@ +CREATE TABLE users ( + user_id INTEGER PRIMARY KEY, + display_name VARCHAR(50) NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE TABLE lottery ( + id SERIAL PRIMARY KEY, + ticket VARCHAR(6) NOT NULL, + price DECIMAL(6, 2) NOT NULL, + amount INTEGER NOT NULL, + current_amount INTEGER NOT NULL, + created_at TIMESTAMP NOT NULL +); + +CREATE TABLE user_ticket ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users (user_id), + lottery_id INTEGER REFERENCES lottery (id), + created_at TIMESTAMP NOT NULL +); + +INSERT INTO + users (user_id, display_name, created_at) +VALUES + (1000000001, 'Alice', NOW ()), + (1000000002, 'Bob', NOW ()); + +INSERT INTO + lottery (ticket, price, amount, current_amount, created_at) +VALUES + ('000001', 80, 1, 1, NOW ()), + ('000002', 80, 2, 2, NOW ()), + ('123456', 80, 3, 3, NOW ()); \ No newline at end of file diff --git a/posttest/Dockerfile b/posttest/Dockerfile new file mode 100644 index 00000000..021b65b1 --- /dev/null +++ b/posttest/Dockerfile @@ -0,0 +1,29 @@ +FROM openjdk:17-jdk-slim as build + +ENV APP_HOME=/usr/app + +WORKDIR $APP_HOME + +COPY build.gradle settings.gradle gradlew $APP_HOME +COPY gradle $APP_HOME/gradle + +RUN chmod +x gradlew +RUN ./gradlew build || return 0 + +COPY . . + +RUN chmod +x gradlew +RUN ./gradlew build + +FROM openjdk:17-jdk-slim + +ENV JAR_FILE=posttest-0.0.1-SNAPSHOT.jar +ENV APP_HOME=/usr/app + +WORKDIR $APP_HOME + +COPY --from=build $APP_HOME/build/libs/$JAR_FILE . + +EXPOSE 8888 + +ENTRYPOINT ["java", "-jar", "posttest-0.0.1-SNAPSHOT.jar"] \ No newline at end of file diff --git a/posttest/build.gradle b/posttest/build.gradle index 6d9dbbe2..560d6a95 100644 --- a/posttest/build.gradle +++ b/posttest/build.gradle @@ -1,5 +1,6 @@ plugins { id 'java' + id 'jacoco' id 'org.springframework.boot' version '3.2.2' id 'io.spring.dependency-management' version '1.1.4' } @@ -23,6 +24,8 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-security' + implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.postgresql:postgresql' compileOnly 'org.projectlombok:lombok' @@ -33,3 +36,22 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + + +test { + finalizedBy jacocoTestReport // report is always generated after tests run +} + +jacocoTestReport { + dependsOn test // tests are required to run before generating the report + reports { + xml.required = false + csv.required = false + html.outputLocation = layout.buildDirectory.dir('jacocoHtml') + } +} + +jacoco { + toolVersion = "0.8.11" + reportsDirectory = layout.buildDirectory.dir('customJacocoReportDir') +} \ No newline at end of file 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..d870272d 100644 --- a/posttest/src/main/java/com/kbtg/bootcamp/posttest/PosttestApplication.java +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/PosttestApplication.java @@ -2,12 +2,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; @SpringBootApplication +@EnableJpaAuditing public class PosttestApplication { - public static void main(String[] args) { SpringApplication.run(PosttestApplication.class, args); } - -} +} \ No newline at end of file diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/SecurityConfig.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/SecurityConfig.java new file mode 100644 index 00000000..c5f5928d --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/SecurityConfig.java @@ -0,0 +1,47 @@ +package com.kbtg.bootcamp.posttest; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +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.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +import static org.springframework.security.config.Customizer.withDefaults; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + @Bean + SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { + return http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests((requests) -> requests + .requestMatchers("/admin/**").hasAnyRole("ADMIN") + .requestMatchers("/**").permitAll() + .anyRequest() + .authenticated()) + .httpBasic(withDefaults()) + .build(); + } + + @Bean + public InMemoryUserDetailsManager userDetailsService() { + UserDetails user = User.withUsername("admin") + .password(passwordEncoder().encode("password")) + .roles("ADMIN") + .build(); + + return new InMemoryUserDetailsManager(user); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/AdminController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/AdminController.java new file mode 100644 index 00000000..acf017d3 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/AdminController.java @@ -0,0 +1,31 @@ +package com.kbtg.bootcamp.posttest.controllers; + +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.services.LotteryService; +import com.kbtg.bootcamp.posttest.models.LotteryResponse; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("admin") +public class AdminController { + @Autowired + private final LotteryService lotteryService; + + public AdminController(LotteryService lotteryService) { + this.lotteryService = lotteryService; + } + + @PostMapping("/lotteries") + public ResponseEntity addLottery(@Valid @RequestBody Lottery lottery) { + var lotteryResponse = lotteryService.addLottery(lottery); + + return new ResponseEntity<>(lotteryResponse, HttpStatus.CREATED); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/LotteryController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/LotteryController.java new file mode 100644 index 00000000..c2531162 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/LotteryController.java @@ -0,0 +1,24 @@ +package com.kbtg.bootcamp.posttest.controllers; + +import com.kbtg.bootcamp.posttest.models.LotteriesResponse; +import com.kbtg.bootcamp.posttest.services.LotteryService; +import org.springframework.beans.factory.annotation.Autowired; +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 { + @Autowired + private final LotteryService lotteryService; + + public LotteryController(LotteryService lotteryService) { + this.lotteryService = lotteryService; + } + + @GetMapping("") + public LotteriesResponse listLotteries() { + return lotteryService.listLotteries(); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/UserLotteryController.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/UserLotteryController.java new file mode 100644 index 00000000..99a74b29 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/controllers/UserLotteryController.java @@ -0,0 +1,39 @@ +package com.kbtg.bootcamp.posttest.controllers; + +import com.kbtg.bootcamp.posttest.models.BuyLotteryResponse; +import com.kbtg.bootcamp.posttest.models.LotteryResponse; +import com.kbtg.bootcamp.posttest.models.MyLotteryResponse; +import com.kbtg.bootcamp.posttest.services.LotteryService; +import com.kbtg.bootcamp.posttest.services.UserService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("users") +public class UserLotteryController { + @Autowired + private final UserService userService; + + @Autowired + private final LotteryService lotteryService; + + public UserLotteryController(UserService userService, LotteryService lotteryService) { + this.userService = userService; + this.lotteryService = lotteryService; + } + + @GetMapping("/{userId}/lotteries") + public MyLotteryResponse listAllMyLottery(@PathVariable("userId") Integer userId) { + return userService.listAllMyLottery(userId); + } + + @PostMapping("/{userId}/lotteries/{ticketId}") + public BuyLotteryResponse buyLottery(@PathVariable("userId") Integer userId, @PathVariable("ticketId") Integer ticketId) { + return lotteryService.buyLottery(userId, ticketId); + } + + @DeleteMapping("/{userId}/lotteries/{ticketId}") + public LotteryResponse sellBackMyLottery(@PathVariable("userId") Integer userId, @PathVariable("ticketId") Integer ticketId) { + return lotteryService.sellBackMyLottery(userId, ticketId); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/AuditEntity.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/AuditEntity.java new file mode 100644 index 00000000..901f4a81 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/AuditEntity.java @@ -0,0 +1,32 @@ +package com.kbtg.bootcamp.posttest.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import jakarta.persistence.*; +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import lombok.ToString; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import java.io.Serializable; +import java.util.Date; + +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +@JsonIgnoreProperties( + value = {"createdAt"}, + allowGetters = true +) +@Data +@Getter +@Setter +@ToString +public abstract class AuditEntity implements Serializable { + @Temporal(TemporalType.TIMESTAMP) + @Column(name = "created_at", nullable = false, updatable = false) + @CreatedDate + @JsonIgnore + private Date createdAt; +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/Lottery.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/Lottery.java new file mode 100644 index 00000000..4b67d6ca --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/Lottery.java @@ -0,0 +1,83 @@ +package com.kbtg.bootcamp.posttest.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.Min; +import lombok.*; +import org.hibernate.validator.constraints.Length; + +import java.math.BigDecimal; + +@Entity +@Table(name = "lottery") +@Data +@NoArgsConstructor +public class Lottery extends AuditEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonIgnore + private Integer id; + + @Length(min = 6, max = 6, message = "Ticket must contain 6 characters") + private String ticket; + + @Column(name = "price", precision = 6, scale = 2) + @DecimalMin(value = "0.00", message = "Price has to be non negative number") + private BigDecimal price; + + @Min(value = 1, message = "Amount must be at least 1") + private Integer amount; + + @JsonIgnore + @Column(name = "current_amount") + @JsonProperty("current_amount") + private Integer currentAmount; + + public Lottery(String ticket, BigDecimal price, Integer amount) { + this.ticket = ticket; + this.price = price; + this.amount = amount; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } + + public BigDecimal getPrice() { + return price; + } + + public void setPrice(BigDecimal price) { + this.price = price; + } + + public Integer getAmount() { + return amount; + } + + public void setAmount(Integer amount) { + this.amount = amount; + } + + public Integer getCurrentAmount() { + return currentAmount; + } + + public void setCurrentAmount(Integer currentAmount) { + this.currentAmount = currentAmount; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/User.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/User.java new file mode 100644 index 00000000..bf77f11b --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/User.java @@ -0,0 +1,44 @@ +package com.kbtg.bootcamp.posttest.entities; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Length; + +@Entity +@Table(name = "users") +@Data +@NoArgsConstructor +public class User extends AuditEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "user_id") + @JsonProperty("user_id") + private Integer userId; + + @Column(name = "display_name") + @JsonProperty("display_name") + @Length(min = 3, max = 50, message = "Display name length should be 3-50 characters") + private String displayName; + + public User(String displayName) { + this.displayName = displayName; + } + + public Integer getUserId() { + return userId; + } + + public void setUserId(Integer userId) { + this.userId = userId; + } + + public String getDisplayName() { + return displayName; + } + + public void setDisplayName(String displayName) { + this.displayName = displayName; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/UserTicket.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/UserTicket.java new file mode 100644 index 00000000..a3d5e455 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/entities/UserTicket.java @@ -0,0 +1,56 @@ +package com.kbtg.bootcamp.posttest.entities; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.*; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Entity +@Table(name = "user_ticket") +@Data +@NoArgsConstructor +public class UserTicket extends AuditEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @JsonIgnore + private Integer id; + + @ManyToOne + @JoinColumn(name = "user_id") + @JsonIgnore + private User user; + + @ManyToOne + @JoinColumn(name = "lottery_id") + @JsonIgnore + private Lottery lottery; + + public UserTicket(User user, Lottery lottery) { + this.user = user; + this.lottery = lottery; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public Lottery getLottery() { + return lottery; + } + + public void setLottery(Lottery lottery) { + this.lottery = lottery; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/LotteryNotFoundException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/LotteryNotFoundException.java new file mode 100644 index 00000000..2cd88d88 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/LotteryNotFoundException.java @@ -0,0 +1,11 @@ +package com.kbtg.bootcamp.posttest.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class LotteryNotFoundException extends RuntimeException { + public LotteryNotFoundException(String message) { + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/LotterySoldOutException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/LotterySoldOutException.java new file mode 100644 index 00000000..88a8ea8d --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/LotterySoldOutException.java @@ -0,0 +1,11 @@ +package com.kbtg.bootcamp.posttest.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class LotterySoldOutException extends LotteryNotFoundException { + public LotterySoldOutException(String message) { + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/UserNotFoundException.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/UserNotFoundException.java new file mode 100644 index 00000000..842c27ed --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/exceptions/UserNotFoundException.java @@ -0,0 +1,11 @@ +package com.kbtg.bootcamp.posttest.exceptions; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class UserNotFoundException extends RuntimeException { + public UserNotFoundException(String message) { + super(message); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/BuyLotteryResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/BuyLotteryResponse.java new file mode 100644 index 00000000..0e1cb6ce --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/BuyLotteryResponse.java @@ -0,0 +1,20 @@ +package com.kbtg.bootcamp.posttest.models; + +import lombok.Data; + +@Data +public class BuyLotteryResponse { + private Integer id; + + public BuyLotteryResponse(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/LotteriesResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/LotteriesResponse.java new file mode 100644 index 00000000..612214ab --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/LotteriesResponse.java @@ -0,0 +1,22 @@ +package com.kbtg.bootcamp.posttest.models; + +import lombok.Data; + +import java.util.List; + +@Data +public class LotteriesResponse { + private List tickets; + + public LotteriesResponse(List tickets) { + this.tickets = tickets; + } + + public List getTickets() { + return tickets; + } + + public void setTickets(List tickets) { + this.tickets = tickets; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/LotteryResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/LotteryResponse.java new file mode 100644 index 00000000..59c204c1 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/LotteryResponse.java @@ -0,0 +1,20 @@ +package com.kbtg.bootcamp.posttest.models; + +import lombok.Data; + +@Data +public class LotteryResponse { + private String ticket; + + public LotteryResponse(String ticket) { + this.ticket = ticket; + } + + public String getTicket() { + return ticket; + } + + public void setTicket(String ticket) { + this.ticket = ticket; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/MyLotteryResponse.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/MyLotteryResponse.java new file mode 100644 index 00000000..a7418d16 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/models/MyLotteryResponse.java @@ -0,0 +1,43 @@ +package com.kbtg.bootcamp.posttest.models; + +import lombok.Data; + +import java.math.BigDecimal; +import java.util.List; + +@Data +public class MyLotteryResponse { + private List tickets; + private Integer count; + private BigDecimal cost; + + public MyLotteryResponse(List tickets, Integer count, BigDecimal cost) { + this.tickets = tickets; + this.count = count; + this.cost = cost; + } + + public List getTickets() { + return tickets; + } + + public void setTickets(List tickets) { + this.tickets = tickets; + } + + public Integer getCount() { + return count; + } + + public void setCount(Integer count) { + this.count = count; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/LotteryRepository.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/LotteryRepository.java new file mode 100644 index 00000000..eb5d7b91 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/LotteryRepository.java @@ -0,0 +1,14 @@ +package com.kbtg.bootcamp.posttest.repositories; + +import com.kbtg.bootcamp.posttest.entities.Lottery; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface LotteryRepository extends JpaRepository { + @Query("SELECT DISTINCT ticket FROM Lottery WHERE currentAmount > 0 ORDER BY ticket") + List findDistinctTickets(); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/UserRepository.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/UserRepository.java new file mode 100644 index 00000000..df870789 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/UserRepository.java @@ -0,0 +1,9 @@ +package com.kbtg.bootcamp.posttest.repositories; + +import com.kbtg.bootcamp.posttest.entities.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/repositories/UserTicketRepository.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/UserTicketRepository.java new file mode 100644 index 00000000..f54b8989 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/repositories/UserTicketRepository.java @@ -0,0 +1,15 @@ +package com.kbtg.bootcamp.posttest.repositories; + +import com.kbtg.bootcamp.posttest.entities.UserTicket; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface UserTicketRepository extends JpaRepository { + List findByUserUserId(@Param("userId") Integer userId); + + UserTicket findFirstByUserUserIdAndLotteryId(@Param("userId") Integer userId, @Param("lotteryId") Integer id); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/BuyerUserService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/BuyerUserService.java new file mode 100644 index 00000000..f8df1809 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/BuyerUserService.java @@ -0,0 +1,32 @@ +package com.kbtg.bootcamp.posttest.services; + +import com.kbtg.bootcamp.posttest.exceptions.UserNotFoundException; +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.models.MyLotteryResponse; +import com.kbtg.bootcamp.posttest.repositories.UserRepository; +import com.kbtg.bootcamp.posttest.entities.UserTicket; +import com.kbtg.bootcamp.posttest.repositories.UserTicketRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; + +@Service +public class BuyerUserService implements UserService { + @Autowired + private UserRepository userRepository; + + @Autowired + private UserTicketRepository userTicketRepository; + + @Override + public MyLotteryResponse listAllMyLottery(Integer userId) { + var user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("User not found")); + var userTickets = userTicketRepository.findByUserUserId(user.getUserId()); + var lotteries = userTickets.stream().map(UserTicket::getLottery).toList(); + var tickets = lotteries.stream().map(Lottery::getTicket).distinct().toList(); + var cost = lotteries.stream().map(Lottery::getPrice).reduce(BigDecimal.ZERO, BigDecimal::add); + + return new MyLotteryResponse(tickets, lotteries.size(), cost); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/LotteryService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/LotteryService.java new file mode 100644 index 00000000..dd78552d --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/LotteryService.java @@ -0,0 +1,18 @@ +package com.kbtg.bootcamp.posttest.services; + +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.models.LotteriesResponse; +import com.kbtg.bootcamp.posttest.models.LotteryResponse; +import com.kbtg.bootcamp.posttest.models.BuyLotteryResponse; +import org.springframework.stereotype.Service; + +@Service +public interface LotteryService { + LotteryResponse addLottery(Lottery lottery); + + LotteriesResponse listLotteries(); + + BuyLotteryResponse buyLottery(Integer userId, Integer ticketId); + + LotteryResponse sellBackMyLottery(Integer userId, Integer ticketId); +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/ThaiLotteryService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/ThaiLotteryService.java new file mode 100644 index 00000000..c5592dd8 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/ThaiLotteryService.java @@ -0,0 +1,79 @@ +package com.kbtg.bootcamp.posttest.services; + +import com.kbtg.bootcamp.posttest.exceptions.LotteryNotFoundException; +import com.kbtg.bootcamp.posttest.exceptions.LotterySoldOutException; +import com.kbtg.bootcamp.posttest.exceptions.UserNotFoundException; +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.models.LotteriesResponse; +import com.kbtg.bootcamp.posttest.repositories.LotteryRepository; +import com.kbtg.bootcamp.posttest.models.LotteryResponse; +import com.kbtg.bootcamp.posttest.models.BuyLotteryResponse; +import com.kbtg.bootcamp.posttest.repositories.UserRepository; +import com.kbtg.bootcamp.posttest.entities.UserTicket; +import com.kbtg.bootcamp.posttest.repositories.UserTicketRepository; +import jakarta.transaction.Transactional; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class ThaiLotteryService implements LotteryService { + @Autowired + private LotteryRepository lotteryRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private UserTicketRepository userTicketRepository; + + @Override + public LotteryResponse addLottery(Lottery lottery) { + lottery.setCurrentAmount(lottery.getAmount()); + + lotteryRepository.save(lottery); + + return new LotteryResponse(lottery.getTicket()); + } + + @Override + public LotteriesResponse listLotteries() { + var tickets = lotteryRepository.findDistinctTickets(); + + return new LotteriesResponse(tickets); + } + + @Override + @Transactional + public BuyLotteryResponse buyLottery(Integer userId, Integer ticketId) { + var user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("User not found")); + var lottery = lotteryRepository.findById(ticketId).orElseThrow(() -> new LotteryNotFoundException("Lottery not found")); + + if (lottery.getCurrentAmount() == 0) { + throw new LotterySoldOutException("Lottery is sold out"); + } + + var userTicket = new UserTicket(user, lottery); + + userTicketRepository.save(userTicket); + + lottery.setCurrentAmount(lottery.getCurrentAmount() - 1); + lotteryRepository.save(lottery); + + return new BuyLotteryResponse(userTicket.getId()); + } + + @Override + @Transactional + public LotteryResponse sellBackMyLottery(Integer userId, Integer ticketId) { + var user = userRepository.findById(userId).orElseThrow(() -> new UserNotFoundException("User not found")); + var lottery = lotteryRepository.findById(ticketId).orElseThrow(() -> new LotteryNotFoundException("Lottery not found")); + var userTicket = userTicketRepository.findFirstByUserUserIdAndLotteryId(user.getUserId(), lottery.getId()); + + userTicketRepository.delete(userTicket); + + lottery.setCurrentAmount(lottery.getCurrentAmount() + 1); + lotteryRepository.save(lottery); + + return new LotteryResponse(lottery.getTicket()); + } +} diff --git a/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/UserService.java b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/UserService.java new file mode 100644 index 00000000..7e142706 --- /dev/null +++ b/posttest/src/main/java/com/kbtg/bootcamp/posttest/services/UserService.java @@ -0,0 +1,7 @@ +package com.kbtg.bootcamp.posttest.services; + +import com.kbtg.bootcamp.posttest.models.MyLotteryResponse; + +public interface UserService { + MyLotteryResponse listAllMyLottery(Integer userId); +} diff --git a/posttest/src/main/resources/application.properties b/posttest/src/main/resources/application.properties index 8d3eb44e..ee0548c9 100644 --- a/posttest/src/main/resources/application.properties +++ b/posttest/src/main/resources/application.properties @@ -1,6 +1,11 @@ -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 +server.port=8888 +spring.datasource.url=jdbc:postgresql://localhost:5432/lottery +spring.datasource.username=lottery_service +spring.datasource.password=lottery_p@ssw0d +spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=validate +spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect +spring.security.user.name=admin +spring.security.user.password=password +logging.level.org.springframework.security=DEBUG +logging.level.org.hibernate.engine.transaction.internal.TransactionImpl=DEBUG \ No newline at end of file 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..864e1020 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,13 @@ package com.kbtg.bootcamp.posttest; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest +@Disabled class PosttestApplicationTests { - @Test void contextLoads() { } - } diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/AdminControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/AdminControllerTest.java new file mode 100644 index 00000000..d1f28855 --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/AdminControllerTest.java @@ -0,0 +1,58 @@ +package com.kbtg.bootcamp.posttest.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.models.LotteryResponse; +import com.kbtg.bootcamp.posttest.services.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.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.math.BigDecimal; + +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class AdminControllerTest { + @Mock + private LotteryService lotteryService; + + @Autowired + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + AdminController adminController = new AdminController(lotteryService); + mockMvc = MockMvcBuilders.standaloneSetup(adminController) + .alwaysDo(print()) + .build(); + } + + @Test + @DisplayName("Test add lottery") + public void addLottery() throws Exception { + var lottery = new Lottery("111111", BigDecimal.valueOf(80), 1); + var lotteryResponse = new LotteryResponse(lottery.getTicket()); + + when(lotteryService.addLottery(lottery)).thenReturn(lotteryResponse); + mockMvc.perform(post("/admin/lotteries") + .contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(lottery))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.ticket").value(lottery.getTicket())); + } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/LotteryControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/LotteryControllerTest.java new file mode 100644 index 00000000..b1cce150 --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/LotteryControllerTest.java @@ -0,0 +1,52 @@ +package com.kbtg.bootcamp.posttest.controllers; + +import com.kbtg.bootcamp.posttest.models.LotteriesResponse; +import com.kbtg.bootcamp.posttest.services.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.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +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.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class LotteryControllerTest { + @Mock + private LotteryService lotteryService; + + @Autowired + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + LotteryController lotteryController = new LotteryController(lotteryService); + mockMvc = MockMvcBuilders.standaloneSetup(lotteryController) + .alwaysDo(print()) + .build(); + } + + @Test + @DisplayName("Test list lotteries") + public void listLotteries() throws Exception { + var lotteries = new LotteriesResponse(List.of("111111", "222222")); + + when(lotteryService.listLotteries()).thenReturn(lotteries); + mockMvc.perform(get("/lotteries") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.tickets", hasSize(2))); + } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/UserLotteryControllerTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/UserLotteryControllerTest.java new file mode 100644 index 00000000..e5d2700a --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/controllers/UserLotteryControllerTest.java @@ -0,0 +1,94 @@ +package com.kbtg.bootcamp.posttest.controllers; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.kbtg.bootcamp.posttest.models.BuyLotteryResponse; +import com.kbtg.bootcamp.posttest.models.LotteryResponse; +import com.kbtg.bootcamp.posttest.models.MyLotteryResponse; +import com.kbtg.bootcamp.posttest.services.LotteryService; +import com.kbtg.bootcamp.posttest.services.UserService; +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.beans.factory.annotation.Autowired; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.math.BigDecimal; +import java.util.List; + +import static org.hamcrest.Matchers.hasSize; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@ExtendWith(MockitoExtension.class) +public class UserLotteryControllerTest { + @Mock + private UserService userService; + + @Mock + private LotteryService lotteryService; + + @Autowired + private MockMvc mockMvc; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @BeforeEach + void setUp() { + UserLotteryController userLotteryController = new UserLotteryController(userService, lotteryService); + mockMvc = MockMvcBuilders.standaloneSetup(userLotteryController) + .alwaysDo(print()) + .build(); + } + + @Test + @DisplayName("Test list all my lottery") + public void listAllMyLottery() throws Exception { + var userId = 1000000001; + var myLotteryResponse = new MyLotteryResponse(List.of("000000", "000001"), 2, BigDecimal.valueOf(160)); + + when(userService.listAllMyLottery(userId)).thenReturn(myLotteryResponse); + mockMvc.perform(get("/users/" + userId + "/lotteries") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.tickets", hasSize(2))) + .andExpect(jsonPath("$.count").value(2)) + .andExpect(jsonPath("$.cost").value(160)); + } + + @Test + @DisplayName("Test buy lottery") + public void buyLottery() throws Exception { + var userId = 1000000001; + var ticketId = 1; + var buyLotteryResponse = new BuyLotteryResponse(1); + + when(lotteryService.buyLottery(userId, ticketId)).thenReturn(buyLotteryResponse); + mockMvc.perform(post("/users/" + userId + "/lotteries/" + ticketId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.id").value(ticketId)); + } + + @Test + @DisplayName("Test sell back my lottery") + public void sellBackMyLottery() throws Exception { + var userId = 1000000001; + var ticketId = 1; + var ticket = "000000"; + var lotteryResponse = new LotteryResponse(ticket); + + when(lotteryService.sellBackMyLottery(userId, ticketId)).thenReturn(lotteryResponse); + mockMvc.perform(delete("/users/" + userId + "/lotteries/" + ticketId) + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.ticket").value(ticket)); + } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/repositories/LotteryRepositoryTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/repositories/LotteryRepositoryTest.java new file mode 100644 index 00000000..91dfe25f --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/repositories/LotteryRepositoryTest.java @@ -0,0 +1,62 @@ +package com.kbtg.bootcamp.posttest.repositories; + +import com.kbtg.bootcamp.posttest.entities.Lottery; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import java.math.BigDecimal; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class LotteryRepositoryTest { + @Autowired + private LotteryRepository lotteryRepository; + + private Lottery lottery; + + @BeforeEach + public void setupTestData() { + lottery = new Lottery("000000", BigDecimal.valueOf(80), 1); + lottery.setId(1); + lottery.setCurrentAmount(lottery.getAmount()); + } + + @Test + @DisplayName("Test save") + public void save() { + lotteryRepository.save(lottery); + + var existsLottery = lotteryRepository.findById(lottery.getId()); + + assertThat(existsLottery).isNotNull(); + } + + @Test + @DisplayName("Test find distinct tickets") + public void findDistinctTickets() { + var newLottery = new Lottery("000000", BigDecimal.valueOf(80), 3); + newLottery.setId(2); + newLottery.setCurrentAmount(newLottery.getAmount()); + + lotteryRepository.save(newLottery); + + var tickets = lotteryRepository.findDistinctTickets(); + + assertThat(tickets).isNotNull(); + assertThat(tickets.size()).isEqualTo(4); + } + + @Test + @DisplayName("Test find by id") + public void findById() { + var existsLottery = lotteryRepository.findById(lottery.getId()); + + assertThat(existsLottery).isNotNull(); + } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/repositories/UserRepositoryTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/repositories/UserRepositoryTest.java new file mode 100644 index 00000000..39a686bb --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/repositories/UserRepositoryTest.java @@ -0,0 +1,36 @@ +package com.kbtg.bootcamp.posttest.repositories; + +import com.kbtg.bootcamp.posttest.entities.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class UserRepositoryTest { + @Autowired + private UserRepository userRepository; + + private User user; + + @BeforeEach + public void setupTestData() { + user = new User("User 1"); + user.setUserId(1000000001); + } + + @Test + @DisplayName("Test find by id") + public void findById() { + userRepository.save(user); + + var existsUser = userRepository.findById(user.getUserId()); + + assertThat(existsUser).isNotNull(); + } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/services/BuyerUserServiceTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/services/BuyerUserServiceTest.java new file mode 100644 index 00000000..54dc195e --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/services/BuyerUserServiceTest.java @@ -0,0 +1,69 @@ +package com.kbtg.bootcamp.posttest.services; + +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.entities.User; +import com.kbtg.bootcamp.posttest.entities.UserTicket; +import com.kbtg.bootcamp.posttest.exceptions.UserNotFoundException; +import com.kbtg.bootcamp.posttest.repositories.UserRepository; +import com.kbtg.bootcamp.posttest.repositories.UserTicketRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class BuyerUserServiceTest { + @Mock + private UserRepository userRepository; + + @Mock + private UserTicketRepository userTicketRepository; + + @InjectMocks + private BuyerUserService buyerUserService; + + @Test + @DisplayName("Test list all my lottery") + public void listAllMyLottery() { + var userId = 1000000001; + var user = new User("User 1"); + user.setUserId(userId); + + var ticket = "000000"; + var lottery = new Lottery(ticket, BigDecimal.valueOf(80), 1); + var userTicket = new UserTicket(user, lottery); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(userTicketRepository.findByUserUserId(userId)).thenReturn(List.of(userTicket)); + + var myLotteryResponse = buyerUserService.listAllMyLottery(userId); + + assertNotNull(myLotteryResponse); + + verify(userRepository).findById(userId); + verify(userTicketRepository).findByUserUserId(userId); + } + + @Test + @DisplayName("Test list all my lottery user not found") + public void listAllMyLotteryUserNotFound() { + var userId = 1000000001; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThrows(UserNotFoundException.class, () -> buyerUserService.listAllMyLottery(userId)); + + verify(userRepository).findById(userId); + } +} diff --git a/posttest/src/test/java/com/kbtg/bootcamp/posttest/services/ThaiLotteryServiceTest.java b/posttest/src/test/java/com/kbtg/bootcamp/posttest/services/ThaiLotteryServiceTest.java new file mode 100644 index 00000000..e920eb57 --- /dev/null +++ b/posttest/src/test/java/com/kbtg/bootcamp/posttest/services/ThaiLotteryServiceTest.java @@ -0,0 +1,210 @@ +package com.kbtg.bootcamp.posttest.services; + +import com.kbtg.bootcamp.posttest.entities.Lottery; +import com.kbtg.bootcamp.posttest.entities.User; +import com.kbtg.bootcamp.posttest.entities.UserTicket; +import com.kbtg.bootcamp.posttest.exceptions.LotteryNotFoundException; +import com.kbtg.bootcamp.posttest.exceptions.LotterySoldOutException; +import com.kbtg.bootcamp.posttest.exceptions.UserNotFoundException; +import com.kbtg.bootcamp.posttest.repositories.LotteryRepository; +import com.kbtg.bootcamp.posttest.repositories.UserRepository; +import com.kbtg.bootcamp.posttest.repositories.UserTicketRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class ThaiLotteryServiceTest { + @Mock + private LotteryRepository lotteryRepository; + + @Mock + private UserRepository userRepository; + + @Mock + private UserTicketRepository userTicketRepository; + + @InjectMocks + private ThaiLotteryService thaiLotteryService; + + @Test + @DisplayName("Test add lottery") + public void addLottery() { + var lottery = new Lottery("000011", BigDecimal.valueOf(80), 1); + + when(lotteryRepository.save(lottery)).thenReturn(lottery); + + var lotteryResponse = thaiLotteryService.addLottery(lottery); + + assertNotNull(lotteryResponse); + + verify(lotteryRepository).save(lottery); + } + + @Test + @DisplayName("Test list all my lottery") + public void listLotteries() { + var lottery1 = "000011"; + var lottery2 = "000012"; + + when(lotteryRepository.findDistinctTickets()).thenReturn(List.of(lottery1, lottery2)); + + var lotteriesResponse = thaiLotteryService.listLotteries(); + + assertNotNull(lotteriesResponse); + + verify(lotteryRepository).findDistinctTickets(); + } + + @Test + @DisplayName("Test buy lottery") + public void buyLottery() { + var userId = 1000000001; + var user = new User("User 1"); + user.setUserId(userId); + + var ticketId = 1; + var lottery = new Lottery("000011", BigDecimal.valueOf(80), 1); + lottery.setCurrentAmount(lottery.getAmount()); + + var userTicket = new UserTicket(user, lottery); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(lotteryRepository.findById(ticketId)).thenReturn(Optional.of(lottery)); + when(userTicketRepository.save(userTicket)).thenReturn(userTicket); + when(lotteryRepository.save(lottery)).thenReturn(lottery); + + var buyLotteryResponse = thaiLotteryService.buyLottery(userId, ticketId); + + assertNotNull(buyLotteryResponse); + + verify(userRepository).findById(userId); + verify(lotteryRepository).findById(ticketId); + verify(userTicketRepository).save(userTicket); + verify(lotteryRepository).save(lottery); + } + + @Test + @DisplayName("Test buy lottery not found") + public void buyLotteryNotFound() { + var userId = 1000000001; + var user = new User("User 1"); + user.setUserId(userId); + + var ticketId = 1; + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(lotteryRepository.findById(ticketId)).thenReturn(Optional.empty()); + + assertThrows(LotteryNotFoundException.class, () -> thaiLotteryService.buyLottery(userId, ticketId)); + + verify(userRepository).findById(userId); + verify(lotteryRepository).findById(ticketId); + } + + @Test + @DisplayName("Test buy lottery user not found") + public void buyLotteryUserNotFound() { + var userId = 1000000001; + var ticketId = 1; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThrows(UserNotFoundException.class, () -> thaiLotteryService.buyLottery(userId, ticketId)); + + verify(userRepository).findById(userId); + } + + @Test + @DisplayName("Test buy lottery sold out") + public void buyLotterySoldOut() { + var userId = 1000000001; + var user = new User("User 1"); + user.setUserId(userId); + + var ticketId = 1; + var lottery = new Lottery("000011", BigDecimal.valueOf(80), 1); + lottery.setCurrentAmount(0); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(lotteryRepository.findById(ticketId)).thenReturn(Optional.of(lottery)); + + assertThrows(LotterySoldOutException.class, () -> thaiLotteryService.buyLottery(userId, ticketId)); + + verify(userRepository).findById(userId); + verify(lotteryRepository).findById(ticketId); + } + + @Test + @DisplayName("Test sell back my lottery") + public void sellBackMyLottery() { + var userId = 1000000001; + var user = new User("User 1"); + user.setUserId(userId); + + var ticketId = 1; + var lottery = new Lottery("000011", BigDecimal.valueOf(80), 1); + lottery.setId(ticketId); + lottery.setCurrentAmount(lottery.getAmount()); + + var userTicket = new UserTicket(user, lottery); + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(lotteryRepository.findById(ticketId)).thenReturn(Optional.of(lottery)); + when(userTicketRepository.findFirstByUserUserIdAndLotteryId(userId, ticketId)).thenReturn(userTicket); + when(lotteryRepository.save(lottery)).thenReturn(lottery); + + var lotteryResponse = thaiLotteryService.sellBackMyLottery(userId, ticketId); + + assertNotNull(lotteryResponse); + + verify(userRepository).findById(userId); + verify(lotteryRepository).findById(ticketId); + verify(userTicketRepository).findFirstByUserUserIdAndLotteryId(userId, ticketId); + verify(userTicketRepository).delete(userTicket); + verify(lotteryRepository).save(lottery); + } + + @Test + @DisplayName("Test sell back my lottery not found") + public void sellBackMyLotteryNotFound() { + var userId = 1000000001; + var user = new User("User 1"); + user.setUserId(userId); + + var ticketId = 1; + + when(userRepository.findById(userId)).thenReturn(Optional.of(user)); + when(lotteryRepository.findById(ticketId)).thenReturn(Optional.empty()); + + assertThrows(LotteryNotFoundException.class, () -> thaiLotteryService.sellBackMyLottery(userId, ticketId)); + + verify(userRepository).findById(userId); + verify(lotteryRepository).findById(ticketId); + } + + @Test + @DisplayName("Test sell back my lottery user not found") + public void sellBackMyLotteryUserNotFound() { + var userId = 1000000001; + var ticketId = 1; + + when(userRepository.findById(userId)).thenReturn(Optional.empty()); + + assertThrows(UserNotFoundException.class, () -> thaiLotteryService.sellBackMyLottery(userId, ticketId)); + + verify(userRepository).findById(userId); + } +}