diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a833849..6f99d47 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,11 @@ - -name: Java CI/CD Pipeline +name: GCD CI/CD Pipeline on: push: branches: + - master - staging + jobs: build: name: Build @@ -27,12 +28,19 @@ jobs: key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} restore-keys: | ${{ runner.os }}-gradle- + - name: Make gradlew executable run: chmod +x ./gradlew - - name: Build with Gradle + + - name: Replace placeholders in application-prod.properties run: | - ./gradlew assemble - # (Optional) Add steps for running tests and generating reports + sed -i 's|${PRODUCTION}|'"${{ secrets.PRODUCTION }}"'|g' src/main/resources/application.properties + sed -i 's|${JDBC_DATABASE_URL}|'"${{ secrets.JDBC_DATABASE_URL }}"'|g' src/main/resources/application-prod.properties + sed -i 's|${JDBC_DATABASE_USERNAME}|'"${{ secrets.JDBC_DATABASE_USERNAME }}"'|g' src/main/resources/application-prod.properties + sed -i 's|${JDBC_DATABASE_PASSWORD}|'"${{ secrets.JDBC_DATABASE_PASSWORD }}"'|g' src/main/resources/application-prod.properties + sed -i 's|${JWT_SECRET}|'"${{ secrets.JWT_SECRET }}"'|g' src/main/resources/application-prod.properties + - name: Build with Gradle + run: ./gradlew assemble - name: Upload Artifact uses: actions/upload-artifact@v4 @@ -54,8 +62,10 @@ jobs: distribution: "temurin" java-version: "21" cache: "gradle" + - name: Make gradlew executable run: chmod +x ./gradlew + - name: Cache Gradle dependencies uses: actions/cache@v4 with: @@ -64,13 +74,6 @@ jobs: restore-keys: | ${{ runner.os }}-gradle- - - name: Test with Gradle - run: | - ./gradlew check --info --stacktrace - ./gradlew test - ./gradlew jacocoTestReport - # (Optional) Add steps for generating coverage report and other post-test tasks - publish: name: Publish Docker Image runs-on: ubuntu-latest @@ -78,25 +81,30 @@ jobs: steps: - name: Checkout Repository uses: actions/checkout@v4 - - name: check directory - run: ls -al + - name: Download Artifact uses: actions/download-artifact@v4 with: name: java-app - - name: check directory - run: ls -al - - name: Login to Docker Hub - run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin docker.io + + - name: Docker login + run: echo ${{ secrets.DOCKER_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin docker.io + - name: Set Docker BuildKit run: export DOCKER_BUILDKIT=1 + - name: Build Docker Image run: | - docker build --build-arg PRODUCTION=${{ secrets.PRODUCTION }} --build-arg JDBC_DATABASE_PASSWORD=${{ secrets.JDBC_DATABASE_PASSWORD }} --build-arg JDBC_DATABASE_URL=${{ secrets.JDBC_DATABASE_URL }} --build-arg JDBC_DATABASE_USERNAME=${{ secrets.JDBC_DATABASE_USERNAME }} -t ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }} . + docker build \ + --build-arg PRODUCTION=${{ secrets.PRODUCTION }} \ + --build-arg JDBC_DATABASE_PASSWORD=${{ secrets.JDBC_DATABASE_PASSWORD }} \ + --build-arg JDBC_DATABASE_URL=${{ secrets.JDBC_DATABASE_URL }} \ + --build-arg JDBC_DATABASE_USERNAME=${{ secrets.JDBC_DATABASE_USERNAME }} \ + --build-arg JWT_SECRET=${{ secrets.JWT_SECRET }} \ + -t ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }} \ + . docker push ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }} - - deploy: name: Deploy to GCP runs-on: ubuntu-latest @@ -108,14 +116,15 @@ jobs: - name: Install SSH client run: sudo apt-get install openssh-client - - name: create ssh key + - name: Create SSH key run: echo "${{ secrets.SSH_KEY }}" > ssh-key.pem - - name: update permission + - name: Update permission run: chmod 400 ssh-key.pem + - name: Deploy to GCP run: | ssh -o StrictHostKeyChecking=no -i ssh-key.pem ${{ secrets.GCP_USERNAME }}@${{ secrets.GCP_STATIC_IP }} " sudo docker container rm -f ${{ secrets.CONTAINER_NAME }} || true && sudo docker image rm -f ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }} || true && - sudo docker run --name ${{ secrets.CONTAINER_NAME }} -d -p 80:8080 ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }}" + sudo docker run --name ${{ secrets.CONTAINER_NAME }} -d -p 80:8080 ${{ secrets.REGISTRY_USER }}/${{ secrets.IMAGE_NAME }}:${{ secrets.IMAGE_TAG }}" \ No newline at end of file diff --git a/.gitignore b/.gitignore index c2065bc..0244794 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,12 @@ out/ ### VS Code ### .vscode/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ diff --git a/.monitoring/docker-compose.yml b/.monitoring/docker-compose.yml new file mode 100644 index 0000000..654ee2e --- /dev/null +++ b/.monitoring/docker-compose.yml @@ -0,0 +1,19 @@ +version: '3.7' + +services: + prometheus: + image: prom/prometheus:v2.44.0 + container_name: prometheus + ports: + - "9090:9090" + volumes: + - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml + + grafana: + image: grafana/grafana:9.5.2 + container_name: grafana + ports: + - "3000:3000" + restart: unless-stopped + volumes: + - ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources \ No newline at end of file diff --git a/.monitoring/grafana/provisioning/datasources/datasources.yml b/.monitoring/grafana/provisioning/datasources/datasources.yml new file mode 100644 index 0000000..8d9f9d8 --- /dev/null +++ b/.monitoring/grafana/provisioning/datasources/datasources.yml @@ -0,0 +1,7 @@ +apiVersion: 1 +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true \ No newline at end of file diff --git a/.monitoring/prometheus/prometheus.yml b/.monitoring/prometheus/prometheus.yml new file mode 100644 index 0000000..58b5e03 --- /dev/null +++ b/.monitoring/prometheus/prometheus.yml @@ -0,0 +1,12 @@ +global: + scrape_interval: 15s # By default, scrape targets every 15 seconds. + evaluation_interval: 15s # By default, scrape rules every 15 seconds. + +scrape_configs: + - job_name: 'MyAppMetrics' + metrics_path: '/actuator/prometheus' + scrape_interval: 3s + static_configs: + - targets: ['34.124.168.155:80'] + labels: + application: 'snackscription_subscriptionbox' diff --git a/build.gradle.kts b/build.gradle.kts index a02ee00..50cde73 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,12 @@ plugins { java - id("org.springframework.boot") version "3.2.5" jacoco + id("org.springframework.boot") version "3.2.5" id("io.spring.dependency-management") version "1.1.4" + id("org.sonarqube") version "4.4.1.3373" } -group = "id.ac.ui.cs.advprog" +group = "snackscription" version = "0.0.1-SNAPSHOT" java { @@ -22,36 +23,73 @@ repositories { mavenCentral() } +val springBootVersion = "2.5.0" +val micrometerVersion = "1.12.5" +val dotenvVersion = "4.0.0" +val jwtVersion = "0.12.5" + dependencies { + implementation("org.springframework.boot:spring-boot-starter-logging") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-thymeleaf") implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.boot:spring-boot-starter-actuator:$springBootVersion") + implementation("me.paulschwarz:spring-dotenv:$dotenvVersion") + implementation("io.jsonwebtoken:jjwt-api:$jwtVersion") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-webflux") compileOnly("org.projectlombok:lombok") - developmentOnly("org.springframework.boot:spring-boot-devtools") - runtimeOnly("org.postgresql:postgresql") annotationProcessor("org.springframework.boot:spring-boot-configuration-processor") annotationProcessor("org.projectlombok:lombok") + developmentOnly("org.springframework.boot:spring-boot-devtools") + runtimeOnly("org.postgresql:postgresql") + runtimeOnly("io.micrometer:micrometer-registry-prometheus:$micrometerVersion") + runtimeOnly("io.jsonwebtoken:jjwt-impl:$jwtVersion") + runtimeOnly("io.jsonwebtoken:jjwt-jackson:$jwtVersion") testImplementation("org.springframework.boot:spring-boot-starter-test") + runtimeOnly("io.micrometer:micrometer-registry-prometheus") + implementation("org.springframework.boot:spring-boot-starter-actuator") } -tasks.withType { - useJUnitPlatform() + +tasks.register("unitTest") { + description = "Runs unit tests." + group = "verification" + + filter { + excludeTestsMatching("*FunctionalTest") + } } +tasks.register("functionalTest") { + description = "Runs functional tests." + group = "verification" -tasks.test { + filter { + includeTestsMatching("*FunctionalTest") + } +} + +tasks.withType().configureEach { useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run } + +tasks.test{ + filter{ + excludeTestsMatching("*FunctionalTest") + } + + finalizedBy(tasks.jacocoTestReport) +} + tasks.jacocoTestReport { classDirectories.setFrom(files(classDirectories.files.map { fileTree(it) { exclude("**/*Application**") } })) dependsOn(tasks.test) // tests are required to run before generating the report reports { - xml.required.set(false) - csv.required.set(false) + xml.required.set(true) + csv.required.set(true) html.outputLocation.set(layout.buildDirectory.dir("jacocoHtml")) } -} - +} \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/config/JWTAuthFilter.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/config/JWTAuthFilter.java new file mode 100644 index 0000000..940439a --- /dev/null +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/config/JWTAuthFilter.java @@ -0,0 +1,48 @@ +package id.ac.ui.cs.advprog.snackscription_subscriptionbox.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.utils.JWTUtils; + +import java.io.IOException; +import java.util.Collections; + +@Component +public class JWTAuthFilter extends OncePerRequestFilter { + + private final JWTUtils jwtUtils; + + public JWTAuthFilter(JWTUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + final String authHeader = request.getHeader("Authorization"); + final String jwtToken; + + if (authHeader == null || authHeader.isBlank()) { + filterChain.doFilter(request, response); + return; + } + + jwtToken = authHeader.substring(7); + + if (jwtUtils.isTokenValid(jwtToken)) { + String role = jwtUtils.extractRole(jwtToken); + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( + null, null, Collections.singletonList(() -> role)); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/config/SecurityConfig.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/config/SecurityConfig.java new file mode 100644 index 0000000..41f7678 --- /dev/null +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/config/SecurityConfig.java @@ -0,0 +1,37 @@ +package id.ac.ui.cs.advprog.snackscription_subscriptionbox.config; + +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.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.utils.JWTUtils; + +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JWTUtils jwtUtils; + + public SecurityConfig(JWTUtils jwtUtils) { + this.jwtUtils = jwtUtils; + } + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception { + httpSecurity + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authorizeRequests -> + authorizeRequests + .requestMatchers("/actuator/prometheus").permitAll() // Allow unauthenticated access + .requestMatchers("/subscription-box/**", "/public/**").permitAll() + .anyRequest().authenticated()) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .addFilterBefore(new JWTAuthFilter(jwtUtils), UsernamePasswordAuthenticationFilter.class); + + return httpSecurity.build(); + } +} diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxController.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxController.java index 725c95b..1123a09 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxController.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxController.java @@ -1,5 +1,10 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.controller; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.utils.JWTUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.DTOMapper; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.SubscriptionBoxDTO; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.SubscriptionBox; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.service.SubscriptionBoxService; import org.springframework.beans.factory.annotation.Autowired; @@ -7,68 +12,96 @@ import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Locale; import java.util.Optional; import java.util.UUID; import java.util.concurrent.CompletableFuture; @RestController @RequestMapping("/subscription-box") -@CrossOrigin(origins = "http://localhost:3000") // Change to specific origin if needed +@CrossOrigin(origins = "*") // Change to specific origin if needed public class SubscriptionBoxController { - + private final JWTUtils jwtUtils; private final SubscriptionBoxService subscriptionBoxService; - @Autowired - public SubscriptionBoxController(SubscriptionBoxService subscriptionBoxService) { + public SubscriptionBoxController(SubscriptionBoxService subscriptionBoxService, JWTUtils jwtUtils) { this.subscriptionBoxService = subscriptionBoxService; + this.jwtUtils = jwtUtils; + } + + private static final Logger logger = LoggerFactory.getLogger(SubscriptionBoxController.class); + + private void validateToken(String token) throws IllegalAccessException { + String jwt = token.replace("Bearer ", ""); + if (!jwtUtils.isTokenValid(jwt)) { + throw new IllegalAccessException("You have no permission."); + } + } + + private void validateAdminOnly(String token) throws IllegalAccessException { + String jwt = token.replace("Bearer ", ""); + if (!jwtUtils.extractRole(jwt).equalsIgnoreCase("admin")) { + throw new IllegalAccessException("You have no permission."); + } + } + + @GetMapping("") + public ResponseEntity main(@RequestHeader(value = "Authorization") String token) throws IllegalAccessException { + validateToken(token); + return ResponseEntity.ok("Snackscription - SubscriptionBox Management API by ADMIN only!"); } @PostMapping("/create") - public CompletableFuture> createSubscriptionBox(@RequestBody SubscriptionBox subscriptionBox) { - return subscriptionBoxService.save(subscriptionBox) + public CompletableFuture> createSubscriptionBox(@RequestHeader(value = "Authorization") String token, @RequestBody SubscriptionBoxDTO subscriptionBoxDTO) throws IllegalAccessException { + validateAdminOnly(token); + return subscriptionBoxService.save(subscriptionBoxDTO) .thenApply(ResponseEntity::ok) .exceptionally(ex -> ResponseEntity.badRequest().build()); } @GetMapping("/list") - public CompletableFuture>> findAll() { + public CompletableFuture>> findAll(@RequestHeader(value = "Authorization") String token) throws IllegalAccessException { + validateToken(token); return subscriptionBoxService.findAll() .thenApply(ResponseEntity::ok); } - @GetMapping("/{id}") - public CompletableFuture> findById(@PathVariable String id) { - try { - UUID.fromString(id); - } catch (IllegalArgumentException e) { - return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); - } - - return subscriptionBoxService.findById(id) - .thenApply(optionalSubscriptionBox -> - optionalSubscriptionBox.map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build())); - } - @PatchMapping("/update") - public CompletableFuture> updateSubscriptionBox(@RequestBody SubscriptionBox subscriptionBox) { - if (subscriptionBox.getId() == null || subscriptionBox.getId().isEmpty()) { + public CompletableFuture> updateSubscriptionBox(@RequestHeader(value = "Authorization") String token, @RequestBody SubscriptionBoxDTO subscriptionBoxDTO) throws IllegalAccessException { + validateAdminOnly(token); + if (subscriptionBoxDTO.getId() == null || subscriptionBoxDTO.getId().isEmpty()) { return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); } - return subscriptionBoxService.findById(subscriptionBox.getId()) + return subscriptionBoxService.findById(subscriptionBoxDTO.getId()) .thenCompose(optionalSubscriptionBox -> { if (optionalSubscriptionBox.isEmpty()) { return CompletableFuture.completedFuture(ResponseEntity.notFound().build()); } else { - return subscriptionBoxService.update(subscriptionBox) + return subscriptionBoxService.update(subscriptionBoxDTO) .thenApply(ResponseEntity::ok); } }); } + @GetMapping("/{id}") + public CompletableFuture> findById(@RequestHeader(value = "Authorization") String token, @PathVariable String id) throws IllegalAccessException { + validateToken(token); + try { + UUID.fromString(id); + } catch (IllegalArgumentException e) { + return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); + } + + return subscriptionBoxService.findById(id) + .thenApply(optionalSubscriptionBox -> + optionalSubscriptionBox.map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build())); + } + @DeleteMapping("/{id}") - public CompletableFuture> deleteSubscriptionBox(@PathVariable String id) { + public CompletableFuture> deleteSubscriptionBox(@RequestHeader(value = "Authorization") String token, @PathVariable String id) throws IllegalAccessException { + validateAdminOnly(token); try { UUID.fromString(id); } catch (IllegalArgumentException e) { @@ -81,31 +114,38 @@ public CompletableFuture> deleteSubscriptionBox(@PathVari } @GetMapping("/price/less-than/{price}") - public CompletableFuture>> findByPriceLessThan(@PathVariable int price) { + public CompletableFuture>> findByPriceLessThan(@RequestHeader(value = "Authorization") String token, @PathVariable int price) throws IllegalAccessException { + validateToken(token); return subscriptionBoxService.findByPriceLessThan(price) .thenApply(ResponseEntity::ok); } + @GetMapping("/price/greater-than/{price}") - public CompletableFuture>> findByPriceGreaterThan(@PathVariable int price) { + public CompletableFuture>> findByPriceGreaterThan(@RequestHeader(value = "Authorization") String token, @PathVariable int price) throws IllegalAccessException { + validateToken(token); return subscriptionBoxService.findByPriceGreaterThan(price) .thenApply(ResponseEntity::ok); } @GetMapping("/price/equals/{price}") - public CompletableFuture>> findByPriceEquals(@PathVariable int price) { + public CompletableFuture>> findByPriceEquals(@RequestHeader(value = "Authorization") String token, @PathVariable int price) throws IllegalAccessException { + validateToken(token); return subscriptionBoxService.findByPriceEquals(price) .thenApply(ResponseEntity::ok); } - @GetMapping("/name/{name}") - public CompletableFuture>>> findByName(@PathVariable String name) { + @GetMapping("/name/{nameURL}") + public CompletableFuture>>> findByName(@RequestHeader(value = "Authorization") String token, @PathVariable String nameURL) throws IllegalAccessException { + validateToken(token); + String name = nameURL.replaceAll("-", " "); return subscriptionBoxService.findByName(name) .thenApply(ResponseEntity::ok); } @GetMapping("/distinct-names") - public CompletableFuture>>> findDistinctNames() { + public CompletableFuture>>> findDistinctNames(@RequestHeader(value = "Authorization") String token) throws IllegalAccessException { + validateToken(token); return subscriptionBoxService.findDistinctNames() .thenApply(ResponseEntity::ok); } diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/DTOMapper.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/DTOMapper.java index 710e0e6..9d841f6 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/DTOMapper.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/DTOMapper.java @@ -1,39 +1,82 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.Item; -import org.springframework.stereotype.Component; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.SubscriptionBox; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.factory.SubscriptionBoxFactory; +import org.springframework.stereotype.Component; import java.util.List; import java.util.Optional; +import java.util.stream.Collectors; @Component public class DTOMapper { - public static SubscriptionBoxDTO convertModeltoDto (SubscriptionBox subscriptionBox){ + public static SubscriptionBoxDTO convertModelToDto(SubscriptionBox subscriptionBox) { + List itemDTOs = Optional.ofNullable(subscriptionBox.getItems()) + .map(items -> items.stream() + .map(DTOMapper::convertItemToDto) + .collect(Collectors.toList())) + .orElse(null); + return new SubscriptionBoxDTO( subscriptionBox.getId(), subscriptionBox.getName(), subscriptionBox.getType(), subscriptionBox.getPrice(), - subscriptionBox.getItems() + itemDTOs, + subscriptionBox.getDescription() ); } - public static SubscriptionBox convertDTOtoModel(SubscriptionBoxDTO subscriptionBoxDTO){ - String id = subscriptionBoxDTO.getId(); - String name = subscriptionBoxDTO.getName(); - String type = subscriptionBoxDTO.getType(); - int price = subscriptionBoxDTO.getPrice(); - List items = subscriptionBoxDTO.getItems(); - return new SubscriptionBoxFactory().create(id,name,type,price,items); + public static SubscriptionBox convertDTOtoModel(SubscriptionBoxDTO subscriptionBoxDTO) { + List items = Optional.ofNullable(subscriptionBoxDTO.getItems()).map(dtoItems -> + dtoItems.stream() + .map(DTOMapper::convertDtoToItem) + .collect(Collectors.toList()) + ).orElse(null); + + return new SubscriptionBoxFactory().create( + subscriptionBoxDTO.getId(), + subscriptionBoxDTO.getName(), + subscriptionBoxDTO.getType(), + subscriptionBoxDTO.getPrice(), + items, + subscriptionBoxDTO.getDescription() + ); } - public static SubscriptionBox updateSubscriptionBox(SubscriptionBox subscriptionBox, SubscriptionBoxDTO subscriptionBoxDTO){ - Optional.ofNullable(subscriptionBoxDTO.getItems()).ifPresent(subscriptionBox::setItems); - Optional.of( - subscriptionBoxDTO.getPrice()).ifPresent(subscriptionBox::setPrice); + public static SubscriptionBox updateSubscriptionBox(SubscriptionBox subscriptionBox, SubscriptionBoxDTO subscriptionBoxDTO) { + Optional.ofNullable(subscriptionBoxDTO.getItems()).ifPresent(dtoItems -> { + List items = dtoItems.stream() + .map(DTOMapper::convertDtoToItem) + .collect(Collectors.toList()); + subscriptionBox.setItems(items); + }); + Optional.of(subscriptionBoxDTO.getPrice()).ifPresent(subscriptionBox::setPrice); + Optional.ofNullable(subscriptionBoxDTO.getDescription()).ifPresent(subscriptionBox::setDescription); return subscriptionBox; + } + + public static ItemDTO convertItemToDto(Item item) { + return new ItemDTO( + item.getId(), + item.getName(), + item.getQuantity() + ); + } + + public static Item convertDtoToItem(ItemDTO itemDTO) { + return new Item( + itemDTO.getId(), + itemDTO.getName(), + itemDTO.getQuantity() + ); + } + public static Item updateItem(Item item, ItemDTO itemDTO) { + Optional.ofNullable(itemDTO.getId()).ifPresent(item::setId); + Optional.ofNullable(itemDTO.getName()).ifPresent(item::setName); + Optional.of(itemDTO.getQuantity()).ifPresent(item::setQuantity); + return item; } } diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/ItemDTO.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/ItemDTO.java new file mode 100644 index 0000000..629e890 --- /dev/null +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/ItemDTO.java @@ -0,0 +1,14 @@ +package id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto; + +import lombok.*; + + +@AllArgsConstructor +@NoArgsConstructor +@Getter +@Setter +public class ItemDTO { + private String id; + private String name; + private int quantity; +} \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/SubscriptionBoxDTO.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/SubscriptionBoxDTO.java index e608fb1..b290672 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/SubscriptionBoxDTO.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/dto/SubscriptionBoxDTO.java @@ -7,11 +7,13 @@ @AllArgsConstructor @NoArgsConstructor -@Getter @Setter +@Getter +@Setter public class SubscriptionBoxDTO { - String id; - String name; - String type; - int price; - List items; -} + private String id; + private String name; + private String type; + private int price; + private List items; + private String description; +} \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/Factory.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/Factory.java index f5f4d16..42ddf56 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/Factory.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/Factory.java @@ -7,5 +7,5 @@ public interface Factory { T create(); - T create(String id, String name, String type, int price, List items ); + T create(String id, String name, String type, int price, List items , String description); } \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactory.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactory.java index c35f306..25a5006 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactory.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactory.java @@ -11,7 +11,7 @@ public SubscriptionBox create(){ return new SubscriptionBox(); } - public SubscriptionBox create(String id, String name, String type, int price, List items ){ - return new SubscriptionBox( name, type, price, items); + public SubscriptionBox create(String id, String name, String type, int price, List items, String description){ + return new SubscriptionBox( name, type, price, items, description); } } diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/Item.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/Item.java index d2b73c6..b83498d 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/Item.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/Item.java @@ -1,5 +1,6 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.model; +import com.fasterxml.jackson.annotation.JsonBackReference; import jakarta.persistence.*; import lombok.Getter; import lombok.Setter; @@ -19,5 +20,15 @@ public class Item { private int quantity; @ManyToMany(mappedBy = "items") + @JsonBackReference private List subscriptionBoxes; + + public Item(String id, String name, int quantity) { + this.id = id; + this.name = name; + this.quantity = quantity; + } + + public Item() { + } } \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBox.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBox.java index 27b9af7..a6142c7 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBox.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBox.java @@ -1,6 +1,7 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.model; +import com.fasterxml.jackson.annotation.JsonManagedReference; import jakarta.persistence.*; import lombok.Builder; import lombok.Getter; @@ -30,19 +31,24 @@ public class SubscriptionBox { joinColumns = @JoinColumn(name = "subscriptionbox_id", referencedColumnName = "id"), inverseJoinColumns = @JoinColumn(name = "item_id", referencedColumnName = "id") ) + @JsonManagedReference List items; + + @Column(name = "box_description") + String description; // Rating rating; public SubscriptionBox(){ this.id = UUID.randomUUID().toString(); } - public SubscriptionBox( String name, String type, int price, List items){ + public SubscriptionBox( String name, String type, int price, List items, String description){ this.id = UUID.randomUUID().toString(); - this.name = name; + this.setName(name); this.setType(type); this.setPrice(price); this.items = items; + this.description = description; } public void setType(String type) { @@ -50,7 +56,7 @@ public void setType(String type) { type.equalsIgnoreCase("quarterly") | type.equalsIgnoreCase("semi-annual") ){ - this.type = type; + this.type = type.toUpperCase(); } else{ throw new IllegalArgumentException("Invalid type"); @@ -58,7 +64,17 @@ public void setType(String type) { } + public void setName(String name) { + if (!name.contains("-") + ){ + this.name = name; + } + else{ + throw new IllegalArgumentException("Do not put '-' inside your name!"); + } + + } public void setPrice(int price) { if (price >0){ this.price = price; diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepository.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepository.java index b45df42..1808ac9 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepository.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepository.java @@ -24,15 +24,39 @@ public class SubscriptionBoxRepository { @Transactional public SubscriptionBox save(SubscriptionBox subscriptionBox) { + if (hasThreeSimilarNames(subscriptionBox.getName())) { + throw new IllegalArgumentException("Cannot save subscription box: more than 3 subscription boxes with similar names already exist."); + } + if (existsByNameAndType(subscriptionBox.getName(), subscriptionBox.getType())) { + throw new IllegalArgumentException("Cannot save subscription box: a subscription box with the same name and type already exists."); + } entityManager.persist(subscriptionBox); return subscriptionBox; } + private boolean hasThreeSimilarNames(String name) { + String jpql = "SELECT sb FROM SubscriptionBox sb WHERE LOWER(sb.name) LIKE LOWER(:name)"; + TypedQuery query = entityManager.createQuery(jpql, SubscriptionBox.class); + query.setParameter("name", "%" + name + "%"); + List result = query.getResultList(); + return result.size() >= 3; + } + + private boolean existsByNameAndType(String name, String type) { + String jpql = "SELECT sb FROM SubscriptionBox sb WHERE LOWER(sb.name) = LOWER(:name) AND LOWER(sb.type) = LOWER(:type)"; + TypedQuery query = entityManager.createQuery(jpql, SubscriptionBox.class); + query.setParameter("name", name); + query.setParameter("type", type); + List result = query.getResultList(); + return !result.isEmpty(); + } + @Transactional public Optional findById(String id){ - SubscriptionBox subscription = entityManager.find(SubscriptionBox.class, id); - return Optional.ofNullable(subscription); + SubscriptionBox subscriptionBox = entityManager.find(SubscriptionBox.class, id); + return Optional.ofNullable(subscriptionBox); } + @Transactional public List findAll(){ String jpql = "SELECT sb FROM SubscriptionBox sb"; @@ -93,59 +117,5 @@ public Optional> findDistinctNames() { return Optional.ofNullable(result.isEmpty() ? null : result); } -// private List subscriptionBoxes = new ArrayList<>(); -// private List filteredBoxesByPrice = new ArrayList<>(); -//// private List filteredBoxesByRating = new ArrayList<>(); -// -// public SubscriptionBox addBox(SubscriptionBox box) { -// subscriptionBoxes.add(box); -// return box; -// } -// -// public SubscriptionBox deleteBox(String id) { -// for (SubscriptionBox subscriptionBox : subscriptionBoxes) { -// if (subscriptionBox.getId().equals(id)) { -// subscriptionBoxes.remove(subscriptionBox); -// return subscriptionBox; -// } -// } -// return null; -// } -// -// public SubscriptionBox editBox(String id, SubscriptionBox updatedBox) { -// for (SubscriptionBox box : subscriptionBoxes) { -// if (box.getId().equals(id)) { -// // Assuming the updatedBox object contains the updated fields -// box.setName(updatedBox.getName()); -// box.setPrice(updatedBox.getPrice()); -// return box; // Return the updated box -// } -// } -// return null; // Return null if no box with the given id was found -// } -// -// -// public List viewAll() { -// return subscriptionBoxes; -// } -// -// public String viewDetails(String boxId) { -// for (SubscriptionBox subscriptionBox : subscriptionBoxes) { -// if (subscriptionBox.getId().equals(boxId)) { -// return subscriptionBox.getName(); -// } -// } -// return null; -// } -// -// public List filterByPrice(int price) { -// List filteredBoxes = new ArrayList<>(); -// for (SubscriptionBox subscriptionBox : subscriptionBoxes) { -// if (subscriptionBox.getPrice() == price) { -// filteredBoxes.add(subscriptionBox); -// } -// } -// filteredBoxesByPrice = filteredBoxes; -// return filteredBoxesByPrice; -// } + } \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxService.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxService.java index a8c52c7..9085098 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxService.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxService.java @@ -2,6 +2,7 @@ import java.util.List; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.SubscriptionBoxDTO; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.SubscriptionBox; import org.springframework.scheduling.annotation.Async; @@ -9,23 +10,23 @@ import java.util.concurrent.CompletableFuture; public interface SubscriptionBoxService { - CompletableFuture save(SubscriptionBox subscriptionBox); + CompletableFuture save(SubscriptionBoxDTO subscriptionBoxDTO); - CompletableFuture> findById(String id); + CompletableFuture> findById(String id); CompletableFuture> findAll(); - CompletableFuture update(SubscriptionBox subscriptionBox); + CompletableFuture update(SubscriptionBoxDTO subscriptionBoxDTO); CompletableFuture delete(String id); - CompletableFuture> findByPriceLessThan(int price); + CompletableFuture> findByPriceLessThan(int price); - CompletableFuture> findByPriceGreaterThan(int price); + CompletableFuture> findByPriceGreaterThan(int price); - CompletableFuture> findByPriceEquals(int price); + CompletableFuture> findByPriceEquals(int price); - CompletableFuture>> findByName(String name); + CompletableFuture>> findByName(String name); CompletableFuture>> findDistinctNames(); diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImpl.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImpl.java index 1c64846..8c6b472 100644 --- a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImpl.java +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImpl.java @@ -5,7 +5,10 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.SubscriptionBoxDTO; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.DTOMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; @@ -20,17 +23,26 @@ public class SubscriptionBoxServiceImpl implements SubscriptionBoxService { @Override @Async - public CompletableFuture save(SubscriptionBox subscriptionBox) { - return CompletableFuture.completedFuture(subscriptionBoxRepository.save(subscriptionBox)); + public CompletableFuture save(SubscriptionBoxDTO subscriptionBoxDTO) { + try{ + SubscriptionBox subscriptionBox = DTOMapper.convertDTOtoModel(subscriptionBoxDTO); + return CompletableFuture.completedFuture(subscriptionBoxRepository.save(subscriptionBox)); + } catch (Exception e) { + throw new IllegalArgumentException("Invalid Request", e); + } } @Override @Async - public CompletableFuture> findById(String id) { + public CompletableFuture> findById(String id) { if (id == null || id.isEmpty()) { throw new IllegalArgumentException("ID cannot be null or empty"); } - return CompletableFuture.completedFuture(subscriptionBoxRepository.findById(id)); + + return subscriptionBoxRepository.findById(id) + .map(subscriptionBox -> CompletableFuture.completedFuture(Optional.of(DTOMapper.convertModelToDto(subscriptionBox)))) + .orElse(CompletableFuture.completedFuture(Optional.empty())); +// } @Override @@ -42,21 +54,32 @@ public CompletableFuture> findAll() { @Override @Async - public CompletableFuture update(SubscriptionBox subscriptionBox) { - if (subscriptionBox == null) { - throw new IllegalArgumentException("SubscriptionBox cannot be null"); + public CompletableFuture update(SubscriptionBoxDTO subscriptionBoxDTO) { +// if (subscriptionBox == null) { +// throw new IllegalArgumentException("SubscriptionBox cannot be null"); +// } +// return CompletableFuture.completedFuture(subscriptionBoxRepository.update(subscriptionBox)); + if (subscriptionBoxDTO == null) { + throw new IllegalArgumentException("Subscription cannot be null"); } - return CompletableFuture.completedFuture(subscriptionBoxRepository.update(subscriptionBox)); + + return subscriptionBoxRepository.findById(subscriptionBoxDTO.getId()) + .map(subscriptionBox -> { + DTOMapper.updateSubscriptionBox(subscriptionBox, subscriptionBoxDTO); + return CompletableFuture.completedFuture(subscriptionBoxRepository.update(subscriptionBox)); + }) + .orElseThrow(() -> new IllegalArgumentException("Subscription isn't found")); + } @Override @Async public CompletableFuture delete(String id) { if (id == null || id.isEmpty()) { - throw new IllegalArgumentException("ID cannot be null or empty"); + return CompletableFuture.failedFuture(new IllegalArgumentException("ID cannot be null or empty")); } - Optional subscriptionBox = subscriptionBoxRepository.findById(id); - if (!subscriptionBox.isPresent()) { + + if (subscriptionBoxRepository.findById(id).isEmpty()) { throw new IllegalArgumentException("Subscription Box not found"); } subscriptionBoxRepository.delete(id); @@ -65,32 +88,45 @@ public CompletableFuture delete(String id) { @Override @Async - public CompletableFuture> findByPriceLessThan(int price) { + public CompletableFuture> findByPriceLessThan(int price) { List result = subscriptionBoxRepository.findByPriceLessThan(price); - return CompletableFuture.completedFuture(result); + List dtoResult = result.stream() + .map(DTOMapper::convertModelToDto) + .collect(Collectors.toList()); + return CompletableFuture.completedFuture(dtoResult); } @Override @Async - public CompletableFuture> findByPriceGreaterThan(int price) { + public CompletableFuture> findByPriceGreaterThan(int price) { List result = subscriptionBoxRepository.findByPriceGreaterThan(price); - return CompletableFuture.completedFuture(result); + List dtoResult = result.stream() + .map(DTOMapper::convertModelToDto) + .collect(Collectors.toList()); + return CompletableFuture.completedFuture(dtoResult); } @Override @Async - public CompletableFuture> findByPriceEquals(int price) { + public CompletableFuture> findByPriceEquals(int price) { List result = subscriptionBoxRepository.findByPriceEquals(price); - return CompletableFuture.completedFuture(result); + List dtoResult = result.stream() + .map(DTOMapper::convertModelToDto) + .collect(Collectors.toList()); + return CompletableFuture.completedFuture(dtoResult); } @Override @Async - public CompletableFuture>> findByName(String name) { + public CompletableFuture>> findByName(String name) { Optional> result = subscriptionBoxRepository.findByName(name); - return CompletableFuture.completedFuture(result); + Optional> dtoResult = result.map(list -> list.stream() + .map(DTOMapper::convertModelToDto) + .collect(Collectors.toList())); + return CompletableFuture.completedFuture(dtoResult); } + @Override public CompletableFuture>> findDistinctNames() { Optional> distinctNames = subscriptionBoxRepository.findDistinctNames(); @@ -100,45 +136,3 @@ public CompletableFuture>> findDistinctNames() { } -// -//@Service -//public class SubscriptionBoxServiceImpl implements SubscriptionBoxService{ -// @Autowired -// private SubscriptionBoxRepository subscriptionBoxRepository; -// -// @Override -// @Async -// public CompletableFuture -// -// @Override -// public SubscriptionBox addBox(SubscriptionBox box) { -// return subscriptionBoxRepository.addBox(box); -// } -// -// @Override -// public SubscriptionBox deleteBox(String id) { -// return subscriptionBoxRepository.deleteBox(id); -// } -// -// @Override -// public SubscriptionBox editBox(String id, SubscriptionBox box) { -// return subscriptionBoxRepository.editBox(id, box); -// } -// -// @Override -// public List viewAll() { -// return subscriptionBoxRepository.viewAll(); -// } -// -// @Override -// public String viewDetails(String boxId) { -// return subscriptionBoxRepository.viewDetails(boxId); -// } -// -// @Override -// public List filterByPrice(int price) { -// return subscriptionBoxRepository.filterByPrice(price); -// } -// -// -//} \ No newline at end of file diff --git a/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/utils/JWTUtils.java b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/utils/JWTUtils.java new file mode 100644 index 0000000..7703fd6 --- /dev/null +++ b/src/main/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/utils/JWTUtils.java @@ -0,0 +1,41 @@ +package id.ac.ui.cs.advprog.snackscription_subscriptionbox.utils; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import org.springframework.beans.factory.annotation.Value; + +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; +import java.util.function.Function; + + +@Component +public class JWTUtils { + private final SecretKey KEY; + + public JWTUtils(@Value("${JWT_SECRET}") String jwtSecret) { + byte[] keyBytes = Base64.getDecoder().decode(jwtSecret.getBytes(StandardCharsets.UTF_8)); + this.KEY = new SecretKeySpec(keyBytes, "HmacSHA256"); + } + + public String extractRole(String token) { + return extractClaims(token, claims -> claims.get("role", String.class)); + } + + private T extractClaims(String token, Function claimsTFunction){ + return claimsTFunction.apply(Jwts.parser().verifyWith(KEY).build().parseSignedClaims(token).getPayload()); + } + + public boolean isTokenValid(String token) { + return !isTokenExpired(token); + } + + public boolean isTokenExpired(String token) { + return extractClaims(token, Claims::getExpiration).before(new Date()); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.properties b/src/main/resources/application-dev.properties index 4176bef..58d8184 100644 --- a/src/main/resources/application-dev.properties +++ b/src/main/resources/application-dev.properties @@ -1,12 +1,7 @@ spring.datasource.url=jdbc:postgresql://localhost:5432/postgres -#spring.datasource.password= ${LOCAL_PASSWORD} +spring.datasource.password= ${LOCAL_PASSWORD:postgres} spring.datasource.username= postgres spring.jpa.hibernate.ddl-auto=create spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect spring.jpa.properties.hibernate.format_sql=true - - - - -spring.datasource.password= nelwanfamily2004 \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index e42b415..e791095 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,2 +1,3 @@ spring.application.name=snackscription_subscriptionbox -spring.profiles.active=${PRODUCTION:dev} \ No newline at end of file +spring.profiles.active=${PRODUCTION:dev} +management.endpoints.web.exposure.include=* \ No newline at end of file diff --git a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxControllerTest.java b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxControllerTest.java index 303cc0a..bcf2c34 100644 --- a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxControllerTest.java +++ b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/controller/SubscriptionBoxControllerTest.java @@ -1,7 +1,11 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.controller; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.DTOMapper; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.SubscriptionBoxDTO; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.Item; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.SubscriptionBox; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.service.SubscriptionBoxService; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.utils.JWTUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; @@ -9,10 +13,7 @@ import org.mockito.MockitoAnnotations; import org.springframework.http.ResponseEntity; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.UUID; +import java.util.*; import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.*; @@ -26,23 +27,37 @@ class SubscriptionBoxControllerTest { @InjectMocks private SubscriptionBoxController subscriptionBoxController; - + @Mock + private JWTUtils jwtUtils; private SubscriptionBox subscriptionBox; + private SubscriptionBoxDTO subscriptionBoxDTO; + private final String validToken = "eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjoiQURNSU4iLCJzdWIiOiJhZG1pbkBnbWFpbC5jb20iLCJpYXQiOjE3MTY2MjAzOTksImV4cCI6MTcxNjcwNjc5OX0.dFmE18NL6H1my8Dki1Lp4DlwGIRbTTpgj3qUFKBoBoo"; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); - subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null); + // Create items + Item item1 = new Item("1", "Item 1", 10); + Item item2 = new Item("2", "Item 2", 20); + List items = Arrays.asList(item1, item2); + + // Create subscription box with items + subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, items, "this is good yas"); subscriptionBox.setId(UUID.randomUUID().toString()); + + // Convert to DTO + subscriptionBoxDTO = DTOMapper.convertModelToDto(subscriptionBox); + when(jwtUtils.isTokenValid(validToken)).thenReturn(true); + when(jwtUtils.extractRole(any(String.class))).thenReturn("admin"); } @Test - void testCreateSubscriptionBox() { - when(subscriptionBoxService.save(any(SubscriptionBox.class))) + void testCreateSubscriptionBox_HappyPath()throws IllegalAccessException { + when(subscriptionBoxService.save(any(SubscriptionBoxDTO.class))) .thenReturn(CompletableFuture.completedFuture(subscriptionBox)); - CompletableFuture> result = subscriptionBoxController.createSubscriptionBox(subscriptionBox); + CompletableFuture> result = subscriptionBoxController.createSubscriptionBox(validToken, subscriptionBoxDTO); assertNotNull(result); assertTrue(result.isDone()); @@ -50,25 +65,40 @@ void testCreateSubscriptionBox() { } @Test - void testFindAll() { - List subscriptionBoxes = Arrays.asList(subscriptionBox); + void testCreateSubscriptionBox_UnhappyPath()throws IllegalAccessException { + when(subscriptionBoxService.save(any(SubscriptionBoxDTO.class))) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Error saving subscription box"))); + + CompletableFuture> result = subscriptionBoxController.createSubscriptionBox(validToken, subscriptionBoxDTO); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.badRequest().build(), result.join()); + } + + @Test + void testFindAll_HappyPath() throws IllegalAccessException { + List subscriptionBoxes = Collections.singletonList(subscriptionBox); when(subscriptionBoxService.findAll()) .thenReturn(CompletableFuture.completedFuture(subscriptionBoxes)); - CompletableFuture>> result = subscriptionBoxController.findAll(); + CompletableFuture>> result = subscriptionBoxController.findAll(validToken); assertNotNull(result); assertTrue(result.isDone()); assertEquals(ResponseEntity.ok(subscriptionBoxes), result.join()); } + @Test - void testFindById() { - when(subscriptionBoxService.findById(subscriptionBox.getId())) - .thenReturn(CompletableFuture.completedFuture(Optional.of(subscriptionBox))); + void testUpdateSubscriptionBox_HappyPath() throws IllegalAccessException { + when(subscriptionBoxService.findById(subscriptionBoxDTO.getId())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(subscriptionBoxDTO))); + when(subscriptionBoxService.update(any(SubscriptionBoxDTO.class))) + .thenReturn(CompletableFuture.completedFuture(subscriptionBox)); - CompletableFuture> result = subscriptionBoxController.findById(subscriptionBox.getId()); + CompletableFuture> result = subscriptionBoxController.updateSubscriptionBox(validToken, subscriptionBoxDTO); assertNotNull(result); assertTrue(result.isDone()); @@ -76,10 +106,10 @@ void testFindById() { } @Test - void testFindByIdInvalidId() { - String invalidId = "invalid_id"; + void testUpdateSubscriptionBox_UnhappyPath() throws IllegalAccessException { + subscriptionBoxDTO.setId(null); - CompletableFuture> result = subscriptionBoxController.findById(invalidId); + CompletableFuture> result = subscriptionBoxController.updateSubscriptionBox(validToken, subscriptionBoxDTO); assertNotNull(result); assertTrue(result.isDone()); @@ -87,25 +117,45 @@ void testFindByIdInvalidId() { } @Test - void testUpdateSubscriptionBox() { + void testUpdateSubscriptionBox_NotFound()throws IllegalAccessException { + when(subscriptionBoxService.findById(subscriptionBoxDTO.getId())) + .thenReturn(CompletableFuture.completedFuture(Optional.empty())); + + CompletableFuture> result = subscriptionBoxController.updateSubscriptionBox(validToken, subscriptionBoxDTO); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.notFound().build(), result.join()); + } + + @Test + void testFindById_HappyPath() throws IllegalAccessException { when(subscriptionBoxService.findById(subscriptionBox.getId())) - .thenReturn(CompletableFuture.completedFuture(Optional.of(subscriptionBox))); - when(subscriptionBoxService.update(any(SubscriptionBox.class))) - .thenReturn(CompletableFuture.completedFuture(subscriptionBox)); + .thenReturn(CompletableFuture.completedFuture(Optional.of(subscriptionBoxDTO))); - CompletableFuture> result = subscriptionBoxController.updateSubscriptionBox(subscriptionBox); + CompletableFuture> result = subscriptionBoxController.findById(validToken, subscriptionBox.getId()); assertNotNull(result); assertTrue(result.isDone()); - assertEquals(ResponseEntity.ok(subscriptionBox), result.join()); + assertEquals(ResponseEntity.ok(subscriptionBoxDTO), result.join()); + } + + @Test + void testFindById_UnhappyPath()throws IllegalAccessException { + String invalidId = "invalid-uuid"; + CompletableFuture> result = subscriptionBoxController.findById(validToken, invalidId); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.badRequest().build(), result.join()); } @Test - void testDeleteSubscriptionBox() { + void testDeleteSubscriptionBox_HappyPath() throws IllegalAccessException { when(subscriptionBoxService.delete(subscriptionBox.getId())) .thenReturn(CompletableFuture.completedFuture(null)); - CompletableFuture> result = subscriptionBoxController.deleteSubscriptionBox(subscriptionBox.getId()); + CompletableFuture> result = subscriptionBoxController.deleteSubscriptionBox(validToken, subscriptionBox.getId()); assertNotNull(result); assertTrue(result.isDone()); @@ -113,72 +163,137 @@ void testDeleteSubscriptionBox() { } @Test - void testFindByPriceLessThan() { - List subscriptionBoxes = Arrays.asList(subscriptionBox); + void testDeleteSubscriptionBox_UnhappyPath() throws IllegalAccessException { + CompletableFuture> expectedResult = CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); + + CompletableFuture> result = subscriptionBoxController.deleteSubscriptionBox(validToken, "invalid Id"); + + assertTrue(result.isDone()); + assertEquals(expectedResult.join(), result.join()); + } + + @Test + void testFindByPriceLessThan_HappyPath() throws IllegalAccessException { + List expectedDTOs = Collections.singletonList(subscriptionBoxDTO); when(subscriptionBoxService.findByPriceLessThan(150)) - .thenReturn(CompletableFuture.completedFuture(subscriptionBoxes)); + .thenReturn(CompletableFuture.completedFuture(expectedDTOs)); - CompletableFuture>> result = subscriptionBoxController.findByPriceLessThan(150); + CompletableFuture>> result = subscriptionBoxController.findByPriceLessThan(validToken, 150); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.ok(expectedDTOs), result.join()); + } + + @Test + void testFindByPriceLessThan_UnhappyPath() throws IllegalAccessException { + when(subscriptionBoxService.findByPriceLessThan(150)) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Error finding by price less than"))); + + CompletableFuture>> result = subscriptionBoxController.findByPriceLessThan(validToken, 150); assertNotNull(result); assertTrue(result.isDone()); - assertEquals(ResponseEntity.ok(subscriptionBoxes), result.join()); } @Test - void testFindByPriceGreaterThan() { - List subscriptionBoxes = Arrays.asList(subscriptionBox); + void testFindByPriceGreaterThan_HappyPath() throws IllegalAccessException { + List expectedDTOs = Collections.singletonList(subscriptionBoxDTO); + when(subscriptionBoxService.findByPriceGreaterThan(50)) + .thenReturn(CompletableFuture.completedFuture(expectedDTOs)); + + CompletableFuture>> result = subscriptionBoxController.findByPriceGreaterThan(validToken, 50); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.ok(expectedDTOs), result.join()); + } + @Test + void testFindByPriceGreaterThan_UnhappyPath() throws IllegalAccessException { when(subscriptionBoxService.findByPriceGreaterThan(50)) - .thenReturn(CompletableFuture.completedFuture(subscriptionBoxes)); + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Error finding by price greater than"))); - CompletableFuture>> result = subscriptionBoxController.findByPriceGreaterThan(50); + CompletableFuture>> result = subscriptionBoxController.findByPriceGreaterThan(validToken, 50); assertNotNull(result); assertTrue(result.isDone()); - assertEquals(ResponseEntity.ok(subscriptionBoxes), result.join()); } @Test - void testFindByPriceEquals() { - List subscriptionBoxes = Arrays.asList(subscriptionBox); + void testFindByPriceEquals_HappyPath() throws IllegalAccessException { + List expectedDTOs = Collections.singletonList(subscriptionBoxDTO); + when(subscriptionBoxService.findByPriceEquals(100)) + .thenReturn(CompletableFuture.completedFuture(expectedDTOs)); + + CompletableFuture>> result = subscriptionBoxController.findByPriceEquals(validToken, 100); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.ok(expectedDTOs), result.join()); + } + @Test + void testFindByPriceEquals_UnhappyPath() throws IllegalAccessException { when(subscriptionBoxService.findByPriceEquals(100)) - .thenReturn(CompletableFuture.completedFuture(subscriptionBoxes)); + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Error finding by price equals"))); - CompletableFuture>> result = subscriptionBoxController.findByPriceEquals(100); + CompletableFuture>> result = subscriptionBoxController.findByPriceEquals(validToken, 100); assertNotNull(result); assertTrue(result.isDone()); - assertEquals(ResponseEntity.ok(subscriptionBoxes), result.join()); + } @Test - void testFindByName() { - List subscriptionBoxes = Arrays.asList(subscriptionBox); + void testFindByName_HappyPath() throws IllegalAccessException { + List expectedDTOs = Collections.singletonList(subscriptionBoxDTO); + String nameURL = "Basic".replaceAll(" ", "-"); + when(subscriptionBoxService.findByName(subscriptionBox.getName())) + .thenReturn(CompletableFuture.completedFuture(Optional.of(expectedDTOs))); + + CompletableFuture>>> result = subscriptionBoxController.findByName(validToken, nameURL); + + assertNotNull(result); + assertTrue(result.isDone()); + assertEquals(ResponseEntity.ok(Optional.of(expectedDTOs)), result.join()); + } - when(subscriptionBoxService.findByName("Basic")) - .thenReturn(CompletableFuture.completedFuture(Optional.of(subscriptionBoxes))); + @Test + void testFindByName_UnhappyPath() throws IllegalAccessException { + String nameURL = "Basic".replaceAll(" ", "-"); + when(subscriptionBoxService.findByName(subscriptionBox.getName())) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Error finding by name"))); - CompletableFuture>>> result = subscriptionBoxController.findByName("Basic"); + CompletableFuture>>> result = subscriptionBoxController.findByName(validToken, nameURL); assertNotNull(result); assertTrue(result.isDone()); - assertEquals(ResponseEntity.ok(Optional.of(subscriptionBoxes)), result.join()); } @Test - void testFindDistinctNames() { + void testFindDistinctNames_HappyPath() throws IllegalAccessException { List names = Arrays.asList("Basic", "Premium"); when(subscriptionBoxService.findDistinctNames()) .thenReturn(CompletableFuture.completedFuture(Optional.of(names))); - CompletableFuture>>> result = subscriptionBoxController.findDistinctNames(); + CompletableFuture>>> result = subscriptionBoxController.findDistinctNames(validToken); assertNotNull(result); assertTrue(result.isDone()); assertEquals(ResponseEntity.ok(Optional.of(names)), result.join()); } + + @Test + void testFindDistinctNames_UnhappyPath() throws IllegalAccessException { + when(subscriptionBoxService.findDistinctNames()) + .thenReturn(CompletableFuture.failedFuture(new RuntimeException("Error finding distinct names"))); + + CompletableFuture>>> result = subscriptionBoxController.findDistinctNames(validToken); + + assertNotNull(result); + assertTrue(result.isDone()); + } } diff --git a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactoryTest.java b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactoryTest.java index a8bea7e..3276de2 100644 --- a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactoryTest.java +++ b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/factory/SubscriptionBoxFactoryTest.java @@ -28,10 +28,10 @@ void testCreateSubscriptionBoxWithParameters() { items.add(new Item()); // Assuming you have a constructor in Item class like this items.add(new Item()); - SubscriptionBox subscriptionBox = subscriptionBoxFactory.create("1", "Deluxe Box", "Monthly", 150, items); + SubscriptionBox subscriptionBox = subscriptionBoxFactory.create("1", "Deluxe Box", "MONTHLY", 150, items, "this is good yas"); assertEquals("Deluxe Box", subscriptionBox.getName()); - assertEquals("Monthly", subscriptionBox.getType()); + assertEquals("MONTHLY", subscriptionBox.getType()); assertEquals(150, subscriptionBox.getPrice()); assertNotNull(subscriptionBox.getItems()); assertEquals(2, subscriptionBox.getItems().size()); // Check that two items were added diff --git a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBoxTest.java b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBoxTest.java index 205f22d..ef6d1b8 100644 --- a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBoxTest.java +++ b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/model/SubscriptionBoxTest.java @@ -1,7 +1,5 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.model; - -import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.SubscriptionBoxDTO; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -9,10 +7,10 @@ import java.util.ArrayList; import java.util.List; - public class SubscriptionBoxTest { List items; SubscriptionBox subscriptionBox; + @BeforeEach public void setUp() { this.subscriptionBox = new SubscriptionBox(); @@ -20,9 +18,10 @@ public void setUp() { this.subscriptionBox.setName("BOX1"); this.subscriptionBox.setType("MONTHLY"); this.subscriptionBox.setPrice(100000); + this.subscriptionBox.setDescription("Basic monthly subscription box"); + this.items = new ArrayList<>(); Item item1 = new Item(); - items.add(item1); Item item2 = new Item(); items.add(item2); @@ -45,20 +44,19 @@ public void testGetType() { } @Test - public void testCreateInvalidType(){ + public void testCreateInvalidType() { assertThrows(IllegalArgumentException.class, () -> { - SubscriptionBox subscriptionBoxTest = new SubscriptionBox(subscriptionBox.getName(), "Daily", subscriptionBox.getPrice(), subscriptionBox.getItems()); + SubscriptionBox subscriptionBoxTest = new SubscriptionBox("BOX2", "Daily", 100000, null, "Daily subscription box"); }); - } @Test - public void testInvalidPrice(){ + public void testInvalidPrice() { assertThrows(IllegalArgumentException.class, () -> { - subscriptionBox.setPrice(-1); + subscriptionBox.setPrice(-1); }); - } + @Test public void testGetPrice() { assertEquals(100000, subscriptionBox.getPrice()); @@ -68,4 +66,15 @@ public void testGetPrice() { public void testGetItems() { assertEquals(2, subscriptionBox.getItems().size()); } -} \ No newline at end of file + + @Test + public void testGetDescription() { + assertEquals("Basic monthly subscription box", subscriptionBox.getDescription()); + } + + @Test + public void testSetDescription() { + subscriptionBox.setDescription("Updated description"); + assertEquals("Updated description", subscriptionBox.getDescription()); + } +} diff --git a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepositoryTest.java b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepositoryTest.java index 42067ef..273c9e8 100644 --- a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepositoryTest.java +++ b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/repository/SubscriptionBoxRepositoryTest.java @@ -10,6 +10,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Optional; @@ -27,16 +28,33 @@ class SubscriptionBoxRepositoryTest { @Test void testSave() { - SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null); + SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); + + // Mock the behavior for hasThreeSimilarNames + TypedQuery mockTypedQueryForSimilarNames = mock(TypedQuery.class); + when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb WHERE LOWER(sb.name) LIKE LOWER(:name)", SubscriptionBox.class)) + .thenReturn(mockTypedQueryForSimilarNames); + when(mockTypedQueryForSimilarNames.setParameter("name", "%Basic%")).thenReturn(mockTypedQueryForSimilarNames); + when(mockTypedQueryForSimilarNames.getResultList()).thenReturn(Collections.emptyList()); // No similar names + + // Mock the behavior for existsByNameAndType + TypedQuery mockTypedQueryForNameAndType = mock(TypedQuery.class); + when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb WHERE LOWER(sb.name) = LOWER(:name) AND LOWER(sb.type) = LOWER(:type)", SubscriptionBox.class)) + .thenReturn(mockTypedQueryForNameAndType); + when(mockTypedQueryForNameAndType.setParameter("name", "Basic")).thenReturn(mockTypedQueryForNameAndType); + when(mockTypedQueryForNameAndType.setParameter("type", "MONTHLY")).thenReturn(mockTypedQueryForNameAndType); // Match exact case + when(mockTypedQueryForNameAndType.getResultList()).thenReturn(Collections.emptyList()); // No existing box with same name and type + SubscriptionBox savedSubscriptionBox = subscriptionBoxRepository.save(subscriptionBox); assertEquals(subscriptionBox, savedSubscriptionBox); verify(entityManager, times(1)).persist(subscriptionBox); } + @Test void testFindAll() { - SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null); - SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null); + SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); + SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null, "Premium monthly subscription box"); TypedQuery query = mock(TypedQuery.class); when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb", SubscriptionBox.class)).thenReturn(query); @@ -51,7 +69,7 @@ void testFindAll() { @Test void testFindById() { - SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null); + SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); subscriptionBox.setId("1"); when(entityManager.find(SubscriptionBox.class, "1")).thenReturn(subscriptionBox); @@ -73,7 +91,7 @@ void testFindByIdSubscriptionNotFound() { @Test void testUpdate() { - SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null); + SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); when(entityManager.merge(subscriptionBox)).thenReturn(subscriptionBox); @@ -85,7 +103,7 @@ void testUpdate() { @Test void testDelete() { - SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null); + SubscriptionBox subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); subscriptionBox.setId("1"); when(entityManager.find(SubscriptionBox.class, "1")).thenReturn(subscriptionBox); @@ -109,8 +127,8 @@ void testDeleteSubscriptionNotFound() { @Test void testFindByPriceLessThan() { - SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null); - SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null); + SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); + SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null, "Premium monthly subscription box"); TypedQuery query = mock(TypedQuery.class); when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb WHERE sb.price < :price", SubscriptionBox.class)).thenReturn(query); @@ -128,8 +146,8 @@ void testFindByPriceLessThan() { @Test void testFindByPriceGreaterThan() { - SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null); - SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null); + SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); + SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null, "Premium monthly subscription box"); TypedQuery query = mock(TypedQuery.class); when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb WHERE sb.price > :price", SubscriptionBox.class)).thenReturn(query); @@ -147,8 +165,8 @@ void testFindByPriceGreaterThan() { @Test void testFindByPriceEquals() { - SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null); - SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null); + SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic", "Monthly", 100, null, "Basic monthly subscription box"); + SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium", "Monthly", 200, null, "Premium monthly subscription box"); TypedQuery query = mock(TypedQuery.class); when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb WHERE sb.price = :price", SubscriptionBox.class)).thenReturn(query); @@ -166,8 +184,8 @@ void testFindByPriceEquals() { @Test void testFindByName() { - SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic Box", "Monthly", 100, null); - SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium Box", "Monthly", 200, null); + SubscriptionBox subscriptionBox1 = new SubscriptionBox("Basic Box", "Monthly", 100, null, "Basic monthly subscription box"); + SubscriptionBox subscriptionBox2 = new SubscriptionBox("Premium Box", "Monthly", 200, null, "Premium monthly subscription box"); TypedQuery query = mock(TypedQuery.class); when(entityManager.createQuery("SELECT sb FROM SubscriptionBox sb WHERE LOWER(sb.name) LIKE LOWER(:name)", SubscriptionBox.class)).thenReturn(query); diff --git a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImplTest.java b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImplTest.java index aa05b32..be1a4e9 100644 --- a/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImplTest.java +++ b/src/test/java/id/ac/ui/cs/advprog/snackscription_subscriptionbox/service/SubscriptionBoxServiceImplTest.java @@ -1,4 +1,6 @@ package id.ac.ui.cs.advprog.snackscription_subscriptionbox.service; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.DTOMapper; +import id.ac.ui.cs.advprog.snackscription_subscriptionbox.dto.SubscriptionBoxDTO; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.model.SubscriptionBox; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.repository.SubscriptionBoxRepository; import id.ac.ui.cs.advprog.snackscription_subscriptionbox.service.SubscriptionBoxServiceImpl; @@ -28,35 +30,16 @@ class SubscriptionBoxServiceImplTest { private SubscriptionBoxServiceImpl subscriptionBoxService; private SubscriptionBox subscriptionBox; + private SubscriptionBoxDTO subscriptionBoxDTO; @BeforeEach void setUp() { - subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null); + subscriptionBox = new SubscriptionBox("Basic", "Monthly", 100, null, "this is good yas"); subscriptionBox.setId("1"); - } - - @Test - void testSave() throws ExecutionException, InterruptedException { - when(subscriptionBoxRepository.save(subscriptionBox)).thenReturn(subscriptionBox); - - CompletableFuture future = subscriptionBoxService.save(subscriptionBox); - SubscriptionBox result = future.get(); - assertEquals(subscriptionBox, result); - verify(subscriptionBoxRepository, times(1)).save(subscriptionBox); } - @Test - void testFindById() throws ExecutionException, InterruptedException { - when(subscriptionBoxRepository.findById("1")).thenReturn(Optional.of(subscriptionBox)); - CompletableFuture> future = subscriptionBoxService.findById("1"); - Optional result = future.get(); - - assertTrue(result.isPresent()); - assertEquals(subscriptionBox, result.get()); - verify(subscriptionBoxRepository, times(1)).findById("1"); - } @Test void testFindByIdInvalidId() { @@ -79,16 +62,6 @@ void testFindAll() throws ExecutionException, InterruptedException { verify(subscriptionBoxRepository, times(1)).findAll(); } - @Test - void testUpdate() throws ExecutionException, InterruptedException { - when(subscriptionBoxRepository.update(subscriptionBox)).thenReturn(subscriptionBox); - - CompletableFuture future = subscriptionBoxService.update(subscriptionBox); - SubscriptionBox result = future.get(); - - assertEquals(subscriptionBox, result); - verify(subscriptionBoxRepository, times(1)).update(subscriptionBox); - } @Test void testUpdateInvalidBox() { @@ -110,14 +83,6 @@ void testDelete() throws ExecutionException, InterruptedException { verify(subscriptionBoxRepository, times(1)).delete("1"); } - @Test - void testDeleteInvalidId() { - assertThrows(IllegalArgumentException.class, () -> { - subscriptionBoxService.delete("").get(); - }); - verify(subscriptionBoxRepository, never()).delete(anyString()); - } - @Test void testDeleteSubscriptionNotFound() { when(subscriptionBoxRepository.findById("1")).thenReturn(Optional.empty()); @@ -130,58 +95,7 @@ void testDeleteSubscriptionNotFound() { verify(subscriptionBoxRepository, never()).delete(anyString()); } - @Test - void testFindByPriceLessThan() throws ExecutionException, InterruptedException { - List subscriptionBoxes = Arrays.asList(subscriptionBox); - when(subscriptionBoxRepository.findByPriceLessThan(150)).thenReturn(subscriptionBoxes); - - CompletableFuture> future = subscriptionBoxService.findByPriceLessThan(150); - List result = future.get(); - - assertEquals(1, result.size()); - assertEquals(subscriptionBox, result.get(0)); - verify(subscriptionBoxRepository, times(1)).findByPriceLessThan(150); - } - - @Test - void testFindByPriceGreaterThan() throws ExecutionException, InterruptedException { - List subscriptionBoxes = Arrays.asList(subscriptionBox); - when(subscriptionBoxRepository.findByPriceGreaterThan(50)).thenReturn(subscriptionBoxes); - - CompletableFuture> future = subscriptionBoxService.findByPriceGreaterThan(50); - List result = future.get(); - - assertEquals(1, result.size()); - assertEquals(subscriptionBox, result.get(0)); - verify(subscriptionBoxRepository, times(1)).findByPriceGreaterThan(50); - } - - @Test - void testFindByPriceEquals() throws ExecutionException, InterruptedException { - List subscriptionBoxes = Arrays.asList(subscriptionBox); - when(subscriptionBoxRepository.findByPriceEquals(100)).thenReturn(subscriptionBoxes); - - CompletableFuture> future = subscriptionBoxService.findByPriceEquals(100); - List result = future.get(); - - assertEquals(1, result.size()); - assertEquals(subscriptionBox, result.get(0)); - verify(subscriptionBoxRepository, times(1)).findByPriceEquals(100); - } - - @Test - void testFindByName() throws ExecutionException, InterruptedException { - List subscriptionBoxes = Arrays.asList(subscriptionBox); - when(subscriptionBoxRepository.findByName("Basic")).thenReturn(Optional.of(subscriptionBoxes)); - - CompletableFuture>> future = subscriptionBoxService.findByName("Basic"); - Optional> result = future.get(); - assertTrue(result.isPresent()); - assertEquals(1, result.get().size()); - assertEquals(subscriptionBox, result.get().get(0)); - verify(subscriptionBoxRepository, times(1)).findByName("Basic"); - } @Test void testFindDistinctNames() throws ExecutionException, InterruptedException {