diff --git a/.github/workflows/leanix-github-agent-code-coverage.yml b/.github/workflows/leanix-github-agent-code-coverage.yml index 2cbc7ba..6198d94 100644 --- a/.github/workflows/leanix-github-agent-code-coverage.yml +++ b/.github/workflows/leanix-github-agent-code-coverage.yml @@ -31,6 +31,17 @@ jobs: arguments: build build-root-directory: . + - name: Report Test Results + uses: dorny/test-reporter@v1 + with: + name: Unit Tests + path: | + ${{ github.workspace }}/build/test-results/test/*.xml + reporter: java-junit + fail-on-error: true + fail-on-empty: true + only-summary: false + - name: Add coverage to PR id: jacoco uses: madrapps/jacoco-report@v1.3 diff --git a/src/main/kotlin/net/leanix/githubagent/controllers/advice/GlobalExceptionHandler.kt b/src/main/kotlin/net/leanix/githubagent/controllers/advice/GlobalExceptionHandler.kt index 83dd6b5..9f7577f 100644 --- a/src/main/kotlin/net/leanix/githubagent/controllers/advice/GlobalExceptionHandler.kt +++ b/src/main/kotlin/net/leanix/githubagent/controllers/advice/GlobalExceptionHandler.kt @@ -2,6 +2,7 @@ package net.leanix.githubagent.controllers.advice import net.leanix.githubagent.exceptions.InvalidEventSignatureException import net.leanix.githubagent.exceptions.WebhookSecretNotSetException +import net.leanix.githubagent.services.SyncLogService import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.http.HttpStatus @@ -10,22 +11,28 @@ import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler @ControllerAdvice -class GlobalExceptionHandler { +class GlobalExceptionHandler( + private val syncLogService: SyncLogService +) { val exceptionLogger: Logger = LoggerFactory.getLogger(GlobalExceptionHandler::class.java) @ExceptionHandler(InvalidEventSignatureException::class) fun handleInvalidEventSignatureException(exception: InvalidEventSignatureException): ProblemDetail { - val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, "Invalid event signature") + val detail = "Received event with an invalid signature" + val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.UNAUTHORIZED, detail) problemDetail.title = exception.message exceptionLogger.warn(exception.message) + syncLogService.sendErrorLog(detail) return problemDetail } @ExceptionHandler(WebhookSecretNotSetException::class) fun handleWebhookSecretNotSetException(exception: WebhookSecretNotSetException): ProblemDetail { - val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Webhook secret not set") + val detail = "Unable to process GitHub event. Webhook secret not set" + val problemDetail = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, detail) problemDetail.title = exception.message + syncLogService.sendErrorLog(detail) return problemDetail } } diff --git a/src/main/kotlin/net/leanix/githubagent/dto/SyncLogDto.kt b/src/main/kotlin/net/leanix/githubagent/dto/SyncLogDto.kt new file mode 100644 index 0000000..e1f3319 --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/dto/SyncLogDto.kt @@ -0,0 +1,23 @@ +package net.leanix.githubagent.dto + +import java.util.UUID + +data class SyncLogDto( + val runId: UUID?, + val trigger: Trigger, + val logLevel: LogLevel, + val message: String +) + +enum class Trigger { + START_FULL_SYNC, + FINISH_FULL_SYNC, + GENERIC +} + +enum class LogLevel { + OK, + WARNING, + INFO, + ERROR +} diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt index 1e2bcdd..6cd401e 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubAuthenticationService.kt @@ -6,6 +6,7 @@ import net.leanix.githubagent.client.GitHubClient import net.leanix.githubagent.config.GitHubEnterpriseProperties import net.leanix.githubagent.dto.Installation import net.leanix.githubagent.exceptions.FailedToCreateJWTException +import net.leanix.githubagent.exceptions.GitHubAppInsufficientPermissionsException import org.bouncycastle.jce.provider.BouncyCastleProvider import org.slf4j.LoggerFactory import org.springframework.core.io.ResourceLoader @@ -28,6 +29,7 @@ class GitHubAuthenticationService( private val resourceLoader: ResourceLoader, private val gitHubEnterpriseService: GitHubEnterpriseService, private val gitHubClient: GitHubClient, + private val syncLogService: SyncLogService, ) { companion object { @@ -58,6 +60,11 @@ class GitHubAuthenticationService( cachingService.set("jwtToken", jwt.getOrThrow(), JWT_EXPIRATION_DURATION) }.onFailure { logger.error("Failed to generate/validate JWT token", it) + if (it is GitHubAppInsufficientPermissionsException) { + syncLogService.sendErrorLog(it.message.toString()) + } else { + syncLogService.sendErrorLog("Failed to generate/validate JWT token") + } if (it is InvalidKeySpecException) { throw IllegalArgumentException("The provided private key is not in a valid PKCS8 format.", it) } else { diff --git a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt index 41f9339..77ad4e4 100644 --- a/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt +++ b/src/main/kotlin/net/leanix/githubagent/services/GitHubScanningService.kt @@ -15,7 +15,8 @@ class GitHubScanningService( private val cachingService: CachingService, private val webSocketService: WebSocketService, private val gitHubGraphQLService: GitHubGraphQLService, - private val gitHubAuthenticationService: GitHubAuthenticationService + private val gitHubAuthenticationService: GitHubAuthenticationService, + private val syncLogService: SyncLogService ) { private val logger = LoggerFactory.getLogger(GitHubScanningService::class.java) @@ -30,8 +31,10 @@ class GitHubScanningService( fetchAndSendRepositoriesData(installation) } }.onFailure { + val message = "Error while scanning GitHub resources" + syncLogService.sendErrorLog(message) cachingService.remove("runId") - logger.error("Error while scanning GitHub resources") + logger.error(message) throw it } } diff --git a/src/main/kotlin/net/leanix/githubagent/services/SyncLogService.kt b/src/main/kotlin/net/leanix/githubagent/services/SyncLogService.kt new file mode 100644 index 0000000..aff569c --- /dev/null +++ b/src/main/kotlin/net/leanix/githubagent/services/SyncLogService.kt @@ -0,0 +1,33 @@ +package net.leanix.githubagent.services + +import net.leanix.githubagent.dto.LogLevel +import net.leanix.githubagent.dto.SyncLogDto +import net.leanix.githubagent.dto.Trigger +import net.leanix.githubagent.shared.LOGS_TOPIC +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class SyncLogService( + private val webSocketService: WebSocketService, + private val cachingService: CachingService +) { + fun sendErrorLog(message: String) { + sendSyncLog(message, LOGS_TOPIC, null, LogLevel.ERROR) + } + + fun sendSyncLog(message: String, topic: String, trigger: Trigger?, logLevel: LogLevel) { + val runId = cachingService.get("runId") as UUID + val syncLogDto = SyncLogDto( + runId = runId, + trigger = trigger ?: Trigger.GENERIC, + logLevel = logLevel, + message = message + ) + webSocketService.sendMessage(constructTopic(topic), syncLogDto) + } + + private fun constructTopic(topic: String): String { + return "${cachingService.get("runId")}/$topic" + } +} diff --git a/src/main/kotlin/net/leanix/githubagent/shared/Constants.kt b/src/main/kotlin/net/leanix/githubagent/shared/Constants.kt index d90231a..4ae4046 100644 --- a/src/main/kotlin/net/leanix/githubagent/shared/Constants.kt +++ b/src/main/kotlin/net/leanix/githubagent/shared/Constants.kt @@ -2,6 +2,7 @@ package net.leanix.githubagent.shared const val TOPIC_PREFIX = "/app/ghe/" const val APP_NAME_TOPIC = "appName" +const val LOGS_TOPIC = "logs" enum class ManifestFileName(val fileName: String) { YAML("leanix.yaml"), diff --git a/src/test/kotlin/net/leanix/githubagent/controllers/GitHubWebhookControllerTest.kt b/src/test/kotlin/net/leanix/githubagent/controllers/GitHubWebhookControllerTest.kt index b68320d..e96ca50 100644 --- a/src/test/kotlin/net/leanix/githubagent/controllers/GitHubWebhookControllerTest.kt +++ b/src/test/kotlin/net/leanix/githubagent/controllers/GitHubWebhookControllerTest.kt @@ -4,6 +4,8 @@ import com.ninjasquad.springmockk.MockkBean import io.mockk.every import net.leanix.githubagent.exceptions.WebhookSecretNotSetException import net.leanix.githubagent.services.GitHubWebhookHandler +import net.leanix.githubagent.services.SyncLogService +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest @@ -20,6 +22,14 @@ class GitHubWebhookControllerTest { @MockkBean private lateinit var gitHubWebhookHandler: GitHubWebhookHandler + @MockkBean + private lateinit var syncLogService: SyncLogService + + @BeforeEach + fun setUp() { + every { syncLogService.sendErrorLog(any()) } returns Unit + } + @Test fun `should return 202 if webhook event is processed successfully`() { val eventType = "PUSH" diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt index 8543776..28ab72a 100644 --- a/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubAuthenticationServiceTest.kt @@ -2,10 +2,13 @@ package net.leanix.githubagent.services import io.mockk.every import io.mockk.mockk +import io.mockk.verify import net.leanix.githubagent.client.GitHubClient import net.leanix.githubagent.config.GitHubEnterpriseProperties +import net.leanix.githubagent.exceptions.UnableToConnectToGitHubEnterpriseException import org.junit.jupiter.api.Assertions.assertNotNull import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertDoesNotThrow import org.springframework.core.io.ClassPathResource @@ -18,14 +21,21 @@ class GitHubAuthenticationServiceTest { private val resourceLoader = mockk() private val gitHubEnterpriseService = mockk() private val gitHubClient = mockk() + private val syncLogService = mockk() private val githubAuthenticationService = GitHubAuthenticationService( cachingService, githubEnterpriseProperties, resourceLoader, gitHubEnterpriseService, - gitHubClient + gitHubClient, + syncLogService ) + @BeforeEach + fun setUp() { + every { syncLogService.sendErrorLog(any()) } returns Unit + } + @Test fun `generateJwtToken with valid data should not throw exception`() { every { cachingService.get(any()) } returns "dummy-value" @@ -46,4 +56,18 @@ class GitHubAuthenticationServiceTest { assertThrows(IllegalArgumentException::class.java) { githubAuthenticationService.generateAndCacheJwtToken() } } + + @Test + fun `generateJwtToken should send error log when throwing an exception`() { + every { cachingService.get(any()) } returns "dummy-value" + every { cachingService.set(any(), any(), any()) } returns Unit + every { githubEnterpriseProperties.pemFile } returns "valid-private-key.pem" + every { resourceLoader.getResource(any()) } returns ClassPathResource("valid-private-key.pem") + every { gitHubEnterpriseService.verifyJwt(any()) } throws UnableToConnectToGitHubEnterpriseException("") + + assertThrows(UnableToConnectToGitHubEnterpriseException::class.java) { + githubAuthenticationService.generateAndCacheJwtToken() + } + verify(exactly = 1) { syncLogService.sendErrorLog("Failed to generate/validate JWT token") } + } } diff --git a/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt b/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt index 7ff87ec..eec53a1 100644 --- a/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt +++ b/src/test/kotlin/net/leanix/githubagent/services/GitHubScanningServiceTest.kt @@ -24,12 +24,14 @@ class GitHubScanningServiceTest { private val webSocketService = mockk(relaxUnitFun = true) private val gitHubGraphQLService = mockk() private val gitHubAuthenticationService = mockk() + private val syncLogService = mockk() private val gitHubScanningService = GitHubScanningService( gitHubClient, cachingService, webSocketService, gitHubGraphQLService, - gitHubAuthenticationService + gitHubAuthenticationService, + syncLogService ) private val runId = UUID.randomUUID() @@ -50,6 +52,7 @@ class GitHubScanningServiceTest { ) every { cachingService.remove(any()) } returns Unit every { gitHubAuthenticationService.generateAndCacheInstallationTokens(any(), any()) } returns Unit + every { syncLogService.sendErrorLog(any()) } returns Unit } @Test