Add integration test for capport API

Add a test to verify the ConnectivityService / NetworkMonitor
integration around the captive portal API.

Test: atest ConnectivityServiceIntegrationTest
Bug: 156062304
Change-Id: I4eed02e09fc4943c011d871c58ba97ec572c7763
This commit is contained in:
Remi NGUYEN VAN
2020-05-10 16:13:15 +09:00
parent 8598423cc4
commit 2ce373abf2
5 changed files with 78 additions and 11 deletions

View File

@@ -30,6 +30,8 @@
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.NETWORK_FACTORY"/> <uses-permission android:name="android.permission.NETWORK_FACTORY"/>
<!-- Obtain LinkProperties callbacks with sensitive fields -->
<uses-permission android:name="android.permission.NETWORK_SETTINGS" />
<uses-permission android:name="android.permission.NETWORK_STACK"/> <uses-permission android:name="android.permission.NETWORK_STACK"/>
<uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY"/> <uses-permission android:name="android.permission.OBSERVE_NETWORK_POLICY"/>
<uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> <uses-permission android:name="android.permission.READ_PRIVILEGED_PHONE_STATE"/>

View File

@@ -28,10 +28,13 @@ import android.net.INetd
import android.net.INetworkPolicyManager import android.net.INetworkPolicyManager
import android.net.INetworkStatsService import android.net.INetworkStatsService
import android.net.LinkProperties import android.net.LinkProperties
import android.net.NetworkCapabilities.NET_CAPABILITY_CAPTIVE_PORTAL
import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET import android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET
import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_CELLULAR import android.net.NetworkCapabilities.TRANSPORT_CELLULAR
import android.net.NetworkRequest import android.net.NetworkRequest
import android.net.TestNetworkStackClient import android.net.TestNetworkStackClient
import android.net.Uri
import android.net.metrics.IpConnectivityLog import android.net.metrics.IpConnectivityLog
import android.os.ConditionVariable import android.os.ConditionVariable
import android.os.IBinder import android.os.IBinder
@@ -64,6 +67,8 @@ import org.mockito.Mockito.spy
import org.mockito.MockitoAnnotations import org.mockito.MockitoAnnotations
import org.mockito.Spy import org.mockito.Spy
import kotlin.test.assertEquals import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertTrue import kotlin.test.assertTrue
import kotlin.test.fail import kotlin.test.fail
@@ -110,6 +115,10 @@ class ConnectivityServiceIntegrationTest {
private val bindingCondition = ConditionVariable(false) private val bindingCondition = ConditionVariable(false)
private val realContext get() = InstrumentationRegistry.getInstrumentation().context private val realContext get() = InstrumentationRegistry.getInstrumentation().context
private val httpProbeUrl get() =
realContext.getResources().getString(R.string.config_captive_portal_http_url)
private val httpsProbeUrl get() =
realContext.getResources().getString(R.string.config_captive_portal_https_url)
private class InstrumentationServiceConnection : ServiceConnection { private class InstrumentationServiceConnection : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
@@ -188,12 +197,8 @@ class ConnectivityServiceIntegrationTest {
val testCallback = TestableNetworkCallback() val testCallback = TestableNetworkCallback()
cm.registerNetworkCallback(request, testCallback) cm.registerNetworkCallback(request, testCallback)
nsInstrumentation.addHttpResponse(HttpResponse( nsInstrumentation.addHttpResponse(HttpResponse(httpProbeUrl, responseCode = 204))
"http://test.android.com", nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
responseCode = 204, contentLength = 42, redirectUrl = null))
nsInstrumentation.addHttpResponse(HttpResponse(
"https://secure.test.android.com",
responseCode = 204, contentLength = 42, redirectUrl = null))
val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), context) val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, LinkProperties(), context)
networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS) networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
@@ -204,4 +209,52 @@ class ConnectivityServiceIntegrationTest {
testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS) testCallback.expectAvailableThenValidatedCallbacks(na.network, TEST_TIMEOUT_MS)
assertEquals(2, nsInstrumentation.getRequestUrls().size) assertEquals(2, nsInstrumentation.getRequestUrls().size)
} }
@Test
fun testCapportApi() {
val request = NetworkRequest.Builder()
.clearCapabilities()
.addCapability(NET_CAPABILITY_INTERNET)
.build()
val testCb = TestableNetworkCallback()
val apiUrl = "https://capport.android.com"
cm.registerNetworkCallback(request, testCb)
nsInstrumentation.addHttpResponse(HttpResponse(
apiUrl,
"""
|{
| "captive": true,
| "user-portal-url": "https://login.capport.android.com",
| "venue-info-url": "https://venueinfo.capport.android.com"
|}
""".trimMargin()))
// Tests will fail if a non-mocked query is received: mock the HTTPS probe, but not the
// HTTP probe as it should not be sent.
// Even if the HTTPS probe succeeds, a portal should be detected as the API takes precedence
// in that case.
nsInstrumentation.addHttpResponse(HttpResponse(httpsProbeUrl, responseCode = 204))
val lp = LinkProperties()
lp.captivePortalApiUrl = Uri.parse(apiUrl)
val na = NetworkAgentWrapper(TRANSPORT_CELLULAR, lp, context)
networkStackClient.verifyNetworkMonitorCreated(na.network, TEST_TIMEOUT_MS)
na.addCapability(NET_CAPABILITY_INTERNET)
na.connect()
testCb.expectAvailableCallbacks(na.network, validated = false, tmt = TEST_TIMEOUT_MS)
val capportData = testCb.expectLinkPropertiesThat(na, TEST_TIMEOUT_MS) {
it.captivePortalData != null
}.lp.captivePortalData
assertNotNull(capportData)
assertTrue(capportData.isCaptive)
assertEquals(Uri.parse("https://login.capport.android.com"), capportData.userPortalUrl)
assertEquals(Uri.parse("https://venueinfo.capport.android.com"), capportData.venueInfoUrl)
val nc = testCb.expectCapabilitiesWith(NET_CAPABILITY_CAPTIVE_PORTAL, na, TEST_TIMEOUT_MS)
assertFalse(nc.hasCapability(NET_CAPABILITY_VALIDATED))
}
} }

