Add utilities for network validation testing
Refactor out TestHttpServer to share it between CaptivePortalApiTest and
CaptivePortalTest, move it to frameworks/libs/net, and add a
NetworkValidationTestUtil class with utilities to set the test
validation URLs.
Test: atest CtsNetTestCasesLatestSdk:CaptivePortal[Api]Test
Bug: 160617623
Bug: 160656765
Change-Id: Icd7829e680b2dddd1ddaa3dc2d946c14c20b5a15
diff --git a/tests/tests/net/Android.bp b/tests/tests/net/Android.bp
index 926b45c..be8b43d 100644
--- a/tests/tests/net/Android.bp
+++ b/tests/tests/net/Android.bp
@@ -47,7 +47,6 @@
"ctstestserver",
"junit",
"junit-params",
- "libnanohttpd",
"mockwebserver",
"net-utils-framework-common",
"truth-prebuilt",
diff --git a/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt b/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt
index 4a7d38a1..12a966f 100644
--- a/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt
+++ b/tests/tests/net/src/android/net/cts/CaptivePortalTest.kt
@@ -19,7 +19,6 @@
import android.Manifest.permission.CONNECTIVITY_INTERNAL
import android.Manifest.permission.NETWORK_SETTINGS
import android.Manifest.permission.READ_DEVICE_CONFIG
-import android.Manifest.permission.WRITE_DEVICE_CONFIG
import android.content.pm.PackageManager.FEATURE_TELEPHONY
import android.content.pm.PackageManager.FEATURE_WIFI
import android.net.ConnectivityManager
@@ -30,20 +29,25 @@
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.NetworkRequest
import android.net.Uri
+import android.net.cts.NetworkValidationTestUtil.clearValidationTestUrlsDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.runAsShell
+import android.net.cts.NetworkValidationTestUtil.setHttpUrlDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setHttpsUrlDeviceConfig
+import android.net.cts.NetworkValidationTestUtil.setUrlExpirationDeviceConfig
+import com.android.testutils.TestHttpServer.Request
import android.net.cts.util.CtsNetUtils
+import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+import android.net.util.NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
import android.net.wifi.WifiManager
import android.os.Build
-import android.os.ConditionVariable
import android.platform.test.annotations.AppModeFull
import android.provider.DeviceConfig
import android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY
import android.text.TextUtils
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import androidx.test.runner.AndroidJUnit4
-import com.android.compatibility.common.util.SystemUtil
+import com.android.testutils.TestHttpServer
import com.android.testutils.isDevSdkInRange
-import fi.iki.elonen.NanoHTTPD
-import fi.iki.elonen.NanoHTTPD.Response.IStatus
import fi.iki.elonen.NanoHTTPD.Response.Status
import junit.framework.AssertionFailedError
import org.junit.After
@@ -55,15 +59,12 @@
import java.util.concurrent.TimeoutException
import kotlin.test.Test
import kotlin.test.assertNotEquals
+import kotlin.test.assertNotNull
import kotlin.test.assertTrue
-private const val TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING = "test_captive_portal_https_url"
-private const val TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING = "test_captive_portal_http_url"
-private const val TEST_URL_EXPIRATION_TIME = "test_url_expiration_time"
-
-private const val TEST_HTTPS_URL_PATH = "https_path"
-private const val TEST_HTTP_URL_PATH = "http_path"
-private const val TEST_PORTAL_URL_PATH = "portal_path"
+private const val TEST_HTTPS_URL_PATH = "/https_path"
+private const val TEST_HTTP_URL_PATH = "/http_path"
+private const val TEST_PORTAL_URL_PATH = "/portal_path"
private const val LOCALHOST_HOSTNAME = "localhost"
@@ -88,24 +89,24 @@
private val pm by lazy { context.packageManager }
private val utils by lazy { CtsNetUtils(context) }
- private val server = HttpServer()
+ private val server = TestHttpServer("localhost")
@Before
fun setUp() {
- doAsShell(READ_DEVICE_CONFIG) {
+ runAsShell(READ_DEVICE_CONFIG) {
// Verify that the test URLs are not normally set on the device, but do not fail if the
// test URLs are set to what this test uses (URLs on localhost), in case the test was
// interrupted manually and rerun.
- assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING)
- assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING)
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTPS_URL)
+ assertEmptyOrLocalhostUrl(TEST_CAPTIVE_PORTAL_HTTP_URL)
}
- clearTestUrls()
+ clearValidationTestUrlsDeviceConfig()
server.start()
}
@After
fun tearDown() {
- clearTestUrls()
+ clearValidationTestUrlsDeviceConfig()
if (pm.hasSystemFeature(FEATURE_WIFI)) {
reconnectWifi()
}
@@ -118,12 +119,6 @@
"$urlKey must not be set in production scenarios (current value: $url)")
}
- private fun clearTestUrls() {
- setHttpsUrl(null)
- setHttpUrl(null)
- setUrlExpiration(null)
- }
-
@Test
fun testCaptivePortalIsNotDefaultNetwork() {
assumeTrue(pm.hasSystemFeature(FEATURE_TELEPHONY))
@@ -132,19 +127,15 @@
utils.connectToCell()
// Have network validation use a local server that serves a HTTPS error / HTTP redirect
- server.addResponse(TEST_PORTAL_URL_PATH, Status.OK,
+ server.addResponse(Request(TEST_PORTAL_URL_PATH), Status.OK,
content = "Test captive portal content")
- server.addResponse(TEST_HTTPS_URL_PATH, Status.INTERNAL_ERROR)
- server.addResponse(TEST_HTTP_URL_PATH, Status.REDIRECT,
- locationHeader = server.makeUrl(TEST_PORTAL_URL_PATH))
- setHttpsUrl(server.makeUrl(TEST_HTTPS_URL_PATH))
- setHttpUrl(server.makeUrl(TEST_HTTP_URL_PATH))
+ server.addResponse(Request(TEST_HTTPS_URL_PATH), Status.INTERNAL_ERROR)
+ server.addResponse(Request(TEST_HTTP_URL_PATH), Status.REDIRECT,
+ locationHeader = makeUrl(TEST_PORTAL_URL_PATH))
+ setHttpsUrlDeviceConfig(makeUrl(TEST_HTTPS_URL_PATH))
+ setHttpUrlDeviceConfig(makeUrl(TEST_HTTP_URL_PATH))
// URL expiration needs to be in the next 10 minutes
- setUrlExpiration(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
-
- // Expect the portal content to be fetched at some point after detecting the portal.
- // Some implementations may fetch the URL before startCaptivePortalApp is called.
- val portalContentRequestCv = server.addExpectRequestCv(TEST_PORTAL_URL_PATH)
+ setUrlExpirationDeviceConfig(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(9))
// Wait for a captive portal to be detected on the network
val wifiNetworkFuture = CompletableFuture<Network>()
@@ -173,9 +164,14 @@
val startPortalAppPermission =
if (isDevSdkInRange(0, Build.VERSION_CODES.Q)) CONNECTIVITY_INTERNAL
else NETWORK_SETTINGS
- doAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
- assertTrue(portalContentRequestCv.block(TEST_TIMEOUT_MS), "The captive portal login " +
- "page was still not fetched ${TEST_TIMEOUT_MS}ms after startCaptivePortalApp.")
+ runAsShell(startPortalAppPermission) { cm.startCaptivePortalApp(network) }
+
+ // Expect the portal content to be fetched at some point after detecting the portal.
+ // Some implementations may fetch the URL before startCaptivePortalApp is called.
+ assertNotNull(server.requestsRecord.poll(TEST_TIMEOUT_MS, pos = 0) {
+ it.path == TEST_PORTAL_URL_PATH
+ }, "The captive portal login page was still not fetched ${TEST_TIMEOUT_MS}ms " +
+ "after startCaptivePortalApp.")
assertNotEquals(network, cm.activeNetwork, wifiDefaultMessage)
} finally {
@@ -186,73 +182,13 @@
}
}
- private fun setHttpsUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTPS_URL_SETTING, url)
- private fun setHttpUrl(url: String?) = setConfig(TEST_CAPTIVE_PORTAL_HTTP_URL_SETTING, url)
- private fun setUrlExpiration(timestamp: Long?) = setConfig(TEST_URL_EXPIRATION_TIME,
- timestamp?.toString())
-
- private fun setConfig(configKey: String, value: String?) {
- doAsShell(WRITE_DEVICE_CONFIG) {
- DeviceConfig.setProperty(
- NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
- }
- }
-
- private fun doAsShell(vararg permissions: String, action: () -> Unit) {
- // Wrap the below call to allow for more kotlin-like syntax
- SystemUtil.runWithShellPermissionIdentity(action, permissions)
- }
+ /**
+ * Create a URL string that, when fetched, will hit the test server with the given URL [path].
+ */
+ private fun makeUrl(path: String) = "http://localhost:${server.listeningPort}" + path
private fun reconnectWifi() {
utils.ensureWifiDisconnected(null /* wifiNetworkToCheck */)
utils.ensureWifiConnected()
}
-
- /**
- * A minimal HTTP server running on localhost (loopback), on a random available port.
- */
- private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) {
- // Map of URL path -> HTTP response code
- private val responses = HashMap<String, Response>()
-
- // Map of path -> CV to open as soon as a request to the path is received
- private val waitForRequestCv = HashMap<String, ConditionVariable>()
-
- /**
- * Create a URL string that, when fetched, will hit this server with the given URL [path].
- */
- fun makeUrl(path: String): String {
- return Uri.Builder()
- .scheme("http")
- .encodedAuthority("localhost:$listeningPort")
- .query(path)
- .build()
- .toString()
- }
-
- fun addResponse(
- path: String,
- statusCode: IStatus,
- locationHeader: String? = null,
- content: String = ""
- ) {
- val response = newFixedLengthResponse(statusCode, "text/plain", content)
- locationHeader?.let { response.addHeader("Location", it) }
- responses[path] = response
- }
-
- /**
- * Create a [ConditionVariable] that will open when a request to [path] is received.
- */
- fun addExpectRequestCv(path: String): ConditionVariable {
- return ConditionVariable().apply { waitForRequestCv[path] = this }
- }
-
- override fun serve(session: IHTTPSession): Response {
- waitForRequestCv[session.queryParameterString]?.open()
- return responses[session.queryParameterString]
- // Default response is a 404
- ?: super.serve(session)
- }
- }
}
\ No newline at end of file
diff --git a/tests/tests/net/src/android/net/cts/CaptivePortalApiTest.kt b/tests/tests/net/src/android/net/cts/NetworkValidationTest.kt
similarity index 84%
rename from tests/tests/net/src/android/net/cts/CaptivePortalApiTest.kt
rename to tests/tests/net/src/android/net/cts/NetworkValidationTest.kt
index ef2b0ce..52c383d 100644
--- a/tests/tests/net/src/android/net/cts/CaptivePortalApiTest.kt
+++ b/tests/tests/net/src/android/net/cts/NetworkValidationTest.kt
@@ -29,6 +29,7 @@
import android.net.TestNetworkInterface
import android.net.TestNetworkManager
import android.net.Uri
+import android.net.cts.NetworkValidationTestUtil.runAsShell
import android.net.dhcp.DhcpDiscoverPacket
import android.net.dhcp.DhcpPacket
import android.net.dhcp.DhcpPacket.DHCP_MESSAGE_TYPE
@@ -40,18 +41,18 @@
import android.platform.test.annotations.AppModeFull
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
-import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
-import com.android.compatibility.common.util.ThrowingRunnable
import com.android.net.module.util.Inet4AddressUtils.getBroadcastAddress
import com.android.net.module.util.Inet4AddressUtils.getPrefixMaskAsInet4Address
import com.android.server.util.NetworkStackConstants.IPV4_ADDR_ANY
+import com.android.testutils.ArpResponder
import com.android.testutils.DevSdkIgnoreRule
import com.android.testutils.DhcpClientPacketFilter
import com.android.testutils.DhcpOptionFilter
import com.android.testutils.RecorderCallback.CallbackEntry
import com.android.testutils.TapPacketReader
+import com.android.testutils.TestHttpServer
import com.android.testutils.TestableNetworkCallback
-import fi.iki.elonen.NanoHTTPD
+import fi.iki.elonen.NanoHTTPD.Response.Status
import org.junit.After
import org.junit.Assume.assumeFalse
import org.junit.Before
@@ -59,8 +60,6 @@
import org.junit.Test
import org.junit.runner.RunWith
import java.net.Inet4Address
-import java.util.concurrent.ArrayBlockingQueue
-import java.util.concurrent.TimeUnit
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
@@ -79,7 +78,7 @@
@AppModeFull(reason = "Instant apps cannot create test networks")
@RunWith(AndroidJUnit4::class)
-class CaptivePortalApiTest {
+class NetworkValidationTest {
@JvmField
@Rule
val ignoreRule = DevSdkIgnoreRule(ignoreClassUpTo = Build.VERSION_CODES.Q)
@@ -89,10 +88,10 @@
private val eth by lazy { context.assertHasService(EthernetManager::class.java) }
private val cm by lazy { context.assertHasService(ConnectivityManager::class.java) }
- private val handlerThread = HandlerThread(CaptivePortalApiTest::class.java.simpleName)
+ private val handlerThread = HandlerThread(NetworkValidationTest::class.java.simpleName)
private val serverIpAddr = InetAddresses.parseNumericAddress("192.0.2.222") as Inet4Address
private val clientIpAddr = InetAddresses.parseNumericAddress("192.0.2.111") as Inet4Address
- private val httpServer = HttpServer()
+ private val httpServer = TestHttpServer()
private val ethRequest = NetworkRequest.Builder()
// ETHERNET|TEST transport networks do not have NET_CAPABILITY_TRUSTED
.removeCapability(NET_CAPABILITY_TRUSTED)
@@ -151,7 +150,15 @@
}
@Test
- fun testApiCallbacks() {
+ fun testCapportApiCallbacks() {
+ httpServer.addResponse(capportUrl, Status.OK, content = """
+ |{
+ | "captive": true,
+ | "user-portal-url": "$TEST_LOGIN_URL",
+ | "venue-info-url": "$TEST_VENUE_INFO_URL"
+ |}
+ """.trimMargin())
+
// Handle the DHCP handshake that includes the capport API URL
val discover = reader.assertDhcpPacketReceived(
DhcpDiscoverPacket::class.java, TEST_TIMEOUT_MS, DHCP_MESSAGE_TYPE_DISCOVER)
@@ -163,11 +170,9 @@
assertEquals(clientIpAddr, request.mRequestedIp)
reader.sendResponse(makeAckPacket(request.clientMac, request.transactionId))
- // Expect a request to the capport API
- val capportReq = httpServer.recordedRequests.poll(TEST_TIMEOUT_MS, TimeUnit.MILLISECONDS)
- assertNotNull(capportReq, "The device did not fetch captive portal API data within timeout")
- assertEquals(capportUrl.path, capportReq.uri)
- assertEquals(capportUrl.query, capportReq.queryParameterString)
+ // The first request received by the server should be for the portal API
+ assertTrue(httpServer.requestsRecord.poll(TEST_TIMEOUT_MS, 0)?.matches(capportUrl) ?: false,
+ "The device did not fetch captive portal API data within timeout")
// Expect network callbacks with capport info
val testCb = TestableNetworkCallback(TEST_TIMEOUT_MS)
@@ -216,30 +221,6 @@
listOf(serverIpAddr) /* gateways */, listOf(serverIpAddr) /* dnsServers */,
serverIpAddr, TEST_DOMAIN_NAME, null /* hostname */, true /* metered */,
TEST_MTU, false /* rapidCommit */, capportUrl.toString())
-
- private fun parseDhcpPacket(bytes: ByteArray) = DhcpPacket.decodeFullPacket(
- bytes, MAX_PACKET_LENGTH, DhcpPacket.ENCAP_L2)
-}
-
-/**
- * A minimal HTTP server running on localhost (loopback), on a random available port.
- *
- * The server records each request in [recordedRequests] and will not serve any further request
- * until the last one is removed from the queue for verification.
- */
-private class HttpServer : NanoHTTPD("localhost", 0 /* auto-select the port */) {
- val recordedRequests = ArrayBlockingQueue<IHTTPSession>(1 /* capacity */)
-
- override fun serve(session: IHTTPSession): Response {
- recordedRequests.offer(session)
- return newFixedLengthResponse("""
- |{
- | "captive": true,
- | "user-portal-url": "$TEST_LOGIN_URL",
- | "venue-info-url": "$TEST_VENUE_INFO_URL"
- |}
- """.trimMargin())
- }
}
private fun <T : DhcpPacket> TapPacketReader.assertDhcpPacketReceived(
@@ -259,12 +240,3 @@
private fun <T> Context.assertHasService(manager: Class<T>): T {
return getSystemService(manager) ?: fail("Service $manager not found")
}
-
-/**
- * Wrapper around runWithShellPermissionIdentity with kotlin-like syntax.
- */
-private fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
- var ret: T? = null
- runWithShellPermissionIdentity(ThrowingRunnable { ret = task() }, *permissions)
- return ret ?: fail("ThrowingRunnable was not run")
-}
diff --git a/tests/tests/net/src/android/net/cts/NetworkValidationTestUtil.kt b/tests/tests/net/src/android/net/cts/NetworkValidationTestUtil.kt
new file mode 100644
index 0000000..5ef1854
--- /dev/null
+++ b/tests/tests/net/src/android/net/cts/NetworkValidationTestUtil.kt
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * 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.
+ */
+
+package android.net.cts
+
+import android.Manifest
+import android.net.util.NetworkStackUtils
+import android.provider.DeviceConfig
+import com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity
+import com.android.compatibility.common.util.ThrowingRunnable
+import kotlin.test.fail
+
+/**
+ * Collection of utility methods for configuring network validation.
+ */
+internal object NetworkValidationTestUtil {
+
+ /**
+ * Clear the test network validation URLs.
+ */
+ fun clearValidationTestUrlsDeviceConfig() {
+ setHttpsUrlDeviceConfig(null)
+ setHttpUrlDeviceConfig(null)
+ setUrlExpirationDeviceConfig(null)
+ }
+
+ /**
+ * Set the test validation HTTPS URL.
+ *
+ * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL
+ */
+ fun setHttpsUrlDeviceConfig(url: String?) =
+ setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTPS_URL, url)
+
+ /**
+ * Set the test validation HTTP URL.
+ *
+ * @see NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL
+ */
+ fun setHttpUrlDeviceConfig(url: String?) =
+ setConfig(NetworkStackUtils.TEST_CAPTIVE_PORTAL_HTTP_URL, url)
+
+ /**
+ * Set the test validation URL expiration.
+ *
+ * @see NetworkStackUtils.TEST_URL_EXPIRATION_TIME
+ */
+ fun setUrlExpirationDeviceConfig(timestamp: Long?) =
+ setConfig(NetworkStackUtils.TEST_URL_EXPIRATION_TIME, timestamp?.toString())
+
+ private fun setConfig(configKey: String, value: String?) {
+ runAsShell(Manifest.permission.WRITE_DEVICE_CONFIG) {
+ DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_CONNECTIVITY, configKey, value, false /* makeDefault */)
+ }
+ }
+
+ /**
+ * Wrapper around runWithShellPermissionIdentity with kotlin-like syntax.
+ */
+ fun <T> runAsShell(vararg permissions: String, task: () -> T): T {
+ var ret: T? = null
+ runWithShellPermissionIdentity(ThrowingRunnable { ret = task() }, *permissions)
+ return ret ?: fail("ThrowingRunnable did not return")
+ }
+}
\ No newline at end of file