Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

-dontwarn com.google.errorprone.**
-dontwarn com.sun.jna.**
-dontwarn com.sun.net.httpserver.**
-dontwarn edu.umd.cs.findbugs.**
-dontwarn java.lang.instrument.**
-dontwarn java.lang.management.**
Expand Down
2 changes: 2 additions & 0 deletions sentry-test-support/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,6 @@ dependencies {
implementation(kotlin(Config.kotlinStdLib))
implementation(libs.kotlin.test.junit)
implementation(libs.mockito.kotlin)

testImplementation(libs.google.truth)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package io.sentry.test

import com.sun.net.httpserver.HttpExchange
import com.sun.net.httpserver.HttpServer
import io.sentry.JsonSerializer
import io.sentry.SentryEnvelope
import io.sentry.SentryOptions
import io.sentry.time.Deadline
import io.sentry.time.JavaMonotonicTicker
import java.io.Closeable
import java.net.InetAddress
import java.net.InetSocketAddress
import java.util.concurrent.CopyOnWriteArrayList
import java.util.concurrent.TimeUnit
import java.util.zip.GZIPInputStream

/**
* A stand-in for Sentry's ingestion endpoint that integration tests point the SDK at through [dsn].
*
* Envelopes travel through the real HTTP transport, so serialization, compression and the DSN are
* covered too. It binds a free port so that test tasks running in parallel don't clash, and it
* works for an app in a child process as well as in the test's own JVM.
*/
class MockSentryServer : Closeable {
private val serializer = JsonSerializer(SentryOptions.empty())
private val received = CopyOnWriteArrayList<SentryEnvelope>()
private val server =
HttpServer.create(InetSocketAddress(InetAddress.getByName(HOST), 0), 0).apply {
createContext("/api/") { exchange -> receive(exchange) }
start()
}

val dsn: String = "http://key@$HOST:${server.address.port}/0"

val envelopes: List<SentryEnvelope>
get() = received.toList()

/** Returns the first received envelope matching [predicate], waiting up to [timeoutMillis]. */
fun awaitEnvelope(
timeoutMillis: Long = DEFAULT_TIMEOUT_MILLIS,
predicate: (SentryEnvelope) -> Boolean,
): SentryEnvelope {
val deadline =
Deadline.after(JavaMonotonicTicker.getInstance(), timeoutMillis, TimeUnit.MILLISECONDS)
while (true) {
received.firstOrNull(predicate)?.let {
return it
}
if (deadline.hasPassed()) {
throw AssertionError(
"No matching envelope within ${timeoutMillis}ms, received ${received.size} envelopes"
)
}
Thread.sleep(POLL_INTERVAL_MILLIS)
}
}

fun reset() {
received.clear()
}

override fun close() {
server.stop(0)
}

private fun receive(exchange: HttpExchange) {
try {
val body =
if (exchange.requestHeaders.getFirst("Content-Encoding") == "gzip") {
GZIPInputStream(exchange.requestBody)
} else {
exchange.requestBody
}
serializer.deserializeEnvelope(body)?.let { received.add(it) }
exchange.sendResponseHeaders(200, -1)
} finally {
exchange.close()
}
}

private companion object {
// An IP rather than "localhost", which can resolve to IPv4 for the server and IPv6 for the SDK.
const val HOST = "127.0.0.1"
const val DEFAULT_TIMEOUT_MILLIS = 10_000L
const val POLL_INTERVAL_MILLIS = 50L
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.sentry.test

import com.google.common.truth.Truth.assertThat
import io.sentry.Sentry
import io.sentry.SentryItemType
import kotlin.test.AfterTest
import kotlin.test.Test
import kotlin.test.assertFailsWith

class MockSentryServerTest {
private val server = MockSentryServer()

@AfterTest
fun teardown() {
Sentry.close()
server.close()
}

@Test
fun `receives an event sent through the HTTP transport`() {
initForTest { it.dsn = server.dsn }

val eventId = Sentry.captureMessage("hello")
Sentry.flush(5000)

val envelope = server.awaitEnvelope { it.header.eventId == eventId }
assertThat(envelope.items.single().header.type).isEqualTo(SentryItemType.Event)
}

@Test
fun `awaitEnvelope fails once the timeout passes without a match`() {
assertFailsWith<AssertionError> { server.awaitEnvelope(timeoutMillis = 100) { true } }
}
}
Loading