Skip to content

Commit

Permalink
test: add sentry integration & e2e tests (#78)
Browse files Browse the repository at this point in the history
* poc

* poc for integration test

* add Sentry integration test

* setup basic e2e test for jvm and android

* add working e2e test without auth token

* use real dsn for e2e test

* change e2e test names

* add elvis operator for auth token

* add env secret to build

* embed SENTRY_AUTH_TOKEN and add docs why apple e2e tests are disabled for now

* use SENTRY_AUTH_TOKEN instead of AUTH_TOKEN

* remove unused code

* fix typo

* remove resources

* remove unused code

* revert code

* add auth token assert with message

* refactor deps to buildSrc

* add more serializable fields

* use darwing ktor client

* improve fakeDsn and test

* omit roboelectric config

* add improvements
  • Loading branch information
buenaflor authored Apr 25, 2023
1 parent 8a56124 commit 489053a
Show file tree
Hide file tree
Showing 12 changed files with 479 additions and 2 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
# Clean, build
- name: Make all
run: make all
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}

# We stop gradle at the end to make sure the cache folders
# don't contain any lock files and are free to be cached.
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ plugins {
id(Config.jetpackCompose).version(Config.composeVersion).apply(false)
id(Config.androidGradle).version(Config.agpVersion).apply(false)
id(Config.BuildPlugins.buildConfig).version(Config.BuildPlugins.buildConfigVersion).apply(false)
kotlin(Config.kotlinSerializationPlugin).version(Config.kotlinVersion).apply(false)
}

