react-native-audio-browser 0.2.0-next.10 → 0.2.0-next.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +6 -5
- package/android/gradle.properties +1 -1
- package/android/src/main/java/com/audiobrowser/tls/AiaCertChaser.kt +46 -5
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingExtendedTrustManager.kt +86 -27
- package/android/src/main/java/com/audiobrowser/tls/AiaChasingTrustManager.kt +58 -14
- package/android/src/main/java/com/audiobrowser/tls/AiaTls.kt +20 -6
- package/android/src/main/java/com/audiobrowser/tls/CachingCertificateFetcher.kt +249 -86
- package/android/src/test/java/com/audiobrowser/TestFixtures.kt +52 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerLayerResolutionTest.kt +6 -0
- package/android/src/test/java/com/audiobrowser/browser/BrowserManagerTest.kt +167 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaCertChaserTest.kt +152 -8
- package/android/src/test/java/com/audiobrowser/tls/AiaChasingTrustManagerTest.kt +218 -10
- package/android/src/test/java/com/audiobrowser/tls/CachingCertificateFetcherCallSiteTest.kt +322 -0
- package/ios/Model/NitroTypeStubs.swift +18 -2
- package/ios/Tests/QueueManagerTests.swift +4 -4
- package/package.json +3 -2
- package/src/nitro-stub-drift.test.ts +237 -0
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
package com.audiobrowser.browser
|
|
2
2
|
|
|
3
|
+
import com.audiobrowser.TestFixtures.imageRowItem
|
|
4
|
+
import com.audiobrowser.TestFixtures.resolvedTrack
|
|
5
|
+
import com.audiobrowser.TestFixtures.staticRoute
|
|
6
|
+
import com.audiobrowser.TestFixtures.track
|
|
7
|
+
import com.audiobrowser.util.BrowserPathHelper
|
|
3
8
|
import kotlinx.coroutines.runBlocking
|
|
4
|
-
import org.junit.Assert
|
|
9
|
+
import org.junit.Assert.assertEquals
|
|
10
|
+
import org.junit.Assert.assertNull
|
|
11
|
+
import org.junit.Assert.fail
|
|
5
12
|
import org.junit.Before
|
|
6
13
|
import org.junit.Test
|
|
7
14
|
import org.junit.runner.RunWith
|
|
8
15
|
import org.robolectric.RobolectricTestRunner
|
|
9
16
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
+
/**
|
|
18
|
+
* Navigation and queue-expansion glue.
|
|
19
|
+
*
|
|
20
|
+
* [BrowserManager.expandQueueFromContextualUrl] is the seam between the page a listener tapped in
|
|
21
|
+
* and the queue that starts playing. Its two halves are covered on their own — the section logic in
|
|
22
|
+
* [SectionScopeTest], the caller's single-track fallback in the player tests — but the wiring
|
|
23
|
+
* between them is what decides whether tapping row 3 of a page resumes the right station, and only
|
|
24
|
+
* these tests exercise it.
|
|
25
|
+
*
|
|
26
|
+
* Routes serve `browseStatic`, the one browse arm that needs neither the JNI bridge nor HTTP.
|
|
27
|
+
* Robolectric is required for `android.net.Uri` (contextual-URL parsing) and
|
|
28
|
+
* `android.util.LruCache` (the content cache).
|
|
29
|
+
*/
|
|
17
30
|
@RunWith(RobolectricTestRunner::class)
|
|
18
31
|
class BrowserManagerTest {
|
|
19
32
|
|
|
@@ -24,6 +37,22 @@ class BrowserManagerTest {
|
|
|
24
37
|
browserManager = BrowserManager()
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
/** Configures a single static route at [path] serving [children] as its page. */
|
|
41
|
+
private fun servePage(
|
|
42
|
+
path: String,
|
|
43
|
+
vararg children: com.margelo.nitro.audiobrowser.Track,
|
|
44
|
+
singleTrack: Boolean = false,
|
|
45
|
+
) {
|
|
46
|
+
browserManager.config =
|
|
47
|
+
BrowserConfig(
|
|
48
|
+
routes =
|
|
49
|
+
arrayOf(staticRoute(path, resolvedTrack(url = path, children = arrayOf(*children)))),
|
|
50
|
+
singleTrack = singleTrack,
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
private fun contextual(path: String, trackId: String) = BrowserPathHelper.build(path, trackId)
|
|
55
|
+
|
|
27
56
|
@Test
|
|
28
57
|
fun `getPath returns default path initially`() {
|
|
29
58
|
assertEquals("/", browserManager.getPath())
|
|
@@ -64,4 +93,134 @@ class BrowserManagerTest {
|
|
|
64
93
|
} catch (_: ContentNotFoundException) {}
|
|
65
94
|
}
|
|
66
95
|
}
|
|
96
|
+
|
|
97
|
+
@Test
|
|
98
|
+
fun `navigate serves a static route and gives playable children contextual urls`() = runBlocking {
|
|
99
|
+
servePage("/library", track(src = "a", title = "A"), track(src = "b", title = "B"))
|
|
100
|
+
|
|
101
|
+
val resolved = browserManager.navigate("/library")
|
|
102
|
+
|
|
103
|
+
assertEquals(listOf("A", "B"), resolved.children?.map { it.title })
|
|
104
|
+
// The tap target: a child's url is rewritten to point back at this page plus its own id.
|
|
105
|
+
assertEquals(
|
|
106
|
+
listOf(contextual("/library", "a"), contextual("/library", "b")),
|
|
107
|
+
resolved.children?.map { it.url },
|
|
108
|
+
)
|
|
109
|
+
assertEquals("/library", browserManager.getPath())
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - expandQueueFromContextualUrl
|
|
113
|
+
|
|
114
|
+
@Test
|
|
115
|
+
fun `expandQueueFromContextualUrl scopes the queue to the tapped section`() = runBlocking {
|
|
116
|
+
servePage(
|
|
117
|
+
"/home",
|
|
118
|
+
track(src = "a", groupTitle = "Recent"),
|
|
119
|
+
track(src = "b", groupTitle = "Recent"),
|
|
120
|
+
track(src = "c", groupTitle = "Popular"),
|
|
121
|
+
track(src = "d", groupTitle = "Popular"),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
val expanded = browserManager.expandQueueFromContextualUrl(contextual("/home", "c"))
|
|
125
|
+
|
|
126
|
+
// "Popular" only — a page aggregating sections must not leak next/previous across them.
|
|
127
|
+
val (queue, index) = requireNotNull(expanded)
|
|
128
|
+
assertEquals(listOf("c", "d"), queue.map { it.src })
|
|
129
|
+
assertEquals(0, index)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
fun `expandQueueFromContextualUrl indexes the tapped track within its section`() = runBlocking {
|
|
134
|
+
servePage(
|
|
135
|
+
"/home",
|
|
136
|
+
track(src = "a", groupTitle = "Recent"),
|
|
137
|
+
track(src = "b", groupTitle = "Popular"),
|
|
138
|
+
track(src = "c", groupTitle = "Popular"),
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
val (queue, index) =
|
|
142
|
+
requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "c")))
|
|
143
|
+
|
|
144
|
+
assertEquals(listOf("b", "c"), queue.map { it.src })
|
|
145
|
+
assertEquals(1, index)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@Test
|
|
149
|
+
fun `expandQueueFromContextualUrl returns null when the id has vanished from the page`() =
|
|
150
|
+
runBlocking {
|
|
151
|
+
// The stored url points at a track the page no longer lists — the page changed under a
|
|
152
|
+
// resumed session. Queueing the current list would resume the wrong station, so expansion
|
|
153
|
+
// aborts and the caller falls back to the stored single track.
|
|
154
|
+
servePage("/home", track(src = "a"), track(src = "b"))
|
|
155
|
+
|
|
156
|
+
assertNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "gone")))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
fun `expandQueueFromContextualUrl returns null for a non-contextual url`() = runBlocking {
|
|
161
|
+
servePage("/home", track(src = "a"))
|
|
162
|
+
|
|
163
|
+
assertNull(browserManager.expandQueueFromContextualUrl("/home"))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@Test
|
|
167
|
+
fun `expandQueueFromContextualUrl returns null when the page has no children`() = runBlocking {
|
|
168
|
+
browserManager.config =
|
|
169
|
+
BrowserConfig(routes = arrayOf(staticRoute("/home", resolvedTrack(url = "/home"))))
|
|
170
|
+
|
|
171
|
+
assertNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "a")))
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Test
|
|
175
|
+
fun `expandQueueFromContextualUrl expands an image row into its items`() = runBlocking {
|
|
176
|
+
servePage(
|
|
177
|
+
"/home",
|
|
178
|
+
track(
|
|
179
|
+
title = "Most Played",
|
|
180
|
+
src = null,
|
|
181
|
+
imageRow = arrayOf(imageRowItem("r1"), imageRowItem("r2")),
|
|
182
|
+
),
|
|
183
|
+
track(src = "a"),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
val (queue, index) =
|
|
187
|
+
requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "r2")))
|
|
188
|
+
|
|
189
|
+
// A tile tap queues its row, not the list rows beside it.
|
|
190
|
+
assertEquals(listOf("r1", "r2"), queue.map { it.src })
|
|
191
|
+
assertEquals(1, index)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
@Test
|
|
195
|
+
fun `expandQueueFromContextualUrl drops unplayable siblings`() = runBlocking {
|
|
196
|
+
servePage(
|
|
197
|
+
"/home",
|
|
198
|
+
track(src = "a", groupTitle = "Mixed"),
|
|
199
|
+
// A browsable row sitting inside the same section — no src, so not queueable.
|
|
200
|
+
track(src = null, groupTitle = "Mixed").copy(url = "/sub"),
|
|
201
|
+
track(src = "b", groupTitle = "Mixed"),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
val (queue, index) =
|
|
205
|
+
requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "b")))
|
|
206
|
+
|
|
207
|
+
assertEquals(listOf("a", "b"), queue.map { it.src })
|
|
208
|
+
assertEquals(1, index)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
@Test
|
|
212
|
+
fun `expandQueueFromContextualUrl honours singleTrack`() = runBlocking {
|
|
213
|
+
servePage(
|
|
214
|
+
"/home",
|
|
215
|
+
track(src = "a", groupTitle = "Recent"),
|
|
216
|
+
track(src = "b", groupTitle = "Recent"),
|
|
217
|
+
singleTrack = true,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
val (queue, index) =
|
|
221
|
+
requireNotNull(browserManager.expandQueueFromContextualUrl(contextual("/home", "b")))
|
|
222
|
+
|
|
223
|
+
assertEquals(listOf("b"), queue.map { it.src })
|
|
224
|
+
assertEquals(0, index)
|
|
225
|
+
}
|
|
67
226
|
}
|
|
@@ -7,6 +7,7 @@ import java.security.cert.PKIXParameters
|
|
|
7
7
|
import java.security.cert.TrustAnchor
|
|
8
8
|
import java.security.cert.X509Certificate
|
|
9
9
|
import java.util.Date
|
|
10
|
+
import javax.security.auth.x500.X500Principal
|
|
10
11
|
import org.junit.Assert.assertEquals
|
|
11
12
|
import org.junit.Assert.assertThrows
|
|
12
13
|
import org.junit.Test
|
|
@@ -15,12 +16,51 @@ import org.mockito.Mockito.`when`
|
|
|
15
16
|
|
|
16
17
|
class AiaCertChaserTest {
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* A certificate whose AIA extension value is exactly [extensionValue].
|
|
21
|
+
*
|
|
22
|
+
* Subject and issuer are stubbed to distinct principals on purpose: an unstubbed mock answers
|
|
23
|
+
* null to both, which `completeChain` would read as a self-signed root and stop on before
|
|
24
|
+
* fetching anything.
|
|
25
|
+
*/
|
|
19
26
|
private fun certWithAia(extensionValue: ByteArray): X509Certificate =
|
|
20
27
|
mock(X509Certificate::class.java).also {
|
|
21
28
|
`when`(it.getExtensionValue("1.3.6.1.5.5.7.1.1")).thenReturn(extensionValue)
|
|
29
|
+
`when`(it.subjectX500Principal).thenReturn(X500Principal("CN=leaf"))
|
|
30
|
+
`when`(it.issuerX500Principal).thenReturn(X500Principal("CN=issuer"))
|
|
22
31
|
}
|
|
23
32
|
|
|
33
|
+
/** A DER tag-length-value, with the length in whichever form fits. */
|
|
34
|
+
private fun der(tag: Int, content: ByteArray): ByteArray {
|
|
35
|
+
val length =
|
|
36
|
+
when (val n = content.size) {
|
|
37
|
+
in 0..0x7F -> byteArrayOf(n.toByte())
|
|
38
|
+
in 0x80..0xFF -> byteArrayOf(0x81.toByte(), n.toByte())
|
|
39
|
+
in 0x100..0xFFFF -> byteArrayOf(0x82.toByte(), (n shr 8).toByte(), n.toByte())
|
|
40
|
+
else -> byteArrayOf(0x83.toByte(), (n shr 16).toByte(), (n shr 8).toByte(), n.toByte())
|
|
41
|
+
}
|
|
42
|
+
return byteArrayOf(tag.toByte()) + length + content
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A well-formed AIA extension value listing [urls] as caIssuers locations. */
|
|
46
|
+
private fun aiaExtension(urls: List<String>): ByteArray {
|
|
47
|
+
val caIssuersOid = byteArrayOf(0x2B, 0x06, 0x01, 0x05, 0x05, 0x07, 0x30, 0x02)
|
|
48
|
+
val descriptions =
|
|
49
|
+
urls.fold(ByteArray(0)) { acc, url ->
|
|
50
|
+
acc + der(0x30, der(0x06, caIssuersOid) + der(0x86, url.toByteArray(Charsets.US_ASCII)))
|
|
51
|
+
}
|
|
52
|
+
return der(0x04, der(0x30, descriptions))
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
fun `the AIA fixture builder round-trips through the parser`() {
|
|
57
|
+
// Guards the two tests below: if this builder were malformed they would pass by parsing
|
|
58
|
+
// nothing at all, rather than by the cap doing its job.
|
|
59
|
+
val urls = listOf("http://ca1.example/i.crt", "http://ca2.example/i.crt")
|
|
60
|
+
|
|
61
|
+
assertEquals(urls, AiaCertChaser.extractCaIssuerUrls(certWithAia(aiaExtension(urls))))
|
|
62
|
+
}
|
|
63
|
+
|
|
24
64
|
@Test(timeout = 5_000)
|
|
25
65
|
fun `a negative long-form length does not hang the parser`() {
|
|
26
66
|
// OCTET STRING { SEQUENCE { tag 0x00, long-form length 0xFFFFFFFA = -6 } }.
|
|
@@ -73,7 +113,7 @@ class AiaCertChaserTest {
|
|
|
73
113
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
74
114
|
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
75
115
|
// Fetcher knows only the R13 URL (the leaf's AIA pointer); empty for the root URL.
|
|
76
|
-
val fetch = { url: String ->
|
|
116
|
+
val fetch = { url: String, _: Long ->
|
|
77
117
|
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
78
118
|
}
|
|
79
119
|
|
|
@@ -90,8 +130,8 @@ class AiaCertChaserTest {
|
|
|
90
130
|
|
|
91
131
|
// A PKCS#7-style bundle where the genuine issuer is not first must still be selected.
|
|
92
132
|
val completed =
|
|
93
|
-
AiaCertChaser.completeChain(listOf(leaf)) {
|
|
94
|
-
if (
|
|
133
|
+
AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
|
|
134
|
+
if (url == "http://r13.i.lencr.org/") listOf(unrelated, r13) else emptyList()
|
|
95
135
|
}
|
|
96
136
|
|
|
97
137
|
assertEquals(listOf(leaf, r13), completed)
|
|
@@ -103,7 +143,7 @@ class AiaCertChaserTest {
|
|
|
103
143
|
var fetches = 0
|
|
104
144
|
|
|
105
145
|
val completed =
|
|
106
|
-
AiaCertChaser.completeChain(listOf(root)) {
|
|
146
|
+
AiaCertChaser.completeChain(listOf(root)) { _, _ ->
|
|
107
147
|
fetches++
|
|
108
148
|
emptyList()
|
|
109
149
|
}
|
|
@@ -112,11 +152,115 @@ class AiaCertChaserTest {
|
|
|
112
152
|
assertEquals(0, fetches)
|
|
113
153
|
}
|
|
114
154
|
|
|
155
|
+
@Test
|
|
156
|
+
fun `completeChain stops at a chain that already reaches a trust anchor`() {
|
|
157
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
158
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
159
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
160
|
+
val fetched = mutableListOf<String>()
|
|
161
|
+
|
|
162
|
+
// R13's own AIA points at the ISRG root, so without knowing the anchors this walks one hop
|
|
163
|
+
// too far and pays for a second blocking fetch on the ordinary rescue path.
|
|
164
|
+
val completed =
|
|
165
|
+
AiaCertChaser.completeChain(
|
|
166
|
+
listOf(leaf),
|
|
167
|
+
anchorSubjects = setOf(root.subjectX500Principal),
|
|
168
|
+
) { url, _ ->
|
|
169
|
+
fetched.add(url)
|
|
170
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else listOf(root)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
assertEquals(listOf(leaf, r13), completed)
|
|
174
|
+
assertEquals(listOf("http://r13.i.lencr.org/"), fetched)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
@Test
|
|
178
|
+
fun `completeChain fetches nothing when the presented chain is already anchored`() {
|
|
179
|
+
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
180
|
+
val r13 = CertFixtures.cert("letsencrypt-r13.pem")
|
|
181
|
+
val root = CertFixtures.cert("isrg-root-x1.pem")
|
|
182
|
+
var fetches = 0
|
|
183
|
+
|
|
184
|
+
// The shape of every non-path failure — expiry, hostname mismatch, CT — where the server
|
|
185
|
+
// sent a complete chain and validation failed for some other reason.
|
|
186
|
+
val completed =
|
|
187
|
+
AiaCertChaser.completeChain(
|
|
188
|
+
listOf(leaf, r13),
|
|
189
|
+
anchorSubjects = setOf(root.subjectX500Principal),
|
|
190
|
+
) { _, _ ->
|
|
191
|
+
fetches++
|
|
192
|
+
emptyList()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
assertEquals(listOf(leaf, r13), completed)
|
|
196
|
+
assertEquals(0, fetches)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
@Test
|
|
200
|
+
fun `completeChain tries only a few of a certificate's CA-Issuers URLs`() {
|
|
201
|
+
// The extension is authored by the server that just failed validation, and nothing limits
|
|
202
|
+
// how many URLs it lists. Each one costs a blocking connect on the handshake thread, so
|
|
203
|
+
// hundreds of them are a denial of service whatever each individual fetch costs.
|
|
204
|
+
val manyUrls = (1..500).map { "http://ca$it.example/issuer.crt" }
|
|
205
|
+
val cert = certWithAia(aiaExtension(manyUrls))
|
|
206
|
+
val fetched = mutableListOf<String>()
|
|
207
|
+
|
|
208
|
+
AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
|
|
209
|
+
fetched.add(url)
|
|
210
|
+
emptyList()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
assertEquals(AiaCertChaser.MAX_URLS_PER_CERT, fetched.size)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
@Test
|
|
217
|
+
fun `duplicate CA-Issuers URLs do not consume the URL cap`() {
|
|
218
|
+
// 500 copies of one dead URL followed by a distinct one: the cap must buy distinct hosts,
|
|
219
|
+
// not three visits to the same dead one.
|
|
220
|
+
val urls = List(500) { "http://ca.example/issuer.crt" } + "http://ca-b.example/issuer.crt"
|
|
221
|
+
val cert = certWithAia(aiaExtension(urls))
|
|
222
|
+
val fetched = mutableListOf<String>()
|
|
223
|
+
|
|
224
|
+
AiaCertChaser.completeChain(listOf(cert)) { url, _ ->
|
|
225
|
+
fetched.add(url)
|
|
226
|
+
emptyList()
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
assertEquals(listOf("http://ca.example/issuer.crt", "http://ca-b.example/issuer.crt"), fetched)
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@Test
|
|
233
|
+
fun `completeChain stops fetching once its wall-clock budget is spent`() {
|
|
234
|
+
val cert = certWithAia(aiaExtension((1..500).map { "http://ca$it.example/issuer.crt" }))
|
|
235
|
+
var fetches = 0
|
|
236
|
+
var nanos = 0L
|
|
237
|
+
val deadlines = mutableListOf<Long>()
|
|
238
|
+
|
|
239
|
+
// Each fetch "takes" 8s against a 20s budget, so the walk must stop after a handful even
|
|
240
|
+
// though the URL cap alone would have allowed more.
|
|
241
|
+
AiaCertChaser.completeChain(
|
|
242
|
+
listOf(cert),
|
|
243
|
+
maxUrlsPerCert = 500,
|
|
244
|
+
budgetMs = 20_000,
|
|
245
|
+
nowNanos = { nanos },
|
|
246
|
+
) { _, deadline ->
|
|
247
|
+
fetches++
|
|
248
|
+
deadlines.add(deadline)
|
|
249
|
+
nanos += 8_000_000_000
|
|
250
|
+
emptyList()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
assertEquals(3, fetches)
|
|
254
|
+
// Every fetch is handed the same walk-wide deadline, so it can clip its own I/O to it —
|
|
255
|
+
// the budget bounds the fetches themselves, not just how many of them start.
|
|
256
|
+
assertEquals(listOf(20_000_000_000L, 20_000_000_000L, 20_000_000_000L), deadlines)
|
|
257
|
+
}
|
|
258
|
+
|
|
115
259
|
@Test
|
|
116
260
|
fun `completeChain stops when the issuer cannot be fetched`() {
|
|
117
261
|
val leaf = CertFixtures.cert("stationplaylist-leaf.pem")
|
|
118
262
|
|
|
119
|
-
val completed = AiaCertChaser.completeChain(listOf(leaf)) { emptyList() }
|
|
263
|
+
val completed = AiaCertChaser.completeChain(listOf(leaf)) { _, _ -> emptyList() }
|
|
120
264
|
|
|
121
265
|
assertEquals(listOf(leaf), completed)
|
|
122
266
|
}
|
|
@@ -134,8 +278,8 @@ class AiaCertChaserTest {
|
|
|
134
278
|
|
|
135
279
|
// After AIA chasing splices in R13, the chain validates to the trusted root.
|
|
136
280
|
val completed =
|
|
137
|
-
AiaCertChaser.completeChain(listOf(leaf)) {
|
|
138
|
-
if (
|
|
281
|
+
AiaCertChaser.completeChain(listOf(leaf)) { url, _ ->
|
|
282
|
+
if (url == "http://r13.i.lencr.org/") listOf(r13) else emptyList()
|
|
139
283
|
}
|
|
140
284
|
validatePath(completed, root, date) // must not throw
|
|
141
285
|
}
|