Skip to content
Merged
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
35 changes: 21 additions & 14 deletions okhttp-testing-support/src/main/kotlin/okhttp3/FakeDns.kt
Original file line number Diff line number Diff line change
Expand Up @@ -195,20 +195,7 @@ class FakeDns(
}
}

// QR = 1 (Response)
// OPCODE = 0 (standard query)
// RD = 1 (Recursion Desired)
// RA = 1 (Recursion Available)
// RCODE = 0 (success)
// QR OPCODE AA TC RD RA Z RCODE
val flags = 0b1___0000__0__0__1__1_000__0000

return DnsMessage(
id = request.id,
flags = flags,
questions = request.questions,
answers = answers,
)
return dnsResponse(request, answers)
}

private fun ResourceRecord.matches(question: Question): Boolean {
Expand Down Expand Up @@ -336,3 +323,23 @@ class FakeDns(
) : Request
}
}

fun dnsResponse(
request: DnsMessage,
answers: List<ResourceRecord>,
): DnsMessage {
// QR = 1 (Response)
// OPCODE = 0 (standard query)
// RD = 1 (Recursion Desired)
// RA = 1 (Recursion Available)
// RCODE = 0 (success)
// QR OPCODE AA TC RD RA Z RCODE
val flags = 0b1___0000__0__0__1__1_000__0000

return DnsMessage(
id = request.id,
flags = flags,
questions = request.questions,
answers = answers,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
/*
* Copyright (c) 2026 OkHttp Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
@file:OptIn(OkHttpInternalApi::class)

package okhttp3.internal.dns

import java.io.IOException
import java.net.InetAddress
import kotlin.test.Test
import okhttp3.Dns
import okhttp3.Protocol
import okhttp3.internal.OkHttpInternalApi

class DnsCallStateMachineTest {
@Test
fun `happy path`() =
testDnsCallStateMachine(
request = Dns.Request(hostname = "lysine.dev"),
) {
enqueue()

val query0 = takeQuery("lysine.dev", TYPE_HTTPS)
val query1 = takeQuery("lysine.dev", TYPE_AAAA)
val query2 = takeQuery("lysine.dev", TYPE_A)

respondIpAddresses(
query = query1.query,
addresses = listOf(InetAddress.getByName("1:2::3:4")),
)
takeOnRecordsIpAddresses(
addresses = listOf(InetAddress.getByName("1:2::3:4")),
)

respondIpAddresses(
query = query2.query,
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)
takeOnRecordsIpAddresses(
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)

respondServiceMetadata(
query = query0.query,
alpnIds = listOf("h2"),
)
takeOnRecordsServiceMetadata(
last = true,
alpnIds = listOf(Protocol.HTTP_2),
)
}

@Test
fun `failure returned last`() =
testDnsCallStateMachine(
request = Dns.Request(hostname = "lysine.dev"),
) {
enqueue()

val query0 = takeQuery("lysine.dev", TYPE_HTTPS)
val query1 = takeQuery("lysine.dev", TYPE_AAAA)
val query2 = takeQuery("lysine.dev", TYPE_A)

respondFailure(
query = query1.query,
e = IOException("boom!"),
)

respondIpAddresses(
query = query2.query,
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)
takeOnRecordsIpAddresses(
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)

respondServiceMetadata(
query = query0.query,
alpnIds = listOf("h2"),
)
takeOnRecordsServiceMetadata(
alpnIds = listOf(Protocol.HTTP_2),
)

takeOnFailure("boom!")
}

/**
* Confirm that the state machine calls doesn't call any [Dns.Callback] methods until the previous
* call to a [Dns.Callback] method has returned.
*
* Usually this will be a concurrency problem, but we can exercise it just as well by making a
* re-entrant call on a single thread.
*/
@Test
fun `calls to onRecords are serialized`() =
testDnsCallStateMachine(request = Dns.Request(hostname = "lysine.dev")) {
enqueue()

val query0 = takeQuery("lysine.dev", TYPE_HTTPS)
val query1 = takeQuery("lysine.dev", TYPE_AAAA)
val query2 = takeQuery("lysine.dev", TYPE_A)

onNextEvent = {
respondIpAddresses(
query = query2.query,
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)
respondIpAddresses(
query = query1.query,
addresses = listOf(InetAddress.getByName("1:2::3:4")),
)
}
respondServiceMetadata(
query = query0.query,
alpnIds = listOf("h2"),
)
takeOnRecordsServiceMetadata(
alpnIds = listOf(Protocol.HTTP_2),
)
takeOnRecordsIpAddresses(
last = true,
addresses =
listOf(
InetAddress.getByName("10.20.30.40"),
InetAddress.getByName("1:2::3:4"),
),
)
}

/**
* The implementation still enqueues canceled queries, because that's an easy way to jump to a
* dispatcher thread to post the failures back to the callback.
*/
@Test
fun `cancel before enqueue`() =
testDnsCallStateMachine(
request = Dns.Request(hostname = "lysine.dev"),
) {
call.cancel()
enqueue()

val query0 = takeCancel("lysine.dev", TYPE_HTTPS)
takeQuery("lysine.dev", TYPE_HTTPS)
val query1 = takeCancel("lysine.dev", TYPE_AAAA)
takeQuery("lysine.dev", TYPE_AAAA)
val query2 = takeCancel("lysine.dev", TYPE_A)
takeQuery("lysine.dev", TYPE_A)

respondFailure(query0.query, IOException("canceled"))
respondFailure(query1.query, IOException("canceled"))
respondFailure(query2.query, IOException("canceled"))

takeOnFailure("canceled")
}

/** Cancels are asynchronous and if the canceled query completes anyway, that's fine. */
@Test
fun `cancel ignored if canceled query completes`() =
testDnsCallStateMachine(
request = Dns.Request(hostname = "lysine.dev"),
) {
enqueue()

val query0 = takeQuery("lysine.dev", TYPE_HTTPS)
val query1 = takeQuery("lysine.dev", TYPE_AAAA)
val query2 = takeQuery("lysine.dev", TYPE_A)

respondIpAddresses(
query = query1.query,
addresses = listOf(InetAddress.getByName("1:2::3:4")),
)
takeOnRecordsIpAddresses(
addresses = listOf(InetAddress.getByName("1:2::3:4")),
)

call.cancel()

takeCancel("lysine.dev", TYPE_HTTPS)
takeCancel("lysine.dev", TYPE_A)

respondIpAddresses(
query = query2.query,
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)
takeOnRecordsIpAddresses(
addresses = listOf(InetAddress.getByName("10.20.30.40")),
)

respondServiceMetadata(
query = query0.query,
alpnIds = listOf("h2"),
)
takeOnRecordsServiceMetadata(
last = true,
alpnIds = listOf(Protocol.HTTP_2),
)
}
}
Loading
Loading