From 5f364c1f5ae927a6896af8bd1bbc07fde98fdc38 Mon Sep 17 00:00:00 2001 From: Marcel Dybek <92519157+MDybek@users.noreply.github.com> Date: Fri, 6 Dec 2024 00:22:58 +0100 Subject: [PATCH] management service tests v1 (#135) * management-service: couple new tests + small reformat * management-service: more test * management-service: fixed most of the tests, few more to go * management-service: all tests works * management-service: all tests works with watch * management-service: refactor + jacoco for test coverage * client: added cash for management-service tests in docker-compose.test.yml * management-service: new tests for utils * management-service: more tests * management-service: more tests * management-service: refactor --- docker-compose.test.yml | 10 + management-service/Dockerfile | 13 +- management-service/pom.xml | 95 +++++- .../zpi/cluster/service/ClusterService.java | 1 - .../healthcheck/HealthCheckController.java | 17 - .../zpi/metadata/service/MetadataService.java | 22 +- .../service/EmailMessagingServiceImpl.java | 1 - .../slack/service/SlackReceiverService.java | 7 + .../zpi/reports/service/ReportsService.java | 10 +- .../pl/pwr/zpi/user/service/UserService.java | 13 - .../resources/templates/email/footer.html | 2 +- .../pwr/zpi/cluster/ClusterServiceTest.groovy | 105 ++++++ .../MetadataHistoryServiceTest.groovy | 212 ++++++++++++ .../zpi/metadata/MetadataServiceTest.groovy | 176 ++++++++++ .../common/ConfidentialTextEncoderTest.groovy | 38 +++ .../discord/DiscordReceiverServiceTest.groovy | 132 ++++++++ .../email/EmailReceiverServiceTest.groovy | 119 +++++++ .../notifications/email/EmailUtilsTest.groovy | 67 ++++ .../slack/SlackNotificationServiceTest.groovy | 84 +++++ .../slack/SlackReceiverServiceTest.groovy | 190 +++++++++++ .../ReportGenerationServiceTest.groovy | 200 +++++++++++ .../reports/ReportScheduleServiceTest.groovy | 42 +++ .../zpi/reports/ReportSchedulerTest.groovy | 111 +++++++ .../pwr/zpi/reports/ReportsServiceTest.groovy | 312 ++++++++++++++++++ .../groovy/pl/pwr/zpi/utils/ClientTest.groovy | 131 ++++++++ .../pl/pwr/zpi/utils/JsonMapperTest.groovy | 60 ++++ .../zpi/MagpieMonitorApplicationTests.java | 13 - 27 files changed, 2110 insertions(+), 73 deletions(-) delete mode 100644 management-service/src/main/java/pl/pwr/zpi/healthcheck/HealthCheckController.java create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/cluster/ClusterServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataHistoryServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/notifications/common/ConfidentialTextEncoderTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/notifications/discord/DiscordReceiverServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailReceiverServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailUtilsTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackNotificationServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackReceiverServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/reports/ReportGenerationServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/reports/ReportScheduleServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/reports/ReportSchedulerTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/reports/ReportsServiceTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/utils/ClientTest.groovy create mode 100644 management-service/src/test/groovy/pl/pwr/zpi/utils/JsonMapperTest.groovy delete mode 100644 management-service/src/test/java/pl/pwr/zpi/MagpieMonitorApplicationTests.java diff --git a/docker-compose.test.yml b/docker-compose.test.yml index 597dd989..58d4bd3d 100644 --- a/docker-compose.test.yml +++ b/docker-compose.test.yml @@ -77,6 +77,15 @@ services: - LETSENCRYPT_HOST=${REPORTS_PRODUCTION_HOST} - VIRTUAL_PORT=${REPORTS_SERVICE_PORT} + management-service-test: + container_name: magpie-monitor-tests-management-service + build: + context: ./management-service + dockerfile: Dockerfile + target: tests + volumes: + - maven-repo:/root/.m2 + pod-agent: user: "0" # Elevated permission needed for bind mount container_name: magpie-monitor-pod-agent @@ -129,6 +138,7 @@ volumes: cache: es-certs: external: true + maven-repo: networks: default: diff --git a/management-service/Dockerfile b/management-service/Dockerfile index 34f0c9cc..8e58b5f6 100644 --- a/management-service/Dockerfile +++ b/management-service/Dockerfile @@ -1,13 +1,24 @@ FROM eclipse-temurin:21-jdk-jammy as builder WORKDIR /opt/app + COPY mvnw pom.xml ./ COPY .mvn/ .mvn RUN chmod +x ./mvnw && ./mvnw dependency:go-offline + COPY ./src ./src RUN ./mvnw clean install -DskipTests +FROM eclipse-temurin:21-jdk-jammy as tests +WORKDIR /opt/app + +COPY --from=builder /opt/app /opt/app + +ENTRYPOINT ["./mvnw", "test"] + FROM eclipse-temurin:21-jre-jammy WORKDIR /opt/app EXPOSE 8080 + COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar -ENTRYPOINT ["java", "-jar", "/opt/app/*.jar" ] + +ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"] diff --git a/management-service/pom.xml b/management-service/pom.xml index 7da0ec81..54393a15 100644 --- a/management-service/pom.xml +++ b/management-service/pom.xml @@ -5,7 +5,7 @@ org.springframework.boot spring-boot-starter-parent - 3.3.5 + 3.4.0 pl.pwr @@ -28,12 +28,15 @@ 21 - 3.3.5 - 1.18.34 + 3.4.0 + 1.18.36 2.6.0 0.12.6 - 0.22.0 - 1.43.1 + 0.24.0 + 1.44.2 + 4.0.24 + 2.4-M4-groovy-4.0 + 5.2.0 @@ -60,7 +63,7 @@ org.springframework.kafka spring-kafka - 3.2.4 + 3.3.0 @@ -97,13 +100,13 @@ com.google.auth google-auth-library-oauth2-http - 1.27.0 + 1.30.0 com.google.http-client google-http-client-gson - 1.45.0 + 1.45.1 @@ -176,6 +179,30 @@ commonmark ${commonmark.version} + + + + org.apache.groovy + groovy-all + ${groovy-all.version} + pom + + + + + org.spockframework + spock-core + ${spock.version} + test + + + + + org.mockito + mockito-inline + ${mockito.version} + test + @@ -184,6 +211,58 @@ org.springframework.boot spring-boot-maven-plugin + + org.codehaus.gmavenplus + gmavenplus-plugin + 4.0.1 + + + + compile + compileTests + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M4 + + false + + **/*Test.java + **/*Spec.java + + + + + org.junit.vintage + junit-vintage-engine + 5.8.1 + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + diff --git a/management-service/src/main/java/pl/pwr/zpi/cluster/service/ClusterService.java b/management-service/src/main/java/pl/pwr/zpi/cluster/service/ClusterService.java index 93db0281..887957e5 100644 --- a/management-service/src/main/java/pl/pwr/zpi/cluster/service/ClusterService.java +++ b/management-service/src/main/java/pl/pwr/zpi/cluster/service/ClusterService.java @@ -99,7 +99,6 @@ public ClusterConfigurationDTO getClusterById(String clusterId) { return isRunning .map(running -> ClusterConfigurationDTO.ofCluster(clusterConfiguration, running)) .orElse(ClusterConfigurationDTO.ofCluster(clusterConfiguration, false)); - }).orElse(ClusterConfigurationDTO.defaultConfiguration()); } diff --git a/management-service/src/main/java/pl/pwr/zpi/healthcheck/HealthCheckController.java b/management-service/src/main/java/pl/pwr/zpi/healthcheck/HealthCheckController.java deleted file mode 100644 index 3f688f69..00000000 --- a/management-service/src/main/java/pl/pwr/zpi/healthcheck/HealthCheckController.java +++ /dev/null @@ -1,17 +0,0 @@ -package pl.pwr.zpi.healthcheck; - -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RequiredArgsConstructor -@RestController -public class HealthCheckController { - @Operation(summary = "Check if the application is running") - @GetMapping("/public/api/v1/healthcheck") - public ResponseEntity checkHealth() { - return ResponseEntity.ok().build(); - } -} diff --git a/management-service/src/main/java/pl/pwr/zpi/metadata/service/MetadataService.java b/management-service/src/main/java/pl/pwr/zpi/metadata/service/MetadataService.java index 538e522b..5ace8c1b 100644 --- a/management-service/src/main/java/pl/pwr/zpi/metadata/service/MetadataService.java +++ b/management-service/src/main/java/pl/pwr/zpi/metadata/service/MetadataService.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; @Slf4j @Service @@ -31,12 +32,10 @@ public class MetadataService { private final MetadataHistoryService metadataHistoryService; public List getAllClusters() { - List activeClusterMetadataDTOS = getActiveClusters(); - - Set inactiveClusterMetadataDTOS = filterInactiveClusters(activeClusterMetadataDTOS); - activeClusterMetadataDTOS.addAll(inactiveClusterMetadataDTOS); - - return activeClusterMetadataDTOS; + return Stream.concat( + getActiveClusters().stream(), + filterInactiveClusters(getActiveClusters()).stream()) + .collect(Collectors.toList()); } public List getActiveClusters() { @@ -66,12 +65,11 @@ public Optional getClusterById(String clusterId) { } public List getClusterNodes(String clusterId) { - List activeNodeMetadataDTOS = getActiveNodesForClusterId(clusterId); - - Set inactiveNodeMetadataDTOS = filterInactiveNodesForClusterId(clusterId, activeNodeMetadataDTOS); - activeNodeMetadataDTOS.addAll(inactiveNodeMetadataDTOS); - - return activeNodeMetadataDTOS; + return Stream.concat( + getActiveNodesForClusterId(clusterId).stream(), + filterInactiveNodesForClusterId(clusterId, getActiveNodesForClusterId(clusterId)).stream() + ) + .collect(Collectors.toList()); } private List getActiveNodesForClusterId(String clusterId) { diff --git a/management-service/src/main/java/pl/pwr/zpi/notifications/email/service/EmailMessagingServiceImpl.java b/management-service/src/main/java/pl/pwr/zpi/notifications/email/service/EmailMessagingServiceImpl.java index 6671a507..46e2c616 100644 --- a/management-service/src/main/java/pl/pwr/zpi/notifications/email/service/EmailMessagingServiceImpl.java +++ b/management-service/src/main/java/pl/pwr/zpi/notifications/email/service/EmailMessagingServiceImpl.java @@ -9,7 +9,6 @@ import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import pl.pwr.zpi.notifications.common.ResourceLoaderUtils; @Service @RequiredArgsConstructor diff --git a/management-service/src/main/java/pl/pwr/zpi/notifications/slack/service/SlackReceiverService.java b/management-service/src/main/java/pl/pwr/zpi/notifications/slack/service/SlackReceiverService.java index b3858918..6faf06e5 100644 --- a/management-service/src/main/java/pl/pwr/zpi/notifications/slack/service/SlackReceiverService.java +++ b/management-service/src/main/java/pl/pwr/zpi/notifications/slack/service/SlackReceiverService.java @@ -130,6 +130,13 @@ public SlackReceiver getEncodedWebhookUrl(Long id) { } public void deleteSlackReceiver(Long receiverId) { + checkIfReceiverExist(receiverId); slackRepository.deleteById(receiverId); } + + private void checkIfReceiverExist(Long receiverId) { + if (!slackRepository.existsById(receiverId)) { + throw new IllegalArgumentException("Webhook with given Id not found"); + } + } } diff --git a/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java b/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java index 2df8081b..9c456a12 100644 --- a/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java +++ b/management-service/src/main/java/pl/pwr/zpi/reports/service/ReportsService.java @@ -56,12 +56,10 @@ public Optional getReportDetailedSummaryById(String re } public Optional getReportIncidents(String reportId) { - return reportRepository.findProjectedIncidentsById(reportId).map(incidentProjection -> { - return ReportIncidentsDTO.builder() - .applicationIncidents(extractApplicationIncidents(incidentProjection)) - .nodeIncidents(extractNodeIncidents(incidentProjection)) - .build(); - }); + return reportRepository.findProjectedIncidentsById(reportId).map(incidentProjection -> ReportIncidentsDTO.builder() + .applicationIncidents(extractApplicationIncidents(incidentProjection)) + .nodeIncidents(extractNodeIncidents(incidentProjection)) + .build()); } public ReportPaginatedIncidentsDTO getReportApplicationIncidents( diff --git a/management-service/src/main/java/pl/pwr/zpi/user/service/UserService.java b/management-service/src/main/java/pl/pwr/zpi/user/service/UserService.java index 40431290..741118b7 100644 --- a/management-service/src/main/java/pl/pwr/zpi/user/service/UserService.java +++ b/management-service/src/main/java/pl/pwr/zpi/user/service/UserService.java @@ -1,13 +1,10 @@ package pl.pwr.zpi.user.service; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Service; import pl.pwr.zpi.user.data.User; import pl.pwr.zpi.user.repository.UserRepository; -import java.time.Instant; import java.util.Optional; @RequiredArgsConstructor @@ -16,16 +13,6 @@ public class UserService { private final UserRepository userRepository; - public Optional getCurrentUser() { - var userDetails = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); - var userEmail = userDetails.getUsername(); - return getUserByEmail(userEmail); - } - - public Optional getUserByEmail(String email) { - return userRepository.findByEmail(email); - } - public User saveUser(User user) { return userRepository.save(user); } diff --git a/management-service/src/main/resources/templates/email/footer.html b/management-service/src/main/resources/templates/email/footer.html index bf8237a2..2d9292a1 100644 --- a/management-service/src/main/resources/templates/email/footer.html +++ b/management-service/src/main/resources/templates/email/footer.html @@ -9,7 +9,7 @@ - Magpie Monitor - complex anomaly detection system + Magpie Monitor - Reading logs is for the frogs, let's find insights from them diff --git a/management-service/src/test/groovy/pl/pwr/zpi/cluster/ClusterServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/cluster/ClusterServiceTest.groovy new file mode 100644 index 00000000..99cfd77a --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/cluster/ClusterServiceTest.groovy @@ -0,0 +1,105 @@ +package pl.pwr.zpi.cluster + +import pl.pwr.zpi.cluster.dto.UpdateClusterConfigurationRequest +import pl.pwr.zpi.cluster.entity.ClusterConfiguration +import pl.pwr.zpi.cluster.repository.ClusterRepository +import pl.pwr.zpi.cluster.service.ClusterService +import pl.pwr.zpi.notifications.ReceiverService +import pl.pwr.zpi.metadata.service.MetadataService +import pl.pwr.zpi.notifications.discord.entity.DiscordReceiver +import pl.pwr.zpi.notifications.email.entity.EmailReceiver +import pl.pwr.zpi.notifications.slack.entity.SlackReceiver +import pl.pwr.zpi.metadata.dto.cluster.ClusterMetadataDTO +import pl.pwr.zpi.reports.enums.Accuracy +import spock.lang.Specification +import spock.lang.Subject + +class ClusterServiceTest extends Specification { + + @Subject + ClusterService clusterService + + ClusterRepository clusterRepository = Mock() + ReceiverService receiverService = Mock() + MetadataService metadataService = Mock() + + def setup() { + clusterService = new ClusterService(clusterRepository, receiverService, metadataService) + } + + def "should update cluster configuration and save it to repository"() { + given: + def request = new UpdateClusterConfigurationRequest( + "cluster-id", Accuracy.HIGH, true, 1000L, + [1L, 2L], [3L, 4L], [5L, 6L], [], [] + ) + def clusterConfiguration = new ClusterConfiguration(id: "cluster-id") + + mockReceiverService() + + clusterRepository.save(_) >> clusterConfiguration + + when: + def response = clusterService.updateClusterConfiguration(request) + + then: + 1 * clusterRepository.save(_) + response.clusterId != null + } + + def "should return cluster configuration by id"() { + given: + def clusterId = "cluster-id" + def clusterConfiguration = new ClusterConfiguration(id: clusterId) + def metadata = new ClusterMetadataDTO(clusterId, System.currentTimeMillis(), Accuracy.HIGH, true, [], [], []) + + clusterConfiguration.nodeConfigurations = [] + clusterConfiguration.applicationConfigurations = [] + + clusterRepository.findById(clusterId) >> Optional.of(clusterConfiguration) + metadataService.getClusterById(clusterId) >> Optional.of(metadata) + + when: + def result = clusterService.getClusterById(clusterId) + + then: + result != null + result.id == clusterId + result.running == metadata.isRunning() + result.accuracy == clusterConfiguration.accuracy + result.nodeConfigurations.isEmpty() + result.applicationConfigurations.isEmpty() + } + + def "should return cluster configuration with false running status if metadata is empty"() { + given: + def clusterId = "cluster-id" + def clusterConfiguration = new ClusterConfiguration(id: clusterId) + + clusterConfiguration.nodeConfigurations = [] + clusterConfiguration.applicationConfigurations = [] + + clusterRepository.findById(clusterId) >> Optional.of(clusterConfiguration) + metadataService.getClusterById(clusterId) >> Optional.empty() + + when: + def result = clusterService.getClusterById(clusterId) + + then: + result != null + result.id == clusterId + result.running == false + result.accuracy == clusterConfiguration.accuracy + result.nodeConfigurations.isEmpty() + result.applicationConfigurations.isEmpty() + } + + private void mockReceiverService() { + receiverService.getSlackReceiverById(1L) >> new SlackReceiver() + receiverService.getSlackReceiverById(2L) >> new SlackReceiver() + receiverService.getDiscordReceiverById(3L) >> new DiscordReceiver() + receiverService.getDiscordReceiverById(4L) >> new DiscordReceiver() + receiverService.getEmailReceiverById(5L) >> new EmailReceiver() + receiverService.getEmailReceiverById(6L) >> new EmailReceiver() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataHistoryServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataHistoryServiceTest.groovy new file mode 100644 index 00000000..c6d4c72f --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataHistoryServiceTest.groovy @@ -0,0 +1,212 @@ +package pl.pwr.zpi.metadata + +import pl.pwr.zpi.metadata.dto.application.ApplicationMetadataDTO +import pl.pwr.zpi.metadata.dto.node.NodeMetadataDTO +import pl.pwr.zpi.metadata.entity.ClusterHistory +import pl.pwr.zpi.metadata.broker.dto.application.ApplicationMetadata +import pl.pwr.zpi.metadata.broker.dto.node.NodeMetadata +import pl.pwr.zpi.metadata.broker.dto.cluster.ClusterMetadata +import pl.pwr.zpi.metadata.repository.ClusterHistoryRepository +import pl.pwr.zpi.metadata.service.MetadataHistoryService +import spock.lang.Specification +import spock.lang.Subject +import spock.lang.Unroll + +class MetadataHistoryServiceTest extends Specification { + + ClusterHistoryRepository clusterHistoryRepository + @Subject + MetadataHistoryService metadataHistoryService + + def setup() { + clusterHistoryRepository = Mock() + metadataHistoryService = new MetadataHistoryService(clusterHistoryRepository) + } + + private Set createNodeHistory(String... nodeIds) { + return nodeIds.collect { new NodeMetadataDTO(it, false) }.toSet() + } + + private Set createAppHistory(String... appNames) { + return appNames.collect { new ApplicationMetadataDTO(it, "type", false) }.toSet() + } + + private ClusterHistory createClusterHistory(String clusterId, Set nodeHistory = [] as Set, Set appHistory = [] as Set) { + return new ClusterHistory(clusterId, appHistory, nodeHistory) + } + + @Unroll + def "should return clusters history for #clusterId"() { + given: + def clusterHistoryList = [createClusterHistory(clusterId)] + + clusterHistoryRepository.findAll() >> clusterHistoryList + + when: + def result = metadataHistoryService.getClustersHistory() + + then: + result == clusterHistoryList + + where: + clusterId | _ + "cluster1" | _ + "cluster2" | _ + } + + + def "should return node history for a pl.pwr.zpi.cluster"() { + given: + def clusterId = "cluster1" + def nodeHistory = createNodeHistory("node1", "node2") + def clusterHistory = createClusterHistory(clusterId, nodeHistory) + + clusterHistoryRepository.findById(clusterId) >> Optional.of(clusterHistory) + + when: + def result = metadataHistoryService.getNodeHistory(clusterId) + + then: + result == nodeHistory + } + + def "should return empty node history if no history found for pl.pwr.zpi.cluster"() { + given: + def clusterId = "cluster1" + clusterHistoryRepository.findById(clusterId) >> Optional.empty() + + when: + def result = metadataHistoryService.getNodeHistory(clusterId) + + then: + result.isEmpty() + } + + def "should return application history for a pl.pwr.zpi.cluster"() { + given: + def clusterId = "cluster1" + def appHistory = createAppHistory("app1", "app2") + def clusterHistory = createClusterHistory(clusterId, [] as Set, appHistory) + + clusterHistoryRepository.findById(clusterId) >> Optional.of(clusterHistory) + + when: + def result = metadataHistoryService.getApplicationHistory(clusterId) + + then: + result == appHistory + } + + def "should return empty application history if no history found for pl.pwr.zpi.cluster"() { + given: + def clusterId = "cluster1" + clusterHistoryRepository.findById(clusterId) >> Optional.empty() + + when: + def result = metadataHistoryService.getApplicationHistory(clusterId) + + then: + result.isEmpty() + } + + @Unroll + def "should update clusters history if not already present for #clusterId"() { + given: + def clusterMetadata = new ClusterMetadata(clusterId) + def clusterHistory = createClusterHistory(clusterId) + + clusterHistoryRepository.existsById(clusterId) >> false + clusterHistoryRepository.save(_) >> clusterHistory + + when: + metadataHistoryService.updateClustersHistory([clusterMetadata]) + + then: + 1 * clusterHistoryRepository.save(_) + + where: + clusterId << ["cluster1", "cluster2"] + } + + @Unroll + def "should not update clusters history if already present for #clusterId"() { + given: + def clusterMetadata = new ClusterMetadata(clusterId) + clusterHistoryRepository.existsById(clusterId) >> true + + when: + metadataHistoryService.updateClustersHistory([clusterMetadata]) + + then: + 0 * clusterHistoryRepository.save(_) + + where: + clusterId << ["cluster1"] + } + + def "should update node history for a pl.pwr.zpi.cluster"() { + given: + def clusterId = "cluster1" + def nodeMetadataList = List.of(new NodeMetadata("node1", List.of()), new NodeMetadata("node2", List.of())) + def clusterHistory = createClusterHistory(clusterId) + + clusterHistoryRepository.findById(clusterId) >> Optional.of(clusterHistory) + clusterHistoryRepository.save(_) >> clusterHistory + + when: + metadataHistoryService.updateNodeHistory(clusterId, nodeMetadataList) + + then: + 1 * clusterHistoryRepository.save(_) + clusterHistory.nodes().size() == 2 + } + + def "should add node history if pl.pwr.zpi.cluster does not exist"() { + given: + def clusterId = "cluster1" + def nodeMetadataList = List.of(new NodeMetadata("node1", List.of()), new NodeMetadata("node2", List.of())) + def clusterHistory = createClusterHistory(clusterId) + + clusterHistoryRepository.findById(clusterId) >> Optional.empty() + clusterHistoryRepository.save(_) >> clusterHistory + + when: + metadataHistoryService.updateNodeHistory(clusterId, nodeMetadataList) + + then: + 1 * clusterHistoryRepository.save(_) + } + + def "should update application history for a pl.pwr.zpi.cluster"() { + given: + def clusterId = "cluster1" + def appMetadataList = [new ApplicationMetadata("app1", "type1"), new ApplicationMetadata("app2", "type2")] + def clusterHistory = createClusterHistory(clusterId) + + clusterHistoryRepository.findById(clusterId) >> Optional.of(clusterHistory) + clusterHistoryRepository.save(_) >> clusterHistory + + when: + metadataHistoryService.updateApplicationHistory(clusterId, appMetadataList) + + then: + 1 * clusterHistoryRepository.save(_) + clusterHistory.applications().size() == 2 + } + + def "should add application history if pl.pwr.zpi.cluster does not exist"() { + given: + def clusterId = "cluster1" + def appMetadataList = [new ApplicationMetadata("app1", "type1"), new ApplicationMetadata("app2", "type2")] + def clusterHistory = createClusterHistory(clusterId) + + clusterHistoryRepository.findById(clusterId) >> Optional.empty() + clusterHistoryRepository.save(_) >> clusterHistory + + when: + metadataHistoryService.updateApplicationHistory(clusterId, appMetadataList) + + then: + 1 * clusterHistoryRepository.save(_) + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataServiceTest.groovy new file mode 100644 index 00000000..37f17c6c --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/metadata/MetadataServiceTest.groovy @@ -0,0 +1,176 @@ +package pl.pwr.zpi.metadata + +import pl.pwr.zpi.metadata.broker.dto.application.AggregatedApplicationMetadata +import pl.pwr.zpi.metadata.broker.dto.application.ApplicationMetadata +import pl.pwr.zpi.metadata.broker.dto.cluster.AggregatedClusterMetadata +import pl.pwr.zpi.metadata.broker.dto.cluster.ClusterMetadata +import pl.pwr.zpi.metadata.broker.dto.node.AggregatedNodeMetadata +import pl.pwr.zpi.metadata.dto.application.ApplicationMetadataDTO +import pl.pwr.zpi.metadata.dto.cluster.ClusterMetadataDTO +import pl.pwr.zpi.metadata.dto.node.NodeMetadataDTO +import pl.pwr.zpi.metadata.entity.ClusterHistory +import pl.pwr.zpi.metadata.repository.AggregatedApplicationMetadataRepository +import pl.pwr.zpi.metadata.repository.AggregatedClusterMetadataRepository +import pl.pwr.zpi.metadata.repository.AggregatedNodeMetadataRepository +import pl.pwr.zpi.metadata.service.MetadataHistoryService +import pl.pwr.zpi.metadata.service.MetadataService +import spock.lang.Specification +import spock.lang.Subject + +class MetadataServiceTest extends Specification { + + AggregatedApplicationMetadataRepository applicationMetadataRepository + AggregatedNodeMetadataRepository nodeMetadataRepository + AggregatedClusterMetadataRepository clusterMetadataRepository + MetadataHistoryService metadataHistoryService + + @Subject + MetadataService metadataService + + def setup() { + applicationMetadataRepository = Mock() + nodeMetadataRepository = Mock() + clusterMetadataRepository = Mock() + metadataHistoryService = Mock() + metadataService = new MetadataService(applicationMetadataRepository, nodeMetadataRepository, clusterMetadataRepository, metadataHistoryService) + } + + private AggregatedClusterMetadata createAggregatedClusterMetadata(Long collectedAt) { + return new AggregatedClusterMetadata(collectedAt, List.of()) + } + + private AggregatedNodeMetadata createAggregatedNodeMetadata(Long collectedAt, String nodeName) { + return new AggregatedNodeMetadata(collectedAt, nodeName, List.of()) + } + + private AggregatedApplicationMetadata createAggregatedApplicationMetadata(Long collectedAt, String clusterId) { + return new AggregatedApplicationMetadata(collectedAt, clusterId, List.of()) + } + + private ClusterMetadataDTO createClusterMetadataDTO(String clusterId) { + return new ClusterMetadataDTO(clusterId, null, null, true, null, null, null) + } + + private ClusterMetadataDTO createInactiveClusterMetadataDTO(String clusterId) { + return new ClusterMetadataDTO(clusterId, null, null, false, null, null, null) + } + + private NodeMetadataDTO createNodeMetadataDTO(String nodeName) { + return new NodeMetadataDTO(nodeName, true) + } + + private ApplicationMetadataDTO createApplicationMetadataDTO(String appName) { + return new ApplicationMetadataDTO(appName, "deployment", true) + } + + private ApplicationMetadataDTO createInactiveApplicationMetadataDTO(String appName) { + return new ApplicationMetadataDTO(appName, "deployment", false) + } + + def "should return all clusters including inactive ones"() { + given: + clusterMetadataRepository.findFirstByOrderByCollectedAtMsDesc() >> Optional.of(new AggregatedClusterMetadata(1732395048724L, [ + new ClusterMetadata("cluster1") + ])) + + metadataHistoryService.getClustersHistory() >> List.of( + new ClusterHistory("cluster2", Set.of(), Set.of()) + ) + + when: + def result = metadataService.getAllClusters() + + then: + result.size() == 2 + result.contains(createClusterMetadataDTO("cluster1")) + result.contains(createInactiveClusterMetadataDTO("cluster2")) + } + + def "should return pl.pwr.zpi.cluster by id"() { + given: + def clusterId = "cluster1" + def clusterDTO = createInactiveClusterMetadataDTO(clusterId) + + clusterMetadataRepository.existsByMetadataClusterId(clusterId) >> true + clusterMetadataRepository.findFirstByOrderByCollectedAtMsDesc() >> Optional.of(new AggregatedClusterMetadata(1732395048724L, List.of(new ClusterMetadata(clusterId)))) + metadataService.getActiveClusters() >> List.of(clusterDTO) + + when: + def result = metadataService.getClusterById(clusterId) + + then: + result.isPresent() + result.get() == clusterDTO + } + + def "should return empty if pl.pwr.zpi.cluster does not exist"() { + given: + def clusterId = "cluster1" + + clusterMetadataRepository.existsByMetadataClusterId(clusterId) >> false + + when: + def result = metadataService.getClusterById(clusterId) + + then: + result.isEmpty() + } + + def "should return all applications for a pl.pwr.zpi.cluster including inactive ones"() { + given: + def clusterId = "cluster1" + def activeApp = createApplicationMetadataDTO("app1") + def inactiveApp = createInactiveApplicationMetadataDTO("app2") + + applicationMetadataRepository.findFirstByClusterIdOrderByCollectedAtMsDesc(clusterId) >> Optional.of( + new AggregatedApplicationMetadata(1732395048724L, "test123", List.of(new ApplicationMetadata("app1", "deployment"))) + ) + + metadataHistoryService.getApplicationHistory(clusterId) >> List.of( + new ApplicationMetadataDTO("app2", "deployment", false) + ) + + when: + def result = metadataService.getClusterApplications(clusterId) + + then: + result.size() == 2 + result.contains(activeApp) + result.contains(inactiveApp) + } + + + + def "should save pl.pwr.zpi.cluster pl.pwr.zpi.metadata"() { + given: + def clusterMetadata = createAggregatedClusterMetadata(1732395048724L) + + when: + metadataService.saveClusterMetadata(clusterMetadata) + + then: + 1 * clusterMetadataRepository.save(clusterMetadata) + } + + def "should save application pl.pwr.zpi.metadata"() { + given: + def applicationMetadata = createAggregatedApplicationMetadata(1732395048724L, "test123") + + when: + metadataService.saveApplicationMetadata(applicationMetadata) + + then: + 1 * applicationMetadataRepository.save(applicationMetadata) + } + + def "should save node pl.pwr.zpi.metadata"() { + given: + def nodeMetadata = createAggregatedNodeMetadata(1732395048724L, "test123") + + when: + metadataService.saveNodeMetadata(nodeMetadata) + + then: + 1 * nodeMetadataRepository.save(nodeMetadata) + } +} \ No newline at end of file diff --git a/management-service/src/test/groovy/pl/pwr/zpi/notifications/common/ConfidentialTextEncoderTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/notifications/common/ConfidentialTextEncoderTest.groovy new file mode 100644 index 00000000..f4dba588 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/notifications/common/ConfidentialTextEncoderTest.groovy @@ -0,0 +1,38 @@ +package pl.pwr.zpi.notifications.common + + +import spock.lang.Specification +import spock.lang.Subject + +class ConfidentialTextEncoderTest extends Specification { + + @Subject + ConfidentialTextEncoder confidentialTextEncoder + + def setup() { + String exampleEncryptionKey = "O8JvErGt84wzZzPPeFg4tQ==" + String secretKeyCode = Base64.getEncoder().encodeToString(exampleEncryptionKey.getBytes("UTF-8")) + String cipherAlgorithm = "AES" + + confidentialTextEncoder = new ConfidentialTextEncoder(secretKeyCode, cipherAlgorithm) + } + + private String processMessage(String plainText) { + String encryptedText = confidentialTextEncoder.encrypt(plainText) + String decryptedText = confidentialTextEncoder.decrypt(encryptedText) + return decryptedText + } + + def "should encrypt and decrypt messages correctly"() { + expect: + processMessage(plainText) == plainText + + where: + plainText << [ + "Example confidential information", + "123454321", + "https://discord.com/api/webhooks/1234554321/xKh5vF0Som55bSex4q9slwOApmB0VXjcUoVS5Z9v9vu89snl-XeedfHj", + "" + ] + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/notifications/discord/DiscordReceiverServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/notifications/discord/DiscordReceiverServiceTest.groovy new file mode 100644 index 00000000..4b8cae1c --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/notifications/discord/DiscordReceiverServiceTest.groovy @@ -0,0 +1,132 @@ +package pl.pwr.zpi.notifications.discord + +import pl.pwr.zpi.notifications.common.ConfidentialTextEncoder +import pl.pwr.zpi.notifications.discord.dto.DiscordReceiverDTO +import pl.pwr.zpi.notifications.discord.dto.UpdateDiscordReceiverRequest +import pl.pwr.zpi.notifications.discord.entity.DiscordReceiver +import pl.pwr.zpi.notifications.discord.repository.DiscordRepository +import pl.pwr.zpi.notifications.discord.service.DiscordReceiverService +import spock.lang.Specification + +class DiscordReceiverServiceTest extends Specification { + + DiscordRepository discordRepository + ConfidentialTextEncoder confidentialTextEncoder + DiscordReceiverService discordReceiverService + + def setup() { + confidentialTextEncoder = Mock() + discordRepository = Mock() + discordReceiverService = new DiscordReceiverService(discordRepository, confidentialTextEncoder) + discordReceiverService.WEBHOOK_URL_REGEX = "https://discord.com/api/webhooks/[0-9]+/[a-zA-Z0-9_-]+" + } + + def "should get all discord integrations"() { + given: + def encryptedWebhookUrl = "encryptedWebhook1" + def decryptedWebhookUrl = "https://discord.com/api/webhooks/1234554321/xKh5vF0Som55bSex4q9slwOApmB0VXjcUoVS5Z9v9vu89snl-XeedfHj" + def discordReceiverList = List.of(createDiscordReceiver(1L, "Receiver 1", encryptedWebhookUrl)) + + when: + def result = discordReceiverService.getAllDiscordIntegrations() + + then: + result == discordReceiverList + 1 * discordRepository.findAll() >> discordReceiverList + 1 * confidentialTextEncoder.decrypt(encryptedWebhookUrl) >> decryptedWebhookUrl + } + + def "should add new discord integration successfully"() { + given: + def encryptedWebhookUrl = "encryptedWebhook1" + def decryptedWebhookUrl = "https://discord.com/api/webhooks/1234554321/xKh5vF0Som55bSex4q9slwOApmB0VXjcUoVS5Z9v9vu89snl-XeedfHj" + def discordReceiverDTO = createDiscordReceiverDTO("Receiver 1", decryptedWebhookUrl) + + confidentialTextEncoder.encrypt(_) >> encryptedWebhookUrl + discordRepository.existsByWebhookUrl(_) >> false + + when: + discordReceiverService.createDiscordReceiver(discordReceiverDTO) + + then: + 1 * confidentialTextEncoder.encrypt(discordReceiverDTO.getWebhookUrl()) >> encryptedWebhookUrl + 1 * discordRepository.existsByWebhookUrl(encryptedWebhookUrl) >> false + 1 * discordRepository.save(_ as DiscordReceiver) + } + + def "should throw exception if webhook already exists when adding new discord integration"() { + given: + def encryptedWebhookUrl = "encryptedWebhook1" + def decryptedWebhookUrl = "https://discord.com/api/webhooks/1234554321/xKh5vF0Som55bSex4q9slwOApmB0VXjcUoVS5Z9v9vu89snl-XeedfHj" + def discordReceiverDTO = createDiscordReceiverDTO("Receiver 1", decryptedWebhookUrl) + + confidentialTextEncoder.encrypt(_) >> encryptedWebhookUrl + discordRepository.existsByWebhookUrl(_) >> true + + when: + discordReceiverService.createDiscordReceiver(discordReceiverDTO) + + then: + 1 * confidentialTextEncoder.encrypt(discordReceiverDTO.getWebhookUrl()) >> encryptedWebhookUrl + 1 * discordRepository.existsByWebhookUrl(encryptedWebhookUrl) >> true + 0 * discordRepository.save(_ as DiscordReceiver) + thrown(IllegalArgumentException) + } + + def "should update discord integration successfully"() { + given: + def id = 1L + def encryptedWebhookUrl = "https://discord.com/api/webhooks/1234554321/********************************************************" + def decryptedWebhookUrl = "https://discord.com/api/webhooks/1234554321/xKh5vF0Som55bSex4q9slwOApmB0VXjcUoVS5Z9v9vu89snl-XeedfHj" + def discordReceiverUpdateRequest = new UpdateDiscordReceiverRequest("Updated Receiver", decryptedWebhookUrl) + def existingReceiver = createDiscordReceiver(id, "Receiver 1", encryptedWebhookUrl) + + discordRepository.findById(id) >> Optional.of(existingReceiver) + confidentialTextEncoder.decrypt(encryptedWebhookUrl) >> decryptedWebhookUrl + confidentialTextEncoder.encrypt(decryptedWebhookUrl) >> encryptedWebhookUrl + discordRepository.existsByWebhookUrl(_) >> false + discordRepository.save(_) >> existingReceiver + + when: + def updatedReceiver = discordReceiverService.updateDiscordIntegration(id, discordReceiverUpdateRequest) + + then: + updatedReceiver != null + updatedReceiver.receiverName == "Updated Receiver" + updatedReceiver.webhookUrl == encryptedWebhookUrl + 1 * discordRepository.findById(id) >> Optional.of(existingReceiver) + 2 * confidentialTextEncoder.encrypt(decryptedWebhookUrl) >> encryptedWebhookUrl + 1 * discordRepository.existsByWebhookUrl(encryptedWebhookUrl) >> false + 1 * discordRepository.save(_ as DiscordReceiver) >> existingReceiver + } + + def "should throw exception if discord receiver not found"() { + given: + def id = 1L + discordRepository.findById(id) >> Optional.empty() + + when: + discordReceiverService.getDiscordReceiver(id) + + then: + thrown(IllegalArgumentException) + 1 * discordRepository.findById(id) >> Optional.empty() + } + + private DiscordReceiverDTO createDiscordReceiverDTO(String name, String webhookUrl) { + return DiscordReceiverDTO.builder() + .name(name) + .webhookUrl(webhookUrl) + .build() + } + + private DiscordReceiver createDiscordReceiver(Long id, String name, String webhookUrl) { + return DiscordReceiver.builder() + .id(id) + .receiverName(name) + .webhookUrl(webhookUrl) + .createdAt(System.currentTimeMillis()) + .updatedAt(System.currentTimeMillis()) + .build() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailReceiverServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailReceiverServiceTest.groovy new file mode 100644 index 00000000..e66aba33 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailReceiverServiceTest.groovy @@ -0,0 +1,119 @@ +package pl.pwr.zpi.notifications.email + +import pl.pwr.zpi.notifications.email.dto.EmailReceiverUpdateRequest +import pl.pwr.zpi.notifications.email.service.EmailReceiverService +import pl.pwr.zpi.notifications.email.dto.EmailReceiverDTO +import pl.pwr.zpi.notifications.email.entity.EmailReceiver +import pl.pwr.zpi.notifications.email.repository.EmailRepository +import spock.lang.Specification + + +class EmailReceiverServiceTest extends Specification { + + def emailRepository = Mock(EmailRepository) + + def emailReceiverService = new EmailReceiverService(emailRepository) + + def "should get all email receivers"() { + given: + def emailReceiverList = [buildEmailReceiver(1L, "Receiver 1", "receiver1@example.com")] + + when: + def result = emailReceiverService.getAllEmails() + + then: + result == emailReceiverList + 1 * emailRepository.findAll() >> emailReceiverList + } + + def "should add a new email receiver"() { + given: + def emailReceiverDTO = buildEmailReceiverDTO("Receiver 1", "receiver1@example.com") + def newReceiver = buildEmailReceiver(null, emailReceiverDTO.getName(), emailReceiverDTO.getEmail()) + + when: + emailReceiverService.addNewEmail(emailReceiverDTO) + + then: + 1 * emailRepository.existsByReceiverEmail(emailReceiverDTO.getEmail()) >> false + 1 * emailRepository.save({ EmailReceiver savedReceiver -> + savedReceiver.receiverName == newReceiver.receiverName && + savedReceiver.receiverEmail == newReceiver.receiverEmail && + savedReceiver.updatedAt >= 0 && + savedReceiver.createdAt >= 0 + }) + } + + def "should throw exception when adding email that already exists"() { + given: + def emailReceiverDTO = buildEmailReceiverDTO("Receiver 1", "receiver1@example.com") + + when: + emailReceiverService.addNewEmail(emailReceiverDTO) + + then: + thrown(IllegalArgumentException) + 1 * emailRepository.existsByReceiverEmail(emailReceiverDTO.getEmail()) >> true + } + + def "should update email receiver successfully"() { + given: + def id = 1L + def emailReceiverDTO = new EmailReceiverUpdateRequest("Updated Receiver", "updatedreceiver@example.com") + def existingReceiver = buildEmailReceiver(id, "Receiver 1", "receiver1@example.com") + + when: + def updatedReceiver = emailReceiverService.updateEmail(id, emailReceiverDTO) + + then: + updatedReceiver.receiverName == "Updated Receiver" + updatedReceiver.receiverEmail == "updatedreceiver@example.com" + 1 * emailRepository.findById(id) >> Optional.of(existingReceiver) + 1 * emailRepository.existsByReceiverEmail(emailReceiverDTO.email()) >> false + 1 * emailRepository.save(_ as EmailReceiver) >> existingReceiver + } + + def "should throw exception when trying to update email with already existing email"() { + given: + def id = 1L + def emailReceiverDTO = new EmailReceiverUpdateRequest("Receiver 1", "receiver1@example.com") + def existingReceiver = buildEmailReceiver(id, "Receiver 2", "receiver2@example.com") + + when: + emailReceiverService.updateEmail(id, emailReceiverDTO) + + then: + thrown(IllegalArgumentException) + 2 * emailRepository.findById(id) >> Optional.of(existingReceiver) + 1 * emailRepository.existsByReceiverEmail(emailReceiverDTO.email()) >> true + } + + def "should throw exception when email receiver not found for update"() { + given: + def id = 1L + def emailReceiverDTO = new EmailReceiverUpdateRequest("Receiver 1", "receiver1@example.com") + + when: + emailReceiverService.updateEmail(id, emailReceiverDTO) + + then: + thrown(IllegalArgumentException) + 1 * emailRepository.findById(id) >> Optional.empty() + } + + private EmailReceiverDTO buildEmailReceiverDTO(String name, String email) { + return EmailReceiverDTO.builder() + .name(name) + .email(email) + .build() + } + + private EmailReceiver buildEmailReceiver(Long id, String name, String email) { + return EmailReceiver.builder() + .id(id) + .receiverName(name) + .receiverEmail(email) + .createdAt(System.currentTimeMillis()) + .build() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailUtilsTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailUtilsTest.groovy new file mode 100644 index 00000000..75bcf175 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/notifications/email/EmailUtilsTest.groovy @@ -0,0 +1,67 @@ +package pl.pwr.zpi.notifications.email + +import pl.pwr.zpi.notifications.common.ResourceLoaderUtils +import pl.pwr.zpi.notifications.email.html.service.MarkdownService +import pl.pwr.zpi.notifications.email.internalization.service.LocalizedMessageService +import pl.pwr.zpi.notifications.email.utils.EmailUtils +import spock.lang.Specification +import spock.lang.Subject + +class EmailUtilsTest extends Specification { + + def markdownService = Mock(MarkdownService) + def localizedTestMailServiceImpl = Mock(LocalizedMessageService) + def localizedNewReportMailServiceImpl = Mock(LocalizedMessageService) + + @Subject + def emailUtils + + def setup() { + emailUtils = new EmailUtils(markdownService, localizedTestMailServiceImpl, localizedNewReportMailServiceImpl) + } + + def "createNewReportEmailTemplate should create a valid new report email template"() { + given: + def bodyText = "New report available." + def buttonText = "View Report" + def urlRedirect = "https://magpie-monitor.rolo-labs.xyz" + localizedNewReportMailServiceImpl.getMessage("new-report.body", _) >> bodyText + localizedNewReportMailServiceImpl.getMessage("new-report.button", _) >> buttonText + markdownService.toHtmlWithMarkdowns(bodyText) >> "${bodyText}" + + when: + def result = emailUtils.createNewReportEmailTemplate(urlRedirect) + + then: + result.contains("${bodyText}") + result.contains(buttonText) + result.contains(urlRedirect) + } + + def "createTestEmailTemplate should generate a valid test email template with localized content"() { + given: + def bodyText = "Test email body" + localizedTestMailServiceImpl.getMessage("test.body", _) >> bodyText + markdownService.toHtmlWithMarkdowns(bodyText) >> "${bodyText}" + + when: + def result = emailUtils.createTestEmailTemplate() + + then: + result.contains("${bodyText}") + !result.contains("Click Here") + } + + def "wrapContent should wrap the content using the wrapper template"() { + given: + def htmlTemplate = "Original Content" + def wrapperTemplate = "%content%" + ResourceLoaderUtils.loadResourceToString(EmailUtils.HTML_WRAPPER_PATH) >> wrapperTemplate + + when: + def result = emailUtils.wrapContent(htmlTemplate) + + then: + result.contains("Original Content") + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackNotificationServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackNotificationServiceTest.groovy new file mode 100644 index 00000000..24fe5064 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackNotificationServiceTest.groovy @@ -0,0 +1,84 @@ +package pl.pwr.zpi.notifications.slack + +import org.springframework.beans.factory.annotation.Value +import pl.pwr.zpi.notifications.slack.entity.SlackReceiver +import pl.pwr.zpi.notifications.slack.service.SlackMessagingService +import pl.pwr.zpi.notifications.slack.service.SlackReceiverService +import pl.pwr.zpi.notifications.common.ConfidentialTextEncoder +import spock.lang.Specification +import spock.lang.Subject + +class SlackNotificationServiceTest extends Specification { + + def slackService = Mock(SlackMessagingService) + def receiverService = Mock(SlackReceiverService) + def confidentialTextEncoder = Mock(ConfidentialTextEncoder) + + @Value("\${magpie.monitor.client.base.url}") + def MAGPIE_MONITOR_CLIENT_BASE_URL = "http://localhost" + + @Subject + def slackNotificationService = new SlackNotificationService(slackService, receiverService, confidentialTextEncoder) + + def "should send test message successfully"() { + given: + def receiverSlackId = 1L + def receiver = buildSlackReceiver(receiverSlackId, "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA") + + when: + slackNotificationService.sendTestMessage(receiverSlackId) + + then: + 1 * receiverService.getById(receiverSlackId) >> receiver + 1 * confidentialTextEncoder.decrypt(receiver.getWebhookUrl()) >> receiver.getWebhookUrl() + 1 * slackService.sendMessage(_, receiver.getWebhookUrl()) + } + + def "should send test message by webhook URL"() { + given: + def webhookUrl = "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA" + + when: + slackNotificationService.sendTestMessage(webhookUrl) + + then: + 1 * slackService.sendMessage(_, webhookUrl) + } + + def "should notify on report generated successfully"() { + given: + def receiverId = 1L + def reportId = "report123" + def receiver = buildSlackReceiver(receiverId, "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA") + + when: + slackNotificationService.notifyOnReportGenerated(receiverId, reportId) + + then: + 1 * receiverService.getEncodedWebhookUrl(receiverId) >> receiver + 1 * slackService.sendMessage(_, "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA") >> {} + } + + def "should throw exception when notify on report generated fails"() { + given: + def receiverId = 1L + def reportId = "report123" + receiverService.getEncodedWebhookUrl(receiverId) >> { throw new Exception("Error retrieving webhook") } + + when: + slackNotificationService.notifyOnReportGenerated(receiverId, reportId) + + then: + thrown(RuntimeException) + } + + private SlackReceiver buildSlackReceiver(Long id, String webhookUrl) { + return SlackReceiver.builder() + .id(id) + .receiverName("Receiver Name") + .webhookUrl(webhookUrl) + .updatedAt(System.currentTimeMillis()) + .createdAt(System.currentTimeMillis()) + .build() + } +} \ No newline at end of file diff --git a/management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackReceiverServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackReceiverServiceTest.groovy new file mode 100644 index 00000000..7ed66402 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/notifications/slack/SlackReceiverServiceTest.groovy @@ -0,0 +1,190 @@ +package pl.pwr.zpi.notifications.slack + +import pl.pwr.zpi.notifications.common.ConfidentialTextEncoder +import pl.pwr.zpi.notifications.slack.dto.SlackReceiverDTO +import pl.pwr.zpi.notifications.slack.dto.UpdateSlackReceiverRequest +import pl.pwr.zpi.notifications.slack.entity.SlackReceiver +import pl.pwr.zpi.notifications.slack.repository.SlackRepository +import pl.pwr.zpi.notifications.slack.service.SlackReceiverService +import spock.lang.Specification +import spock.lang.Subject + +class SlackReceiverServiceTest extends Specification { + + def slackRepository + def confidentialTextEncoder + + @Subject + def slackReceiverService + + def setup() { + slackRepository = Mock(SlackRepository) + confidentialTextEncoder = Mock(ConfidentialTextEncoder) + slackReceiverService = new SlackReceiverService(slackRepository, confidentialTextEncoder) + slackReceiverService.WEBHOOK_URL_REGEX = "https://hooks.slack.com/services/[A-Z0-9]+/[A-Z0-9]+/[a-zA-Z0-9]+" + } + + def "getAllSlackIntegrations should anonymize webhook URL for each receiver"() { + given: + def receiver1 = new SlackReceiver(1L, "Receiver1", "encryptedUrl1", System.currentTimeMillis(), System.currentTimeMillis()) + def receiver2 = new SlackReceiver(2L, "Receiver2", "encryptedUrl2", System.currentTimeMillis(), System.currentTimeMillis()) + slackRepository.findAll() >> [receiver1, receiver2] + confidentialTextEncoder.decrypt(_) >> { args -> args[0] } + + when: + def result = slackReceiverService.getAllSlackIntegrations() + + then: + 1 * confidentialTextEncoder.decrypt("encryptedUrl1") >> "https://slack.com/receiver1/token1" + 1 * confidentialTextEncoder.decrypt("encryptedUrl2") >> "https://slack.com/receiver2/token2" + result.size() == 2 + result[0].webhookUrl == "https://slack.com/receiver1/******" + result[1].webhookUrl == "https://slack.com/receiver2/******" + } + + def "getEncodedWebhookUrl should return receiver with decoded webhook URL"() { + given: + def receiver = new SlackReceiver(id: 1L, receiverName: "Receiver1", webhookUrl: "encryptedUrl") + slackRepository.findById(1L) >> Optional.of(receiver) + confidentialTextEncoder.decrypt(_) >> "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA" + + when: + def result = slackReceiverService.getEncodedWebhookUrl(1L) + + then: + result.webhookUrl == "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA" + } + + def "validateReceiverName should throw exception for short name"() { + given: + def shortName = "A" + + when: + slackReceiverService.validateReceiverName(shortName) + + then: + thrown(RuntimeException) + } + + def "validateWebhookUrl should throw exception for invalid URL"() { + given: + def invalidUrl = "invalidUrl" + + when: + slackReceiverService.validateWebhookUrl(invalidUrl) + + then: + thrown(RuntimeException) + } + + def "checkIfUserCanUpdateWebhookUrl should throw exception if URL is already used"() { + given: + slackRepository.findById(1L) >> Optional.of(new SlackReceiver(id: 1L, receiverName: "Receiver1", webhookUrl: "encryptedUrl")) + slackRepository.existsByWebhookUrl("newWebhookUrl") >> true + + when: + slackReceiverService.checkIfUserCanUpdateWebhookUrl("newWebhookUrl", 1L) + + then: + thrown(IllegalArgumentException) + } + + def "patchReceiver should update receiver name and webhook URL"() { + given: + def receiver = new SlackReceiver(id: 1L, receiverName: "OldReceiver", webhookUrl: "newencryptedUrl") + def updateRequest = new UpdateSlackReceiverRequest("UpdatedReceiver", "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA") + slackRepository.findById(1L) >> Optional.of(receiver) + slackRepository.existsByWebhookUrl("EncryptedUrl") >> true + confidentialTextEncoder.encrypt(_) >> "newEncryptedUrl" + slackRepository.save(receiver) >> receiver + confidentialTextEncoder.decrypt(_) >> "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/Xk3uMvmSOCsFhhTWPSGA" + + when: + slackReceiverService.updateSlackIntegration(1L, updateRequest) + + then: + receiver.receiverName == "UpdatedReceiver" + receiver.webhookUrl == "https://hooks.slack.com/services/T04PB0Y4K8Q/B07QG098S7M/********************" + } + + def "addNewSlackIntegration should save new slack integration"() { + given: + def slackReceiverDTO = buildSlackReceiverDTO("NewReceiver", "https://slack.com/webhook/abc123") + def encryptedUrl = "encryptedUrl" + confidentialTextEncoder.encrypt(_) >> encryptedUrl + slackRepository.existsByWebhookUrl(_) >> false + + when: + slackReceiverService.addNewSlackIntegration(slackReceiverDTO) + + then: + 1 * slackRepository.save(_ as SlackReceiver) + } + + def "addNewSlackIntegration should throw exception if webhook already exists"() { + given: + def slackReceiverDTO = buildSlackReceiverDTO("NewReceiver", "https://slack.com/webhook/abc123") + def encryptedUrl = "encryptedUrl" + confidentialTextEncoder.encrypt(_) >> encryptedUrl + slackRepository.existsByWebhookUrl(_) >> true + + when: + slackReceiverService.addNewSlackIntegration(slackReceiverDTO) + + then: + thrown(IllegalArgumentException) + } + + def "getById should return the SlackReceiver"() { + given: + def receiver = new SlackReceiver(id: 1L, receiverName: "Receiver1", webhookUrl: "encryptedUrl") + slackRepository.findById(1L) >> Optional.of(receiver) + + when: + def result = slackReceiverService.getById(1L) + + then: + result.id == 1L + result.receiverName == "Receiver1" + } + + def "getById should throw exception if receiver is not found"() { + given: + slackRepository.findById(1L) >> Optional.empty() + + when: + slackReceiverService.getById(1L) + + then: + thrown(IllegalArgumentException) + } + + def "deleteSlackReceiver should delete the receiver"() { + given: + slackRepository.existsById(1L) >> true + + when: + slackReceiverService.deleteSlackReceiver(1L) + + then: + 1 * slackRepository.deleteById(1L) + } + + def "deleteSlackReceiver should throw exception if receiver is not found"() { + given: + slackRepository.existsById(1L) >> false + + when: + slackReceiverService.deleteSlackReceiver(1L) + + then: + thrown(IllegalArgumentException) + } + + private buildSlackReceiverDTO(String name, String webhookUrl) { + return SlackReceiverDTO.builder() + .name(name) + .webhookUrl(webhookUrl) + .build() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportGenerationServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportGenerationServiceTest.groovy new file mode 100644 index 00000000..1fdfa483 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportGenerationServiceTest.groovy @@ -0,0 +1,200 @@ +package pl.pwr.zpi.reports + +import pl.pwr.zpi.reports.dto.event.ReportGenerated +import pl.pwr.zpi.reports.dto.event.ReportRequestFailed +import pl.pwr.zpi.reports.dto.event.ReportRequested +import pl.pwr.zpi.reports.dto.request.CreateReportRequest +import pl.pwr.zpi.reports.entity.report.Report +import pl.pwr.zpi.reports.entity.report.request.ReportGenerationRequestMetadata +import pl.pwr.zpi.reports.enums.Accuracy +import pl.pwr.zpi.reports.enums.ReportGenerationStatus +import pl.pwr.zpi.reports.enums.ReportType +import pl.pwr.zpi.reports.service.ReportGenerationService +import pl.pwr.zpi.reports.repository.* +import pl.pwr.zpi.notifications.ReportNotificationService +import pl.pwr.zpi.reports.broker.ReportPublisher +import spock.lang.Specification + +class ReportGenerationServiceTest extends Specification { + + def reportPublisher = Mock(ReportPublisher) + def reportNotificationService = Mock(ReportNotificationService) + def reportRepository = Mock(ReportRepository) + def nodeIncidentRepository = Mock(NodeIncidentRepository) + def nodeIncidentSourcesRepository = Mock(NodeIncidentSourcesRepository) + def applicationIncidentRepository = Mock(ApplicationIncidentRepository) + def applicationIncidentSourcesRepository = Mock(ApplicationIncidentSourcesRepository) + def reportGenerationRequestMetadataRepository = Mock(ReportGenerationRequestMetadataRepository) + + def reportGenerationService = new ReportGenerationService( + reportPublisher, + reportNotificationService, + reportRepository, + nodeIncidentRepository, + nodeIncidentSourcesRepository, + applicationIncidentRepository, + applicationIncidentSourcesRepository, + reportGenerationRequestMetadataRepository + ) + + def "should create report and publish report requested event"() { + given: + def reportRequest = createCreateReportRequest("cluster123", 0L, 86400000L) + def reportType = ReportType.SCHEDULED + def reportRequested = ReportRequested.of(reportRequest) + + when: + reportGenerationService.createReport(reportRequest, reportType) + + then: + 1 * reportGenerationRequestMetadataRepository.save(_ as ReportGenerationRequestMetadata) + 1 * reportPublisher.publishReportRequestedEvent({ ReportRequested reportRequestedEvent -> + reportRequestedEvent.reportRequest.clusterId == reportRequested.reportRequest.clusterId && + reportRequestedEvent.reportRequest.sinceMs == reportRequested.reportRequest.sinceMs && + reportRequestedEvent.reportRequest.toMs == reportRequested.reportRequest.toMs && + reportRequestedEvent.reportRequest.applicationConfiguration == reportRequested.reportRequest.applicationConfiguration && + reportRequestedEvent.reportRequest.nodeConfiguration == reportRequested.reportRequest.nodeConfiguration + }, _) + } + + def "should handle report generation failure and notify"() { + given: + def correlationId = "correlation123" + def requestFailed = ReportRequestFailed.builder() + .correlationId(correlationId) + .errorType("Failed to generate report") + .errorMessage("Error occurred") + .timestampMs(System.currentTimeMillis()) + .build() + def createReportRequest = CreateReportRequest.builder() + .clusterId("cluster123") + .accuracy(Accuracy.HIGH) + .sinceMs(0L) + .toMs(86400000L) + .slackReceiverIds([]) + .emailReceiverIds([]) + .discordReceiverIds([]) + .applicationConfigurations([]) + .nodeConfigurations([]) + .build() + def reportMetadata = ReportGenerationRequestMetadata.builder() + .correlationId(correlationId) + .status(ReportGenerationStatus.ERROR) + .error(requestFailed) + .reportType(ReportType.SCHEDULED) + .createReportRequest(createReportRequest) + .build() + + when: + reportGenerationService.handleReportGenerationError(requestFailed) + + then: + 1 * reportGenerationRequestMetadataRepository.findByCorrelationId(correlationId) >> Optional.of(reportMetadata) + 1 * reportNotificationService.notifySlackOnReportGenerationFailed(_, _) + 1 * reportNotificationService.notifyDiscordOnReportGenerationFailed(_, _) + 1 * reportNotificationService.notifyEmailOnReportGenerationFailed(_, _) + } + + def "should handle report generated and save report"() { + given: + def correlationId = "correlation123" + def report = Report.builder() + .nodeReports([]) + .applicationReports([]) + .build() + def reportGenerated = new ReportGenerated(correlationId, report, System.currentTimeMillis()) + def createReportRequest = CreateReportRequest.builder() + .clusterId("cluster123") + .accuracy(Accuracy.HIGH) + .sinceMs(0L) + .toMs(86400000L) + .slackReceiverIds([]) + .emailReceiverIds([]) + .discordReceiverIds([]) + .applicationConfigurations([]) + .nodeConfigurations([]) + .build() + def reportMetadata = ReportGenerationRequestMetadata.builder() + .correlationId(correlationId) + .status(ReportGenerationStatus.GENERATED) + .reportType(ReportType.SCHEDULED) + .createReportRequest(createReportRequest) + .build() + + when: + reportGenerationService.handleReportGenerated(reportGenerated) + + then: + 1 * reportGenerationRequestMetadataRepository.findByCorrelationId(correlationId) >> Optional.of(reportMetadata) + 1 * reportRepository.save(_ as Report) + 1 * reportNotificationService.notifySlackOnReportCreated(_, _) + 1 * reportNotificationService.notifyDiscordOnReportCreated(_, _) + 1 * reportNotificationService.notifyEmailOnReportCreated(_, _) + } + + def "should retry failed report generation request"() { + given: + def correlationId = "correlation123" + def reportRequest = createCreateReportRequest("cluster123", 0L, 86400000L) + def reportMetadata = ReportGenerationRequestMetadata.builder() + .correlationId(correlationId) + .status(ReportGenerationStatus.GENERATED) + .reportType(ReportType.SCHEDULED) + .createReportRequest(reportRequest) + .build() + + reportGenerationRequestMetadataRepository.findByCorrelationId(correlationId) >> Optional.of(reportMetadata) + + when: + reportGenerationService.retryFailedReportGenerationRequest(correlationId) + + then: + 1 * reportPublisher.publishReportRequestedEvent(_, _) + } + + def "should save report generation pl.pwr.zpi.metadata"() { + given: + def correlationId = "correlation123" + def reportRequest = createCreateReportRequest("cluster123", 0L, 86400000L) + def reportType = ReportType.SCHEDULED + + when: + reportGenerationService.persistReportGenerationRequestMetadata(correlationId, reportRequest, reportType) + + then: + 1 * reportGenerationRequestMetadataRepository.save(_ as ReportGenerationRequestMetadata) + } + + def "should throw exception if no pl.pwr.zpi.metadata found on report generation failure"() { + given: + def correlationId = "correlation123" + def requestFailed = ReportRequestFailed.builder() + .correlationId(correlationId) + .errorType("Failed to generate report") + .errorMessage("Error occurred") + .timestampMs(System.currentTimeMillis()) + .build() + + reportGenerationRequestMetadataRepository.findByCorrelationId(correlationId) >> Optional.empty() + + when: + reportGenerationService.handleReportGenerationError(requestFailed) + + then: + thrown(RuntimeException) + } + + private CreateReportRequest createCreateReportRequest(String clusterId, long sinceMs, long toMs) { + return CreateReportRequest.builder() + .clusterId(clusterId) + .accuracy(Accuracy.HIGH) + .sinceMs(sinceMs) + .toMs(toMs) + .slackReceiverIds([]) + .emailReceiverIds([]) + .discordReceiverIds([]) + .applicationConfigurations([]) + .nodeConfigurations([]) + .build() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportScheduleServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportScheduleServiceTest.groovy new file mode 100644 index 00000000..20ef3edb --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportScheduleServiceTest.groovy @@ -0,0 +1,42 @@ +package pl.pwr.zpi.reports + +import pl.pwr.zpi.reports.dto.request.CreateReportScheduleRequest +import pl.pwr.zpi.reports.dto.scheduler.ReportSchedule +import pl.pwr.zpi.reports.repository.ReportScheduleRepository +import pl.pwr.zpi.cluster.repository.ClusterRepository +import pl.pwr.zpi.reports.service.ReportScheduleService +import spock.lang.Specification + +class ReportScheduleServiceTest extends Specification { + + def clusterRepository = Mock(ClusterRepository) + def reportScheduleRepository = Mock(ReportScheduleRepository) + def reportScheduleService = new ReportScheduleService(reportScheduleRepository, clusterRepository) + + def "should schedule report when pl.pwr.zpi.cluster exists"() { + given: + def clusterId = "cluster123" + def scheduleRequest = new CreateReportScheduleRequest(clusterId, 86400000L) + + when: + reportScheduleService.scheduleReport(scheduleRequest) + + then: + 1 * clusterRepository.existsById(clusterId) >> true + noExceptionThrown() + } + + def "should throw exception when pl.pwr.zpi.cluster does not exist"() { + given: + def clusterId = "cluster123" + def scheduleRequest = new CreateReportScheduleRequest(clusterId, 86400000L) + clusterRepository.existsById(clusterId) >> false + + when: + reportScheduleService.scheduleReport(scheduleRequest) + + then: + 1 * clusterRepository.existsById(clusterId) + thrown(IllegalArgumentException) + } +} \ No newline at end of file diff --git a/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportSchedulerTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportSchedulerTest.groovy new file mode 100644 index 00000000..596e13ae --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportSchedulerTest.groovy @@ -0,0 +1,111 @@ +package pl.pwr.zpi.reports + +import pl.pwr.zpi.reports.enums.Accuracy +import pl.pwr.zpi.reports.scheduler.ReportScheduler +import spock.lang.Specification +import pl.pwr.zpi.reports.dto.request.CreateReportRequest +import pl.pwr.zpi.reports.dto.scheduler.ReportSchedule +import pl.pwr.zpi.reports.enums.ReportType +import pl.pwr.zpi.reports.repository.ReportScheduleRepository +import pl.pwr.zpi.reports.service.ReportGenerationService +import pl.pwr.zpi.cluster.entity.ClusterConfiguration +import pl.pwr.zpi.cluster.repository.ClusterRepository + +class ReportSchedulerTest extends Specification { + + def reportScheduleRepository = Mock(ReportScheduleRepository) + def clusterRepository = Mock(ClusterRepository) + def reportGenerationService = Mock(ReportGenerationService) + + def reportScheduler = new ReportScheduler(reportScheduleRepository, clusterRepository, reportGenerationService) + + def "should generate reports for all schedules"() { + given: + def schedule1 =createReportSchedule("cluster1", 1000L, 1000L) + def schedule2 = createReportSchedule("cluster2", 2000L, 2000L) + + def clusterConfig1 = createClusterConfiguration("cluster1") + def clusterConfig2 = createClusterConfiguration("cluster2") + + reportScheduleRepository.findAll() >> [schedule1, schedule2] + clusterRepository.findById("cluster1") >> Optional.of(clusterConfig1) + clusterRepository.findById("cluster2") >> Optional.of(clusterConfig2) + + when: + reportScheduler.generateReports() + + then: + 2 * reportGenerationService.createReport(_ as CreateReportRequest, ReportType.SCHEDULED) + 2 * reportScheduleRepository.save(_ as ReportSchedule) + } + + def "should process a schedule and generate report"() { + given: + def schedule = createReportSchedule("cluster1", 1000L, 1000L) + def clusterConfig = createClusterConfiguration("cluster1") + + long nextGenerationTime = schedule.lastGenerationMs + schedule.periodMs + CreateReportRequest reportRequest = CreateReportRequest.fromClusterConfiguration(clusterConfig, schedule.lastGenerationMs, nextGenerationTime) + + reportScheduleRepository.findAll() >> [schedule] + clusterRepository.findById("cluster1") >> Optional.of(clusterConfig) + + when: + reportScheduler.generateReports() + + then: + 1 * reportGenerationService.createReport(reportRequest, ReportType.SCHEDULED) + 1 * reportScheduleRepository.save(schedule) + schedule.lastGenerationMs == nextGenerationTime + } + + def "should throw exception when pl.pwr.zpi.cluster is not found"() { + given: + def schedule = createReportSchedule("cluster1", 1000L, 1000L) + + reportScheduleRepository.findAll() >> [schedule] + clusterRepository.findById("cluster1") >> Optional.empty() + + when: + reportScheduler.processSchedule(schedule) + + then: + thrown(IllegalStateException) + } + + def "should not generate report if next generation time is in the future"() { + given: + def futureTime = System.currentTimeMillis() + 10000L + def schedule = createReportSchedule("cluster1", futureTime, 1000L) + + reportScheduleRepository.findAll() >> [schedule] + + when: + reportScheduler.generateReports() + + then: + 0 * reportGenerationService.createReport(_, _) + } + + private ReportSchedule createReportSchedule(String clusterId, long lastGenerationMs, long periodMs) { + return ReportSchedule.builder() + .clusterId(clusterId) + .lastGenerationMs(lastGenerationMs) + .periodMs(periodMs) + .build() + } + + private ClusterConfiguration createClusterConfiguration(String clusterId) { + return ClusterConfiguration.builder() + .id(clusterId) + .accuracy(Accuracy.HIGH) + .isEnabled(true) + .generatedEveryMillis(2300000L) + .slackReceivers([]) + .discordReceivers([]) + .emailReceivers([]) + .applicationConfigurations([]) + .nodeConfigurations([]) + .build() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportsServiceTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportsServiceTest.groovy new file mode 100644 index 00000000..36ba6a55 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/reports/ReportsServiceTest.groovy @@ -0,0 +1,312 @@ +package pl.pwr.zpi.reports + +import pl.pwr.zpi.reports.dto.report.* +import pl.pwr.zpi.reports.dto.report.application.ApplicationIncidentDTO +import pl.pwr.zpi.reports.dto.report.node.NodeIncidentDTO +import pl.pwr.zpi.reports.dto.request.CreateReportRequest +import pl.pwr.zpi.reports.entity.report.application.ApplicationIncident +import pl.pwr.zpi.reports.entity.report.application.ApplicationIncidentSource +import pl.pwr.zpi.reports.entity.report.node.NodeIncident +import pl.pwr.zpi.reports.entity.report.node.NodeIncidentSource +import pl.pwr.zpi.reports.entity.report.request.ReportGenerationRequestMetadata +import pl.pwr.zpi.reports.enums.Accuracy +import pl.pwr.zpi.reports.enums.ReportGenerationStatus +import pl.pwr.zpi.reports.enums.ReportType +import pl.pwr.zpi.reports.enums.Urgency +import pl.pwr.zpi.reports.repository.* +import pl.pwr.zpi.reports.repository.projection.ReportDetailedSummaryProjection +import org.springframework.data.domain.Pageable +import pl.pwr.zpi.reports.repository.projection.ReportSummaryProjection +import pl.pwr.zpi.reports.service.ReportsService +import spock.lang.Specification + + +class ReportsServiceTest extends Specification { + + ReportRepository reportRepository + NodeIncidentRepository nodeIncidentRepository + ApplicationIncidentRepository applicationIncidentRepository + ApplicationIncidentSourcesRepository applicationIncidentSourcesRepository + NodeIncidentSourcesRepository nonNodeIncidentSourcesRepository + ReportGenerationRequestMetadataRepository reportGenerationRequestMetadataRepository + + ReportsService reportsService + + def setup() { + reportRepository = Mock() + nodeIncidentRepository = Mock() + applicationIncidentRepository = Mock() + applicationIncidentSourcesRepository = Mock() + nonNodeIncidentSourcesRepository = Mock() + reportGenerationRequestMetadataRepository = Mock() + reportsService = new ReportsService(reportRepository, nodeIncidentRepository, + applicationIncidentRepository, applicationIncidentSourcesRepository, + nonNodeIncidentSourcesRepository, reportGenerationRequestMetadataRepository) + } + + def "should get failed report generation requests"() { + given: + def failedRequests = [Mock(ReportGenerationRequestMetadata), Mock(ReportGenerationRequestMetadata)] + reportGenerationRequestMetadataRepository.findByStatus(ReportGenerationStatus.ERROR) >> failedRequests + + when: + def result = reportsService.getFailedReportGenerationRequests() + + then: + result == failedRequests + } + + def "should get awaiting generation reports"() { + given: + def generatingReports = List.of( + createReportGenerationRequestMetadata("132321", ReportGenerationStatus.GENERATING, "cluster1", 1000L, 2000L), + createReportGenerationRequestMetadata("132322", ReportGenerationStatus.GENERATING, "cluster2", 1000L, 2000L)) + reportGenerationRequestMetadataRepository.findByStatus(ReportGenerationStatus.GENERATING) >> generatingReports + + when: + def result = reportsService.getAwaitingGenerationReports() + + then: + result.size() == generatingReports.size() + } + + def "should get report summaries"() { + given: + def reportSummary = Mock(ReportSummaryProjection) { + getId() >> "123" + getClusterId() >> "cluster1" + getTitle() >> "Scheduled Report" + getUrgency() >> Urgency.HIGH + getRequestedAtMs() >> System.currentTimeMillis() + getSinceMs() >> System.currentTimeMillis() - 10000L + getToMs() >> System.currentTimeMillis() + } + + def reportSummaries = [reportSummary] + reportRepository.findAllByReportType(ReportType.SCHEDULED) >> reportSummaries + + def expectedReportSummaries = reportSummaries.collect { report -> + ReportSummaryDTO.builder() + .id(report.id) + .clusterId(report.clusterId) + .title(report.title) + .urgency(report.urgency) + .requestedAtMs(report.requestedAtMs) + .sinceMs(report.sinceMs) + .toMs(report.toMs) + .build() + } + + when: + def result = reportsService.getReportSummaries("SCHEDULED") + + then: + result == expectedReportSummaries + } + + def "should get report detailed summary by id"() { + given: + def reportId = "report123" + + def reportDetailedSummaryProjection = Mock(ReportDetailedSummaryProjection) { + getId() >> reportId + getClusterId() >> "cluster1" + getTitle() >> "Detailed Report" + getUrgency() >> Urgency.HIGH + getRequestedAtMs() >> 1732461533844L + getSinceMs() >> 1732461523846L + getToMs() >> 1732461533877L + getTotalApplicationEntries() >> 10 + getTotalNodeEntries() >> 5 + getAnalyzedApplications() >> 8 + getAnalyzedNodes() >> 4 + } + + reportRepository.findProjectedDetailedById(reportId) >> Optional.of(reportDetailedSummaryProjection) + + when: + def result = reportsService.getReportDetailedSummaryById(reportId) + + then: + result.isPresent() + result.get().id == reportId + result.get().clusterId == "cluster1" + result.get().title == "Detailed Report" + result.get().urgency == Urgency.HIGH + result.get().requestedAtMs == 1732461533844L + result.get().sinceMs == 1732461523846L + result.get().toMs == 1732461533877L + result.get().totalApplicationEntries == 10 + result.get().totalNodeEntries == 5 + result.get().analyzedApplications == 8 + result.get().analyzedNodes == 4 + } + + + def "should return empty optional when report detailed summary not found"() { + given: + def reportId = "report123" + reportRepository.findProjectedDetailedById(reportId) >> Optional.empty() + + when: + def result = reportsService.getReportDetailedSummaryById(reportId) + + then: + result == Optional.empty() + } + + def "should get node incidents by report id"() { + given: + def reportId = "report123" + def pageable = Pageable.unpaged() + + def nodeIncidents = [createNodeIncident("incident123", reportId)] + def nodeIncidentDTOs = nodeIncidents.collect { NodeIncidentDTO.fromNodeIncident(it) } + + nodeIncidentRepository.findByReportId(reportId, pageable) >> nodeIncidents + nodeIncidentRepository.countByReportId(reportId) >> nodeIncidents.size() + + when: + def result = reportsService.getReportNodeIncidents(reportId, pageable) + + then: + result.data == nodeIncidentDTOs + result.totalEntries == nodeIncidents.size() + } + + def "should get application incidents by report id"() { + given: + def reportId = "report123" + def pageable = Pageable.unpaged() + + def applicationIncidents = List.of(createApplicationIncident("incident123", reportId)) + def applicationIncidentDTOs = applicationIncidents.collect { ApplicationIncidentDTO.fromApplicationIncident(it) } + + applicationIncidentRepository.findByReportId(reportId, pageable) >> applicationIncidents + applicationIncidentRepository.countByReportId(reportId) >> applicationIncidents.size() + + when: + def result = reportsService.getReportApplicationIncidents(reportId, pageable) + + then: + result.data == applicationIncidentDTOs + result.totalEntries == applicationIncidents.size() + } + + def "should get application incident sources by incident id"() { + given: + def incidentId = "incident123" + def pageable = Mock(Pageable) + def sources = [Mock(ApplicationIncidentSource)] + applicationIncidentSourcesRepository.findByIncidentId(incidentId, pageable) >> sources + applicationIncidentSourcesRepository.countByIncidentId(incidentId) >> sources.size() + + when: + def result = reportsService.getApplicationIncidentSourcesByIncidentId(incidentId, pageable) + + then: + result.data == sources + result.totalEntries == sources.size() + } + + def "should get node incident sources by incident id"() { + given: + def incidentId = "incident123" + def pageable = Mock(Pageable) + def sources = [Mock(NodeIncidentSource)] + nonNodeIncidentSourcesRepository.findByIncidentId(incidentId, pageable) >> sources + nonNodeIncidentSourcesRepository.countByIncidentId(incidentId) >> sources.size() + + when: + def result = reportsService.getNodeIncidentSourcesByIncidentId(incidentId, pageable) + + then: + result.data == sources + result.totalEntries == sources.size() + } + + def "should get application incident by id"() { + given: + def incidentId = "incident123" + def applicationIncident = createApplicationIncident("test123", incidentId) + def applicationIncidentDTO = ApplicationIncidentDTO.fromApplicationIncident(applicationIncident) + + applicationIncidentRepository.findById(incidentId) >> Optional.of(applicationIncident) + + when: + def result = reportsService.getApplicationIncidentById(incidentId) + + then: + result == Optional.of(applicationIncidentDTO) + } + + def "should get node incident by id"() { + given: + def incidentId = "incident123" + def nodeIncident = createNodeIncident("test123", incidentId) + def nodeIncidentDTO = NodeIncidentDTO.fromNodeIncident(nodeIncident) + + nodeIncidentRepository.findById(incidentId) >> Optional.of(nodeIncident) + + when: + def result = reportsService.getNodeIncidentById(incidentId) + + then: + result == Optional.of(nodeIncidentDTO) + } + + private ReportGenerationRequestMetadata createReportGenerationRequestMetadata(String correlationId, ReportGenerationStatus status, String clusterId, long sinceMs, long toMs) { + return ReportGenerationRequestMetadata.builder() + .correlationId(correlationId) + .status(status) + .createReportRequest(createCreateReportRequest(clusterId, sinceMs, toMs)) + .reportType(ReportType.SCHEDULED) + .build() + } + + private CreateReportRequest createCreateReportRequest(String clusterId, long sinceMs, long toMs) { + return CreateReportRequest.builder() + .clusterId(clusterId) + .accuracy(Accuracy.HIGH) + .sinceMs(sinceMs) + .toMs(toMs) + .slackReceiverIds([]) + .emailReceiverIds([]) + .discordReceiverIds([]) + .applicationConfigurations([]) + .nodeConfigurations([]) + .build() + } + + private ApplicationIncident createApplicationIncident(String id, String reportId) { + return ApplicationIncident.builder() + .id(id) + .reportId(reportId) + .title("Test Incident") + .accuracy(Accuracy.HIGH) + .customPrompt("Custom prompt") + .clusterId("cluster123") + .applicationName("TestApp") + .category("Category1") + .summary("Incident Summary") + .recommendation("Recommendation for incident") + .urgency(Urgency.HIGH) + .sources([]) + .build() + } + + private NodeIncident createNodeIncident(String incidentId, String reportId) { + return NodeIncident.builder() + .id(incidentId) + .reportId(reportId) + .title("Test Node Incident") + .clusterId("cluster123") + .nodeName("Node1") + .category("Node Category") + .summary("Node Incident Summary") + .recommendation("Recommendation for node incident") + .urgency(Urgency.HIGH) + .sources([]) + .build() + } +} diff --git a/management-service/src/test/groovy/pl/pwr/zpi/utils/ClientTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/utils/ClientTest.groovy new file mode 100644 index 00000000..de655ceb --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/utils/ClientTest.groovy @@ -0,0 +1,131 @@ +package pl.pwr.zpi.utils + +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import okhttp3.Call +import okhttp3.MediaType +import okhttp3.OkHttpClient +import okhttp3.Response +import okhttp3.ResponseBody +import pl.pwr.zpi.utils.client.Client +import pl.pwr.zpi.utils.exception.JsonMappingException +import spock.lang.Specification + +import java.lang.reflect.Field + +class ClientTest extends Specification { + + Client client + OkHttpClient mockHttpClient + ObjectMapper objectMapper + + def setup() { + mockHttpClient = Mock(OkHttpClient) + objectMapper = new ObjectMapper() + client = new Client() + + Field httpClientField = Client.getDeclaredField("httpClient") + httpClientField.accessible = true + httpClientField.set(client, mockHttpClient) + + Field objectMapperField = Client.getDeclaredField("objectMapper") + objectMapperField.accessible = true + objectMapperField.set(client, objectMapper) + } + + def "get should return mapped object on valid response"() { + given: + String url = "https://example.com/api/resource" + Map params = [param1: "value1", param2: "value2"] + String jsonResponse = '{"key":"value"}' + def responseMock = Mock(Response) { + isSuccessful() >> true + body() >> ResponseBody.create(MediaType.get("application/json"), jsonResponse) + } + mockHttpClient.newCall(_) >> Mock(Call) { + execute() >> responseMock + } + + when: + Map result = client.get(url, params, Map) + + then: + result.key == "value" + } + + def "getList should return list of mapped objects on valid response"() { + given: + String url = "https://example.com/api/resource" + Map params = [:] + String jsonResponse = '[{"key":"value1"}, {"key":"value2"}]' + def responseMock = Mock(Response) { + isSuccessful() >> true + body() >> ResponseBody.create(MediaType.get("application/json"), jsonResponse) + } + mockHttpClient.newCall(_) >> Mock(Call) { + execute() >> responseMock + } + + when: + List result = client.getList(url, params, new TypeReference>() {}) + + then: + result.size() == 2 + result[0].key == "value1" + result[1].key == "value2" + } + + def "get should throw JsonMappingException on invalid JSON"() { + given: + String url = "https://example.com/api/resource" + Map params = [:] + String invalidJsonResponse = 'invalid-json' + def responseMock = Mock(Response) { + isSuccessful() >> true + body() >> ResponseBody.create(MediaType.get("application/json"), invalidJsonResponse) + } + mockHttpClient.newCall(_) >> Mock(Call) { + execute() >> responseMock + } + + when: + client.get(url, params, Map) + + then: + thrown(JsonMappingException) + } + + def "sendGetRequest should throw RuntimeException on unsuccessful response"() { + given: + String url = "https://example.com/api/resource" + def responseMock = Mock(Response) { + isSuccessful() >> false + code() >> 500 + } + mockHttpClient.newCall(_) >> Mock(Call) { + execute() >> responseMock + } + + when: + client.get(url, [:], Map) + + then: + RuntimeException ex = thrown() + ex.message == "Failed to fetch the resource" + } + + def "sendGetRequest should throw RuntimeException on IOException"() { + given: + String url = "https://example.com/api/resource" + mockHttpClient.newCall(_) >> Mock(Call) { + execute() >> { throw new IOException("Test IO Exception") } + } + + when: + client.get(url, [:], Map) + + then: + RuntimeException ex = thrown() + ex.message == "Error fetching resource" + } +} \ No newline at end of file diff --git a/management-service/src/test/groovy/pl/pwr/zpi/utils/JsonMapperTest.groovy b/management-service/src/test/groovy/pl/pwr/zpi/utils/JsonMapperTest.groovy new file mode 100644 index 00000000..b5e3a052 --- /dev/null +++ b/management-service/src/test/groovy/pl/pwr/zpi/utils/JsonMapperTest.groovy @@ -0,0 +1,60 @@ +package pl.pwr.zpi.utils + +import pl.pwr.zpi.utils.exception.JsonMappingException +import pl.pwr.zpi.utils.mapper.JsonMapper +import spock.lang.Specification + +class JsonMapperTest extends Specification { + + JsonMapper jsonMapper + + def setup() { + jsonMapper = new JsonMapper() + } + + def "fromJson should correctly deserialize a valid JSON string into an object"() { + given: + String json = '{"name": "John Doe", "age": 30}' + + when: + Person person = jsonMapper.fromJson(json, Person) + + then: + person.name == "John Doe" + person.age == 30 + } + + def "fromJson should throw JsonMappingException when JSON string is invalid"() { + given: + String invalidJson = '{name: "John Doe", age: }' + + when: + jsonMapper.fromJson(invalidJson, Person) + + then: + JsonMappingException ex = thrown() + ex.message.contains("Unexpected character") + } + + def "fromJson should throw JsonMappingException for incompatible JSON and target class"() { + given: + String json = '{"name": "John Doe", "age": 30}' + + when: + jsonMapper.fromJson(json, DifferentClass) + + then: + JsonMappingException ex = thrown() + ex.message.contains("Unrecognized field") + } + + + static class Person { + String name + int age + } + + static class DifferentClass { + String firstName + } +} \ No newline at end of file diff --git a/management-service/src/test/java/pl/pwr/zpi/MagpieMonitorApplicationTests.java b/management-service/src/test/java/pl/pwr/zpi/MagpieMonitorApplicationTests.java deleted file mode 100644 index 8cdccdcf..00000000 --- a/management-service/src/test/java/pl/pwr/zpi/MagpieMonitorApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package pl.pwr.zpi; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class MagpieMonitorApplicationTests { - - @Test - void contextLoads() { - } - -}