allprojects {
Expand Down
12 changes: 12 additions & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ object Config {
val jetpackCompose = "org.jetbrains.compose"
val gradleMavenPublishPlugin = "com.vanniktech.maven.publish"
val androidGradle = "com.android.library"
val kotlinSerializationPlugin = "plugin.serialization"

object BuildPlugins {
val buildConfig = "com.codingfeline.buildkonfig"
Expand All @@ -35,6 +36,17 @@ object Config {
val kotlinCommon = "org.jetbrains.kotlin:kotlin-test-common"
val kotlinCommonAnnotation = "org.jetbrains.kotlin:kotlin-test-annotations-common"
val kotlinJunit = "org.jetbrains.kotlin:kotlin-test-junit"
val kotlinCoroutinesCore = "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.0-RC"
val kotlinCoroutinesTest = "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.0-RC"
val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0"

val ktorClientCore = "io.ktor:ktor-client-core:2.3.0"
val ktorClientSerialization = "io.ktor:ktor-client-serialization:2.3.0"
val ktorClientOkHttp = "io.ktor:ktor-client-okhttp:2.3.0"
val ktorClientDarwin = "io.ktor:ktor-client-darwin:2.3.0"

val roboelectric = "org.robolectric:robolectric:4.9"
val junitKtx = "androidx.test.ext:junit-ktx:1.1.5"
}

object Android {
Expand Down
19 changes: 17 additions & 2 deletions sentry-kotlin-multiplatform/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@ plugins {
kotlin(Config.cocoapods)
id(Config.androidGradle)
id(Config.BuildPlugins.buildConfig)
kotlin(Config.kotlinSerializationPlugin)
`maven-publish`
}

android {
compileSdk = Config.Android.compileSdkVersion
defaultConfig {
minSdk = Config.Android.minSdkVersion
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
Expand Down Expand Up @@ -46,6 +47,11 @@ kotlin {
}
val commonTest by getting {
dependencies {
implementation(Config.TestLibs.kotlinCoroutinesCore)
implementation(Config.TestLibs.kotlinCoroutinesTest)
implementation(Config.TestLibs.ktorClientCore)
implementation(Config.TestLibs.ktorClientSerialization)
implementation(Config.TestLibs.kotlinxSerializationJson)
implementation(Config.TestLibs.kotlinCommon)
implementation(Config.TestLibs.kotlinCommonAnnotation)
}
Expand All @@ -56,7 +62,12 @@ kotlin {
implementation(Config.Libs.sentryAndroid)
}
}
val androidUnitTest by getting
val androidUnitTest by getting {
dependencies {
implementation(Config.TestLibs.roboelectric)
implementation(Config.TestLibs.junitKtx)
}
}
val jvmMain by getting
val jvmTest by getting

Expand All @@ -74,6 +85,7 @@ kotlin {
androidUnitTest.dependsOn(this)
dependencies {
implementation(Config.TestLibs.kotlinJunit)
implementation(Config.TestLibs.ktorClientOkHttp)
}
}

Expand Down Expand Up @@ -145,6 +157,9 @@ kotlin {
dependsOn(commonTest)
commonIosTest.dependsOn(this)
commonTvWatchMacOsTest.dependsOn(this)
dependencies {
implementation(Config.TestLibs.ktorClientDarwin)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.sentry.kotlin.multiplatform

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.runner.RunWith

@RunWith(AndroidJUnit4::class)
actual abstract class BaseSentryTest {
actual val platform: String = "Android"
actual val authToken: String? = System.getenv("SENTRY_AUTH_TOKEN")
actual fun sentryInit(optionsConfiguration: OptionsConfiguration) {
val context = InstrumentationRegistry.getInstrumentation().targetContext
Sentry.init(context, optionsConfiguration)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.sentry.kotlin.multiplatform

actual abstract class BaseSentryTest {
actual val platform: String = "Apple"
actual val authToken: String? = "fake-auth-token"
actual fun sentryInit(optionsConfiguration: OptionsConfiguration) {
Sentry.init(optionsConfiguration)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package io.sentry.kotlin.multiplatform

expect abstract class BaseSentryTest() {
val platform: String
val authToken: String?
fun sentryInit(optionsConfiguration: OptionsConfiguration)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package io.sentry.kotlin.multiplatform

import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.request.headers
import io.ktor.client.statement.bodyAsText
import io.ktor.http.HttpHeaders
import io.sentry.kotlin.multiplatform.utils.org
import io.sentry.kotlin.multiplatform.utils.projectSlug
import io.sentry.kotlin.multiplatform.utils.realDsn
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.withContext
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
import kotlin.time.Duration.Companion.seconds

@Serializable
private data class SentryEventSerializable(
val id: String? = null,
val project: Long? = null,
val release: String? = null,
val platform: String? = null,
val message: String? = "",
val tags: List<Map<String, String>> = listOf(),
val fingerprint: List<String> = listOf(),
val level: String? = null,
val logger: String? = null,
val title: String? = null
)

class SentryE2ETest : BaseSentryTest() {
private val client = HttpClient()
private val jsonDecoder = Json { ignoreUnknownKeys = true }
private var sentEvent: SentryEvent? = null

@BeforeTest
fun setup() {
assertNotNull(authToken)
assertTrue(authToken.isNotEmpty())
sentryInit { options ->
options.dsn = realDsn
options.beforeSend = { event ->
sentEvent = event
event
}
}
}

private suspend fun fetchEvent(eventId: String): String {
val url =
"https://sentry.io/api/0/projects/$org/$projectSlug/events/$eventId/"
val response = client.get(url) {
headers {
append(
HttpHeaders.Authorization,
"Bearer $authToken"
)
}
}
return response.bodyAsText()
}

private suspend fun waitForEventRetrieval(eventId: String): SentryEventSerializable {
var json = ""
val result: SentryEventSerializable = withContext(Dispatchers.Default) {
while (json.isEmpty() || json.contains("Event not found")) {
delay(5000)
json = fetchEvent(eventId)
assertFalse(json.contains("Invalid token"), "Invalid auth token")
}
jsonDecoder.decodeFromString(json)
}
return result
}

// TODO: e2e tests are currently disabled for Apple targets as there are SSL issues that prevent sending events in tests
// See: https://github.com/getsentry/sentry-kotlin-multiplatform/issues/17

@Test
fun `capture message and fetch event from Sentry`() = runTest(timeout = 30.seconds) {
if (platform != "Apple") {
val message = "Test running on $platform"
val eventId = Sentry.captureMessage(message)
val fetchedEvent = waitForEventRetrieval(eventId.toString())
fetchedEvent.tags.forEach { println(it["value"]) }
assertEquals(eventId.toString(), fetchedEvent.id)
assertEquals(sentEvent?.message?.formatted, fetchedEvent.message)
assertEquals(message, fetchedEvent.title)
assertEquals(sentEvent?.release, fetchedEvent.release)
assertEquals(2, fetchedEvent.tags.find { it["value"] == sentEvent?.environment }?.size)
assertEquals(sentEvent?.fingerprint?.toList(), fetchedEvent.fingerprint)
assertEquals(2, fetchedEvent.tags.find { it["value"] == sentEvent?.level?.name?.lowercase() }?.size)
assertEquals(sentEvent?.logger, fetchedEvent.logger)
}
}

@Test
fun `capture exception and fetch event from Sentry`() = runTest(timeout = 30.seconds) {
if (platform != "Apple") {
val exceptionMessage = "Test exception on platform $platform"
val eventId =
Sentry.captureException(IllegalArgumentException(exceptionMessage))
val fetchedEvent = waitForEventRetrieval(eventId.toString())
assertEquals(eventId.toString(), fetchedEvent.id)
assertEquals("IllegalArgumentException: $exceptionMessage", fetchedEvent.title)
assertEquals(sentEvent?.release, fetchedEvent.release)
assertEquals(2, fetchedEvent.tags.find { it["value"] == sentEvent?.environment }?.size)
assertEquals(sentEvent?.fingerprint?.toList(), fetchedEvent.fingerprint)
assertEquals(2, fetchedEvent.tags.find { it["value"] == SentryLevel.ERROR.toString().lowercase() }?.size)
assertEquals(sentEvent?.logger, fetchedEvent.logger)
}
}

@AfterTest
fun tearDown() {
sentEvent = null
Sentry.close()
}
}
Loading

0 comments on commit 489053a

Please sign in to comment.