- 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 |