git.iko.soy/opencode-android
on 44de46e help sign in
102 lines 3.4 kB history raw
1package soy.iko.opencode.data.network
3import io.ktor.client.HttpClient
4import io.ktor.client.engine.mock.MockEngine
5import io.ktor.client.engine.mock.respond
6import io.ktor.client.plugins.ClientRequestException
7import io.ktor.client.plugins.ServerResponseException
8import io.ktor.client.request.get
9import io.ktor.http.HttpStatusCode
10import io.ktor.http.headersOf
11import kotlinx.coroutines.CancellationException
12import kotlinx.coroutines.test.runTest
13import org.junit.Assert.assertEquals
14import org.junit.Assert.assertTrue
15import org.junit.Test
16import java.io.IOException
18class WithRetryTest {
20 /** Produce the real exception Ktor throws for a status when expectSuccess = true. */
21 private suspend fun httpError(status: HttpStatusCode): Throwable {
22 val client = HttpClient(MockEngine { respond("", status, headersOf("Content-Length", "0")) }) {
23 expectSuccess = true
24 }
25 return client.use { runCatching { it.get("test") }.exceptionOrNull()!! }
26 }
28 @Test
29 fun returnsValueOnFirstAttempt() = runTest {
30 var calls = 0
31 val result = withRetryInternal { calls++; "ok" }
32 assertEquals("ok", result)
33 assertEquals(1, calls)
34 }
36 @Test
37 fun retriesTransientFailureThenSucceeds() = runTest {
38 var calls = 0
39 val result = withRetryInternal(initialDelayMs = 0) {
40 calls++
41 if (calls < 3) throw IOException("boom")
42 "ok"
43 }
44 assertEquals("ok", result)
45 assertEquals(3, calls)
46 }
48 @Test
49 fun throwsAfterExhaustingAttempts() = runTest {
50 var calls = 0
51 val error = runCatching {
52 withRetryInternal(maxAttempts = 3, initialDelayMs = 0) {
53 calls++
54 throw IOException("always")
55 }
56 }.exceptionOrNull()!!
57 assertTrue("expected IOException, got $error", error is IOException)
58 assertEquals(3, calls)
59 }
61 @Test
62 fun clientErrorIsNotRetried() = runTest {
63 val notFound = httpError(HttpStatusCode.NotFound)
64 assertTrue("expected ClientRequestException, got $notFound", notFound is ClientRequestException)
65 var calls = 0
66 val error = runCatching {
67 withRetryInternal(initialDelayMs = 0) {
68 calls++
69 throw notFound
70 }
71 }.exceptionOrNull()!!
72 assertEquals("4xx must not be retried", 1, calls)
73 assertTrue(error is ClientRequestException)
74 }
76 @Test
77 fun serverErrorIsRetried() = runTest {
78 val serverError = httpError(HttpStatusCode.InternalServerError)
79 assertTrue("expected ServerResponseException, got $serverError", serverError is ServerResponseException)
80 var calls = 0
81 runCatching {
82 withRetryInternal(maxAttempts = 3, initialDelayMs = 0) {
83 calls++
84 throw serverError
85 }
86 }
87 assertEquals("5xx must be retried", 3, calls)
88 }
90 @Test
91 fun cancellationIsRethrownImmediately() = runTest {
92 var calls = 0
93 val error = runCatching {
94 withRetryInternal(initialDelayMs = 0) {
95 calls++
96 throw CancellationException("cancel")
97 }
98 }.exceptionOrNull()
99 assertEquals("cancellation must not be retried", 1, calls)
100 assertTrue("expected CancellationException, got $error", error is CancellationException)
101 }