Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add sentry integration & e2e tests #78

Merged
merged 25 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,17 @@
package io.sentry.kotlin.multiplatform

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

@RunWith(AndroidJUnit4::class)
@Config(sdk = [28])
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
actual abstract class BaseSentryTest {
actual val platform: String = "Android"
actual val authToken: String? = System.getenv("SENTRY_AUTH_TOKEN")
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
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? = ""
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,130 @@
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.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.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<TagSerializable> = listOf(),
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
val fingerprint: List<String> = listOf(),
val level: String? = null,
val logger: String? = null,
val title: String? = null
)

@Serializable
private data class TagSerializable(
val key: String? = null,
val value: 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)
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/sentry-sdks/sentry-kotlin-multiplatform/events/$eventId/"
buenaflor marked this conversation as resolved.
Show resolved Hide resolved
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())
assertEquals(eventId.toString(), fetchedEvent.id)
assertEquals(sentEvent?.message?.formatted, fetchedEvent.message)
assertEquals(message, fetchedEvent.title)
assertEquals(sentEvent?.release, fetchedEvent.release)
assertEquals(sentEvent?.environment, fetchedEvent.tags.find { it.key == "environment" }?.value)
assertEquals(sentEvent?.fingerprint?.toList(), fetchedEvent.fingerprint)
assertEquals(sentEvent?.level?.name?.lowercase(), fetchedEvent.tags.find { it.key == "level" }?.value)
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(sentEvent?.environment, fetchedEvent.tags.find { it.key == "environment" }?.value)
assertEquals(sentEvent?.fingerprint?.toList(), fetchedEvent.fingerprint)
assertEquals(SentryLevel.ERROR.toString().lowercase(), fetchedEvent.tags.find { it.key == "level" }?.value)
assertEquals(sentEvent?.logger, fetchedEvent.logger)
}
}

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