View File

@@ -22,16 +22,21 @@ import android.os.Parcelable
data class HttpResponse( data class HttpResponse(
val requestUrl: String, val requestUrl: String,
val responseCode: Int, val responseCode: Int,
val contentLength: Long, val content: String = "",
val redirectUrl: String? val redirectUrl: String? = null
) : Parcelable { ) : Parcelable {
constructor(p: Parcel): this(p.readString(), p.readInt(), p.readLong(), p.readString()) constructor(p: Parcel): this(p.readString(), p.readInt(), p.readString(), p.readString())
constructor(requestUrl: String, contentBody: String): this(
requestUrl,
responseCode = 200,
content = contentBody,
redirectUrl = null)
override fun writeToParcel(dest: Parcel, flags: Int) { override fun writeToParcel(dest: Parcel, flags: Int) {
with(dest) { with(dest) {
writeString(requestUrl) writeString(requestUrl)
writeInt(responseCode) writeInt(responseCode)
writeLong(contentLength) writeString(content)
writeString(redirectUrl) writeString(redirectUrl)
} }
} }

View File

@@ -65,6 +65,9 @@ class NetworkStackInstrumentationService : Service() {
* *
* <p>For any subsequent HTTP/HTTPS query, the first response with a matching URL will be * <p>For any subsequent HTTP/HTTPS query, the first response with a matching URL will be
* used to mock the query response. * used to mock the query response.
*
* <p>All requests that are expected to be sent must have a mock response: if an unexpected
* request is seen, the test will fail.
*/ */
override fun addHttpResponse(response: HttpResponse) { override fun addHttpResponse(response: HttpResponse) {
httpResponses.getValue(response.requestUrl).add(response) httpResponses.getValue(response.requestUrl).add(response)

View File

@@ -33,9 +33,11 @@ import com.android.server.net.integrationtests.NetworkStackInstrumentationServic
import org.mockito.Mockito.doReturn import org.mockito.Mockito.doReturn
import org.mockito.Mockito.mock import org.mockito.Mockito.mock
import org.mockito.Mockito.spy import org.mockito.Mockito.spy
import java.io.ByteArrayInputStream
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.URL import java.net.URL
import java.net.URLConnection import java.net.URLConnection
import java.nio.charset.StandardCharsets
private const val TEST_NETID = 42 private const val TEST_NETID = 42
@@ -71,11 +73,13 @@ class TestNetworkStackService : Service() {
private inner class TestNetwork(netId: Int) : Network(netId) { private inner class TestNetwork(netId: Int) : Network(netId) {
override fun openConnection(url: URL): URLConnection { override fun openConnection(url: URL): URLConnection {
val response = InstrumentationConnector.processRequest(url) val response = InstrumentationConnector.processRequest(url)
val responseBytes = response.content.toByteArray(StandardCharsets.UTF_8)
val connection = mock(HttpURLConnection::class.java) val connection = mock(HttpURLConnection::class.java)
doReturn(response.responseCode).`when`(connection).responseCode doReturn(response.responseCode).`when`(connection).responseCode
doReturn(response.contentLength).`when`(connection).contentLengthLong doReturn(responseBytes.size.toLong()).`when`(connection).contentLengthLong
doReturn(response.redirectUrl).`when`(connection).getHeaderField("location") doReturn(response.redirectUrl).`when`(connection).getHeaderField("location")
doReturn(ByteArrayInputStream(responseBytes)).`when`(connection).inputStream
return connection return connection
} }
} }