From 2fe028f271161f01443f14378998b0358e76c6ec Mon Sep 17 00:00:00 2001 From: sdikyarts Date: Sun, 26 May 2024 04:46:25 +0700 Subject: [PATCH] Grafana etc. init set up + refactors --- .github/workflows/ci.yml | 1 + .github/workflows/sonarcloud.yml | 75 +++++++++++++++++++ .gitignore | 1 + build.gradle.kts | 1 + .../controller/AdminController.java | 20 ++++- .../service/AdminServiceImpl.java | 3 + src/main/resources/application.properties | 2 +- .../config/AsyncConfigurationTest.java | 27 +++++++ .../controller/AdminControllerTest.java | 58 +++++++++++++- .../service/AdminServiceImplTest.java | 53 +++++++++++-- 10 files changed, 230 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/sonarcloud.yml create mode 100644 src/test/java/snackscription/subscriptionadmin/config/AsyncConfigurationTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4c0ab85..c7b3736 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,6 +4,7 @@ on: push: branches: - main + - staging jobs: build: diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml new file mode 100644 index 0000000..9889b3b --- /dev/null +++ b/.github/workflows/sonarcloud.yml @@ -0,0 +1,75 @@ +name: SonarCloud + +on: + push: + branches: + - staging + - main + pull_request: + types: [opened, synchronize, reopened] + +jobs: + build: + name: Build, analyze, and test + runs-on: ubuntu-latest + + services: + postgres: + image: postgres:latest + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: subscription-admin + + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: "temurin" + java-version: "21" + cache: "gradle" + - name: Cache SonarCloud packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Gradle packages + uses: actions/cache@v3 + with: + path: ~/.gradle/caches + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} + restore-keys: ${{ runner.os }}-gradle + - name: Wait for PostgreSQL + run: | + for i in {1..30}; do + if pg_isready -h localhost -p 5432 -U postgres; then + echo "PostgreSQL is up and running" + break + fi + echo "Waiting for PostgreSQL..." + sleep 1 + done + if ! pg_isready -h localhost -p 5432 -U postgres; then + echo "PostgreSQL failed to start" && exit 1 + fi + - name: Build and analyze + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + SPRING_DATASOURCE_URL: jdbc:postgresql://localhost:5432/subscription-admin + SPRING_DATASOURCE_USERNAME: postgres + SPRING_DATASOURCE_PASSWORD: postgres + run: | + chmod +x ./gradlew + ./gradlew build jacocoTestReport sonarqube --info \ No newline at end of file diff --git a/.gitignore b/.gitignore index c2065bc..a93ec83 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ HELP.md +.env .gradle build/ !gradle/wrapper/gradle-wrapper.jar diff --git a/build.gradle.kts b/build.gradle.kts index 32f129f..8e9d3b1 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -43,6 +43,7 @@ dependencies { testImplementation("org.junit.jupiter:junit-jupiter-engine:$junitJupiterVersion") runtimeOnly("io.micrometer:micrometer-registry-prometheus:1.12.5") implementation("org.springframework.boot:spring-boot-starter-actuator:3.2.5") + implementation("me.paulschwarz:spring-dotenv:4.0.0") } tasks.register("unitTest") { diff --git a/src/main/java/snackscription/subscriptionadmin/controller/AdminController.java b/src/main/java/snackscription/subscriptionadmin/controller/AdminController.java index de7d02e..52b8aa3 100644 --- a/src/main/java/snackscription/subscriptionadmin/controller/AdminController.java +++ b/src/main/java/snackscription/subscriptionadmin/controller/AdminController.java @@ -21,6 +21,11 @@ public AdminController(AdminService adminService) { this.adminService = adminService; } + @GetMapping("") + public ResponseEntity home() { + return ResponseEntity.ok("Snackscription - Admin Subscription Management API"); + } + @PostMapping("/create") public CompletableFuture> create(@RequestBody AdminDTO adminDTO) { return adminService.create(adminDTO).thenApply(ResponseEntity::ok) @@ -50,7 +55,12 @@ public CompletableFuture> update(@RequestBody return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); } - return adminService.update(adminDTO).thenApply(ResponseEntity::ok) + CompletableFuture updatedSubscription = adminService.update(adminDTO); + if(updatedSubscription == null) { + return CompletableFuture.completedFuture(ResponseEntity.notFound().build()); + } + + return updatedSubscription.thenApply(ResponseEntity::ok) .exceptionally(ex -> ResponseEntity.notFound().build()); } @@ -61,7 +71,11 @@ public CompletableFuture> delete(@PathVariable String sub } catch (IllegalArgumentException e) { return CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); } - return adminService.delete(subscriptionId).thenApply(deleted -> ResponseEntity.ok("DELETE SUCCESS")) - .exceptionally(ex -> ResponseEntity.notFound().build()); + try { + return adminService.delete(subscriptionId).thenApply(deleted -> ResponseEntity.ok("DELETE SUCCESS")) + .exceptionally(ex -> ResponseEntity.notFound().build()); + } catch (IllegalArgumentException e) { + return CompletableFuture.completedFuture(ResponseEntity.notFound().build()); + } } } \ No newline at end of file diff --git a/src/main/java/snackscription/subscriptionadmin/service/AdminServiceImpl.java b/src/main/java/snackscription/subscriptionadmin/service/AdminServiceImpl.java index 8e4d0eb..aa45c8f 100644 --- a/src/main/java/snackscription/subscriptionadmin/service/AdminServiceImpl.java +++ b/src/main/java/snackscription/subscriptionadmin/service/AdminServiceImpl.java @@ -23,6 +23,9 @@ public class AdminServiceImpl implements AdminService{ @Override @Async public CompletableFuture create(AdminDTO adminDTO) { + if (adminDTO == null || adminDTO.getSubscriptionId() == null) { + throw new IllegalArgumentException("AdminDTO cannot be null and must have a valid subscriptionId"); + } AdminSubscription adminSubscription = DTOMapper.convertDTOtoModel(adminDTO); return CompletableFuture.completedFuture(adminRepository.create(adminSubscription)); } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 0bc5585..01705af 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,3 @@ spring.application.name=subscription-admin -spring.profiles.active=${PRODUCTION:dev} +spring.profiles.active=${PRODUCTION:prod} management.endpoints.web.exposure.include=* \ No newline at end of file diff --git a/src/test/java/snackscription/subscriptionadmin/config/AsyncConfigurationTest.java b/src/test/java/snackscription/subscriptionadmin/config/AsyncConfigurationTest.java new file mode 100644 index 0000000..6762f37 --- /dev/null +++ b/src/test/java/snackscription/subscriptionadmin/config/AsyncConfigurationTest.java @@ -0,0 +1,27 @@ +package snackscription.subscriptionadmin.config; + +import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +class AsyncConfigTest { + + @Test + void testAsyncExecutor() { + + AsyncConfiguration asyncConfiguration = new AsyncConfiguration(); + + AsyncTaskExecutor executor = asyncConfiguration.getAsyncExecutor(); + + assertNotNull(executor); + + assertEquals(5, ((ThreadPoolTaskExecutor) executor).getCorePoolSize()); + assertEquals(10, ((ThreadPoolTaskExecutor) executor).getMaxPoolSize()); + assertEquals(100, ((ThreadPoolTaskExecutor) executor).getQueueCapacity()); + assertEquals("Async-Executor-", ((ThreadPoolTaskExecutor) executor).getThreadNamePrefix()); + } +} \ No newline at end of file diff --git a/src/test/java/snackscription/subscriptionadmin/controller/AdminControllerTest.java b/src/test/java/snackscription/subscriptionadmin/controller/AdminControllerTest.java index 1468c42..a0c2885 100644 --- a/src/test/java/snackscription/subscriptionadmin/controller/AdminControllerTest.java +++ b/src/test/java/snackscription/subscriptionadmin/controller/AdminControllerTest.java @@ -11,8 +11,8 @@ import java.util.Collections; import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; +import java.util.UUID; +import java.util.concurrent.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.Mockito.*; @@ -49,6 +49,13 @@ void setUp(){ adminSubscription.setSubscriptionStatus("PENDING"); } + @Test + void testHome(){ + ResponseEntity response = adminController.home(); + assertNotNull(response); + assertEquals(ResponseEntity.ok("Snackscription - Admin Subscription Management API"), response); + } + @Test void testCreate(){ when(adminService.create(adminDTO)).thenReturn(CompletableFuture.completedFuture(adminSubscription)); @@ -100,4 +107,51 @@ void testDelete(){ assertNotNull(response); assertEquals(ResponseEntity.ok("DELETE SUCCESS"), response.join()); } + + @Test + void testUpdateInvalidSubscriptionId(){ + AdminDTO adminDTO = new AdminDTO(); + + CompletableFuture> expectedResponse = CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); + CompletableFuture> response = adminController.update(adminDTO); + + assertTrue(response.isDone()); + assertEquals(expectedResponse.join(), response.join()); + } + + @Test + void testUpdateNonexistentSubscription(){ + AdminDTO adminDTO = new AdminDTO(); + adminDTO.setSubscriptionId(UUID.randomUUID().toString()); + CompletableFuture> expectedResponse = CompletableFuture.completedFuture(ResponseEntity.notFound().build()); + + when(adminService.findById(adminDTO.getSubscriptionId())).thenReturn(CompletableFuture.completedFuture(null)); + + CompletableFuture> response = adminController.update(adminDTO); + + assertTrue(response.isDone()); + assertEquals(expectedResponse.join(), response.join()); + } + + @Test + void testDeleteInvalidSubscriptionId(){ + CompletableFuture> expectedResponse = CompletableFuture.completedFuture(ResponseEntity.badRequest().build()); + CompletableFuture> response = adminController.delete("invalid-id"); + + assertTrue(response.isDone()); + assertEquals(expectedResponse.join(), response.join()); + } + + @Test + void testDeleteNonexistentSubscription(){ + String validUUID = UUID.randomUUID().toString(); + CompletableFuture> expectedResponse = CompletableFuture.completedFuture(ResponseEntity.notFound().build()); + + when(adminService.delete(validUUID)).thenThrow(new IllegalArgumentException("Subscription not found")); + + CompletableFuture> response = adminController.delete(validUUID); + + assertTrue(response.isDone()); + assertEquals(expectedResponse.join(), response.join()); + } } diff --git a/src/test/java/snackscription/subscriptionadmin/service/AdminServiceImplTest.java b/src/test/java/snackscription/subscriptionadmin/service/AdminServiceImplTest.java index e96829f..6ff226d 100644 --- a/src/test/java/snackscription/subscriptionadmin/service/AdminServiceImplTest.java +++ b/src/test/java/snackscription/subscriptionadmin/service/AdminServiceImplTest.java @@ -10,8 +10,6 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; -import org.springframework.http.ResponseEntity; -import snackscription.subscriptionadmin.controller.AdminController; import snackscription.subscriptionadmin.dto.AdminDTO; import snackscription.subscriptionadmin.factory.AdminSubscriptionFactory; import snackscription.subscriptionadmin.model.AdminSubscription; @@ -28,14 +26,13 @@ public class AdminServiceImplTest { private AdminRepository adminRepository; @Mock - private AdminSubscriptionFactory adminSubscriptionFactory; + private AdminDTO adminDTO; @InjectMocks private AdminServiceImpl adminService; - private AdminController adminController; + @Mock private AdminSubscription adminSubscription; - private AdminDTO adminDTO; @BeforeEach void setUp() { @@ -117,4 +114,50 @@ void testDelete() { assertTrue(result.isDone()); assertNull(result.join()); } + + @Test + void findByIdNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> { + adminService.findById(null).join(); + adminService.findById("").join(); + }); + } + + @Test + void updateNull() { + assertThrows(IllegalArgumentException.class, () -> adminService.update(null).join()); + } + + @Test + void updateNonExistent() { + assertThrows(IllegalArgumentException.class, () -> { + when(adminRepository.findById(adminDTO.getSubscriptionId())).thenReturn(Optional.empty()); + adminService.update(adminDTO).join(); + }); + } + + @Test + void deleteNullOrEmpty() { + assertThrows(IllegalArgumentException.class, () -> { + adminService.delete(null).join(); + adminService.delete("").join(); + }); + } + + @Test + void deleteNonExistent() { + assertThrows(IllegalArgumentException.class, () -> { + String id = "non-existent-id"; + when(adminRepository.findById(id)).thenReturn(Optional.empty()); + adminService.delete(id).join(); + }); + } + + @Test + void createInvalid() { + assertThrows(IllegalArgumentException.class, () -> { + AdminDTO invalidDTO = new AdminDTO(); + adminService.create(invalidDTO).join(); + }); + } }