diff options
author | Treehugger Robot <android-test-infra-autosubmit@system.gserviceaccount.com> | 2024-05-17 12:24:06 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2024-05-17 12:24:06 +0000 |
commit | 144d2108d3d9e441cf32d9880a0c3174e79a0747 (patch) | |
tree | 0d98e0fe83814e419ddd77bd45f46a6385823b94 | |
parent | 55e4ddcd8c9f462f9409d2a7ab8a7929d839359f (diff) | |
parent | b81730a65834a22e597d39bfce5b9e44c0d1dbd7 (diff) | |
download | cronet-main.tar.gz |
30 files changed, 3513 insertions, 3 deletions
diff --git a/Android.extras.bp b/Android.extras.bp index ab649813f..fb0705279 100644 --- a/Android.extras.bp +++ b/Android.extras.bp @@ -88,7 +88,6 @@ java_library { sdk_version: "module_current", min_sdk_version: "30", visibility: [ - "//packages/modules/Connectivity:__subpackages__", "//external/cronet/android:__subpackages__", ], apex_available: [ @@ -170,7 +169,7 @@ filegroup { "components/cronet/testing/test_server/data/**/*", ], visibility: [ - "//packages/modules/Connectivity:__subpackages__", + "//external/cronet/android/tests:__subpackages__", ], } @@ -201,7 +200,7 @@ android_library { ], lint: { test: true }, visibility: [ - "//packages/modules/Connectivity:__subpackages__", + "//external/cronet/android/tests:__subpackages__", ], } diff --git a/TEST_MAPPING b/TEST_MAPPING index 2f43f267d..63e797f46 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -11,5 +11,39 @@ { "name": "cronet_unittests_tester" } + // Runs both NetHttpTests and CtsNetHttpTestCases + // TODO(b/338358650): Add to presubmit after SLO checker is verified. + // { + // "name": "NetHttpCoverageTests", + // "options": [ + // { + // "exclude-annotation": "com.android.testutils.SkipPresubmit" + // }, + // { + // // These sometimes take longer than 1 min which is the presubmit timeout + // "exclude-annotation": "androidx.test.filters.LargeTest" + // } + // ] + // } + ], + "postsubmit": [ + { + "name": "NetHttpCoverageTests" + } ] + // TODO(b/338358650): Add to presubmit after SLO checker is verified. + // "mainline-presubmit": [ + // { + // "name": "NetHttpCoverageTests[CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex]", + // "options": [ + // { + // "exclude-annotation": "com.android.testutils.SkipPresubmit" + // }, + // { + // // These sometimes take longer than 1 min which is the presubmit timeout + // "exclude-annotation": "androidx.test.filters.LargeTest" + // } + // ] + // } + // ] } diff --git a/android/tests/common/Android.bp b/android/tests/common/Android.bp new file mode 100644 index 000000000..875d919ce --- /dev/null +++ b/android/tests/common/Android.bp @@ -0,0 +1,75 @@ +// Copyright (C) 2023 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. +// + +// Tests in this folder are included both in unit tests and CTS. +// They must be fast and stable, and exercise public or test APIs. + +package { + default_team: "trendy_team_fwk_core_networking", + // See: http://go/android-license-faq + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "NetHttpCoverageTests", + enforce_default_target_sdk_version: true, + min_sdk_version: "30", + test_suites: [ + "general-tests", + "mts-tethering", + ], + static_libs: [ + "modules-utils-native-coverage-listener", + "CtsNetHttpTestsLib", + "NetHttpTestsLibPreJarJar", + ], + jarjar_rules: ":net-http-test-jarjar-rules", + compile_multilib: "both", // Include both the 32 and 64 bit versions + jni_libs: [ + "cronet_aml_components_cronet_android_cronet_tests__testing", + "cronet_aml_third_party_netty_tcnative_netty_tcnative_so__testing", + "libnativecoverage", + ], + data: [":cronet_javatests_resources"], +} + +// MTS-only specific targets. +java_genrule { + name: "net-http-test-jarjar-rules", + tool_files: [ + ":NetHttpTestsLibPreJarJar{.jar}", + "jarjar_excludes.txt", + ], + tools: [ + "jarjar-rules-generator", + ], + out: ["net_http_test_jarjar_rules.txt"], + cmd: "$(location jarjar-rules-generator) " + + "$(location :NetHttpTestsLibPreJarJar{.jar}) " + + "--prefix android.net.connectivity " + + "--excludes $(location jarjar_excludes.txt) " + + "--output $(out)", +} + +android_library { + name: "NetHttpTestsLibPreJarJar", + static_libs: [ + "cronet_aml_api_java", + "cronet_aml_java__testing", + "cronet_java_tests", + ], + sdk_version: "module_current", + min_sdk_version: "30", +} diff --git a/android/tests/common/AndroidManifest.xml b/android/tests/common/AndroidManifest.xml new file mode 100644 index 000000000..418af8641 --- /dev/null +++ b/android/tests/common/AndroidManifest.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + package="com.android.net.http.tests.coverage"> + + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.INTERNET"/> + + <application android:networkSecurityConfig="@xml/network_security_config" + tools:replace="android:label" + android:label="NetHttp coverage tests" > + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.net.http.tests.coverage" + android:label="NetHttp coverage tests"> + </instrumentation> +</manifest> diff --git a/android/tests/common/AndroidTest.xml b/android/tests/common/AndroidTest.xml new file mode 100644 index 000000000..bb7ed1147 --- /dev/null +++ b/android/tests/common/AndroidTest.xml @@ -0,0 +1,59 @@ +<!-- + ~ Copyright (C) 2023 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. + --> +<configuration description="Runs coverage tests for NetHttp"> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="test-file-name" value="NetHttpCoverageTests.apk" /> + <option name="install-arg" value="-t" /> + </target_preparer> + <option name="test-tag" value="NetHttpCoverageTests" /> + + <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> + <option name="push-file" key="net" value="/storage/emulated/0/chromium_tests_root/net" /> + <option name="push-file" key="test_server" value="/storage/emulated/0/chromium_tests_root/components/cronet/testing/test_server" /> + </target_preparer> + <!-- Tethering/Connectivity is a SDK 30+ module however Cronet is installed on 31+ due to b/270049141. --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.Sdk31ModuleController" /> + <!-- Only run NetHttpCoverageTests in MTS if the Tethering Mainline module is installed. --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.tethering" /> + </object> + <option name="config-descriptor:metadata" key="mainline-param" + value="CaptivePortalLoginGoogle.apk+NetworkStackGoogle.apk+com.google.android.resolv.apex+com.google.android.tethering.apex" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.net.http.tests.coverage" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <!-- b/298380508 --> + <option name="exclude-filter" value="org.chromium.net.CronetUrlRequestContextTest#testSetLibraryLoaderIsEnforcedByDefaultEmbeddedProvider" /> + <!-- b/316559294 --> + <option name="exclude-filter" value="org.chromium.net.NQETest#testQuicDisabled" /> + <!-- b/316559294 --> + <option name="exclude-filter" value="org.chromium.net.NQETest#testPrefsWriteRead" /> + <!-- b/316554711--> + <option name="exclude-filter" value="org.chromium.net.NetworkChangesTest" /> + <!-- b/316550794 --> + <option name="exclude-filter" value="org.chromium.net.impl.CronetLoggerTest#testEngineCreation" /> + <!-- b/327182569 --> + <option name="exclude-filter" value="org.chromium.net.urlconnection.CronetURLStreamHandlerFactoryTest#testSetUrlStreamFactoryUsesCronetForNative" /> + <option name="hidden-api-checks" value="false"/> + <option name="isolated-storage" value="false"/> + <option name="orchestrator" value="true"/> + <option + name="device-listeners" + value="com.android.modules.utils.testing.NativeCoverageHackInstrumentationListener" /> + </test> +</configuration> diff --git a/android/tests/common/jarjar_excludes.txt b/android/tests/common/jarjar_excludes.txt new file mode 100644 index 000000000..b5cdf6e16 --- /dev/null +++ b/android/tests/common/jarjar_excludes.txt @@ -0,0 +1,29 @@ +# Exclude some test prefixes, as they can't be found after being jarjared. +com\.android\.testutils\..+ +# jarjar-gen can't handle some kotlin object expression, exclude packages that include them +androidx\..+ +# don't jarjar netty as it does JNI +io\.netty\..+ +kotlin\.test\..+ +kotlin\.reflect\..+ +org\.mockito\..+ +# Do not jarjar the api classes +android\.net\..+ +# cronet_tests.so is not jarjared and uses base classes. We can remove this when there's a +# separate java base target to depend on. +org\.chromium\.base\..+ +J\.cronet_tests_N(\$.+)? + +# don't jarjar automatically generated FooJni files. +org\.chromium\.net\..+Jni(\$.+)? + +# Do not jarjar the tests and its utils as they also do JNI with cronet_tests.so +org\.chromium\.net\..*Test.*(\$.+)? +org\.chromium\.net\.NativeTestServer(\$.+)? +org\.chromium\.net\.MockUrlRequestJobFactory(\$.+)? +org\.chromium\.net\.QuicTestServer(\$.+)? +org\.chromium\.net\.MockCertVerifier(\$.+)? +org\.chromium\.net\.LogcatCapture(\$.+)? +org\.chromium\.net\.ReportingCollector(\$.+)? +org\.chromium\.net\.Http2TestServer(\$.+)? +org\.chromium\.net\.Http2TestHandler(\$.+)?
\ No newline at end of file diff --git a/android/tests/common/res/raw/quicroot.pem b/android/tests/common/res/raw/quicroot.pem new file mode 100644 index 000000000..af21b3e9a --- /dev/null +++ b/android/tests/common/res/raw/quicroot.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIC/jCCAeagAwIBAgIUXOi6XoxnMUjJg4jeOwRhsdqEqEQwDQYJKoZIhvcNAQEL +BQAwFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMB4XDTIzMDYwMTExMjcwMFoXDTMz +MDUyOTExMjcwMFowFzEVMBMGA1UEAwwMVGVzdCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAl9xCMPMIvfmJWz25AG/VtgWbqNs67HXQbXWf +pDF2wjQpHVOYbfl7Zgly5O+5es1aUbJaGyZ9G6xuYSXKFnnYLoP7M86O05fQQBAj +K+IE5nO6136ksCAfxCFTFfn4vhPvK8Vba5rqox4WeIXYKvHYSoiHz0ELrnFOHcyN +Innyze7bLtkMCA1ShHpmvDCR+U3Uj6JwOfoirn29jjU/48/ORha7dcJYtYXk2eGo +RJfrtIx20tXAaKaGnXOCGYbEVXTeQkQPqKFVzqP7+KYS/Y8eNFV35ugpLNES+44T +bQ2QruTZdrNRjJkEoyiB/E53a0OUltB/R7Z0L0xstnKfsAf3OwIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUVdXNh2lk +51/6hMmz0Z+OpIe8+f0wDQYJKoZIhvcNAQELBQADggEBADNg7G8n6DUrQ5doXzm9 +kOp5siX6iPs0zFReXKhIT1Gef63l3tb7AdPedF03aj9XkUt0shhNOGG5SK2k5KBQ +MJc9muYRCAyo2xMr3rFUQdI5B51SCy5HeAMralgTHXN0Hv+TH04YfRrACVmr+5ke +pH3bF1gYaT+Zy5/pHJnV5lcwS6/H44g9XXWIopjWCwbfzKxIuWofqL4fiToPSIYu +MCUI4bKZipcJT5O6rdz/S9lbgYVjOJ4HAoT2icNQqNMMfULKevmF8SdJzfNd35yn +tAKTROhIE2aQRVCclrjo/T3eyjWGGoJlGmxKbeCf/rXzcn1BRtk/UzLnbUFFlg5l +axw= +-----END CERTIFICATE-----
\ No newline at end of file diff --git a/android/tests/common/res/values/cronet-test-rule-configuration.xml b/android/tests/common/res/values/cronet-test-rule-configuration.xml new file mode 100644 index 000000000..48ce420ab --- /dev/null +++ b/android/tests/common/res/values/cronet-test-rule-configuration.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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. + --> + +<resources> + <bool name="is_running_in_aosp">true</bool> +</resources>
\ No newline at end of file diff --git a/android/tests/common/res/xml/network_security_config.xml b/android/tests/common/res/xml/network_security_config.xml new file mode 100644 index 000000000..32b717114 --- /dev/null +++ b/android/tests/common/res/xml/network_security_config.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2022 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. + --> + +<network-security-config> + <base-config> + <trust-anchors> + <certificates src="@raw/quicroot"/> + <certificates src="system"/> + </trust-anchors> + </base-config> + <!-- Since Android 9 (API 28) cleartext support is disabled by default, this + causes some of our tests to fail (see crbug/1220357). + The following configs allow http requests for the domains used in these + tests. + + TODO(stefanoduo): Figure out if we really need to use http for these tests + --> + <domain-config cleartextTrafficPermitted="true"> + <!-- Used as the base URL by native test server (net::EmbeddedTestServer) --> + <domain includeSubdomains="true">127.0.0.1</domain> + <!-- Used by CronetHttpURLConnectionTest#testIOExceptionInterruptRethrown --> + <domain includeSubdomains="true">localhost</domain> + <!-- Used by CronetHttpURLConnectionTest#testBadIP --> + <domain includeSubdomains="true">0.0.0.0</domain> + <!-- Used by CronetHttpURLConnectionTest#testSetUseCachesFalse --> + <domain includeSubdomains="true">host-cache-test-host</domain> + <!-- Used by CronetHttpURLConnectionTest#testBadHostname --> + <domain includeSubdomains="true">this-weird-host-name-does-not-exist</domain> + <!-- Used by CronetUrlRequestContextTest#testHostResolverRules --> + <domain includeSubdomains="true">some-weird-hostname</domain> + </domain-config> +</network-security-config>
\ No newline at end of file diff --git a/android/tests/cts/Android.bp b/android/tests/cts/Android.bp new file mode 100644 index 000000000..92b73d954 --- /dev/null +++ b/android/tests/cts/Android.bp @@ -0,0 +1,71 @@ +// +// Copyright (C) 2019 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 { + default_team: "trendy_team_fwk_core_networking", + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_library { + name: "CtsNetHttpTestsLib", + defaults: [ + "cts_defaults", + ], + sdk_version: "test_current", + min_sdk_version: "30", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "androidx.test.ext.junit", + "ctstestrunner-axt", + "ctstestserver", + "hamcrest-library", + "junit", + "kotlin-test", + "mockito-target", + "net-tests-utils", + "truth", + ], + libs: [ + "android.test.base", + "androidx.annotation_annotation", + "framework-connectivity", + "org.apache.http.legacy", + ], + lint: { + test: true, + }, +} + +android_test { + name: "CtsNetHttpTestCases", + defaults: [ + "cts_defaults", + ], + enforce_default_target_sdk_version: true, + sdk_version: "test_current", + min_sdk_version: "30", + static_libs: ["CtsNetHttpTestsLib"], + // Tag this as a cts test artifact + test_suites: [ + "cts", + "general-tests", + "mts-tethering", + "mcts-tethering", + ], +} diff --git a/android/tests/cts/AndroidManifest.xml b/android/tests/cts/AndroidManifest.xml new file mode 100644 index 000000000..26900b2af --- /dev/null +++ b/android/tests/cts/AndroidManifest.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/* + * Copyright (C) 2019 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. + */ +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="android.net.http.cts"> + + <uses-permission android:name="android.permission.INTERNET"/> + <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/> + + <application android:networkSecurityConfig="@xml/network_security_config"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation + android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="android.net.http.cts" + android:label="CTS tests of android.net.http"> + </instrumentation> +</manifest> diff --git a/android/tests/cts/AndroidTest.xml b/android/tests/cts/AndroidTest.xml new file mode 100644 index 000000000..e0421fd59 --- /dev/null +++ b/android/tests/cts/AndroidTest.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2019 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. + --> +<configuration description="Config for CTS Cronet test cases"> + <option name="test-suite-tag" value="cts" /> + <option name="config-descriptor:metadata" key="component" value="networking" /> + <!-- Instant apps cannot create sockets. See b/264248246 --> + <option name="config-descriptor:metadata" key="parameter" value="not_instant_app" /> + <option name="config-descriptor:metadata" key="parameter" value="multi_abi" /> + <option name="config-descriptor:metadata" key="parameter" value="secondary_user" /> + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="CtsNetHttpTestCases.apk" /> + </target_preparer> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="android.net.http.cts" /> + <option name="runtime-hint" value="10s" /> + </test> + + <!-- Only run CtsNetHttpTestCases in MTS if the Tethering Mainline module is installed. --> + <object type="module_controller" + class="com.android.tradefed.testtype.suite.module.MainlineTestModuleController"> + <option name="mainline-module-package-name" value="com.google.android.tethering" /> + </object> +</configuration> diff --git a/android/tests/cts/assets/html/hello_world.html b/android/tests/cts/assets/html/hello_world.html new file mode 100644 index 000000000..ea62ce2b4 --- /dev/null +++ b/android/tests/cts/assets/html/hello_world.html @@ -0,0 +1,24 @@ +<!-- + ~ Copyright (C) 2022 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. + --> + +<html> +<head> + <title>hello world</title> +</head> +<body> +<h3>hello world</h3><br> +</body> +</html>
\ No newline at end of file diff --git a/android/tests/cts/res/xml/network_security_config.xml b/android/tests/cts/res/xml/network_security_config.xml new file mode 100644 index 000000000..7d7530bfc --- /dev/null +++ b/android/tests/cts/res/xml/network_security_config.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + ~ Copyright (C) 2022 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. + --> + +<network-security-config> + <domain-config cleartextTrafficPermitted="true"> + <domain includeSubdomains="true">localhost</domain> + </domain-config> +</network-security-config>
\ No newline at end of file diff --git a/android/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt b/android/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt new file mode 100644 index 000000000..464862d8a --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/BidirectionalStreamTest.kt @@ -0,0 +1,201 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.content.Context +import android.net.http.BidirectionalStream +import android.net.http.HttpEngine +import android.net.http.cts.util.TestBidirectionalStreamCallback +import android.net.http.cts.util.TestBidirectionalStreamCallback.ResponseStep +import android.net.http.cts.util.assumeOKStatusCode +import android.net.http.cts.util.skipIfNoInternetConnection +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.android.testutils.SkipPresubmit +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlin.test.assertEquals +import org.hamcrest.MatcherAssert +import org.hamcrest.Matchers +import org.junit.After +import org.junit.AssumptionViolatedException +import org.junit.Before +import org.junit.runner.RunWith + +private const val URL = "https://source.android.com" + +/** + * This tests uses a non-hermetic server. Instead of asserting, assume the next callback. This way, + * if the request were to fail, the test would just be skipped instead of failing. + */ +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class BidirectionalStreamTest { + private val context: Context = ApplicationProvider.getApplicationContext() + private val callback = TestBidirectionalStreamCallback() + private val httpEngine = HttpEngine.Builder(context).build() + private var stream: BidirectionalStream? = null + + @Before + fun setUp() { + skipIfNoInternetConnection(context) + } + + @After + @Throws(Exception::class) + fun tearDown() { + httpEngine.shutdown() + } + + private fun createBidirectionalStreamBuilder(url: String): BidirectionalStream.Builder { + return httpEngine.newBidirectionalStreamBuilder(url, callback.executor, callback) + } + + @Test + @Throws(Exception::class) + @SkipPresubmit(reason = "b/293141085 Confirm non-flaky and move to presubmit after SLO") + fun testBidirectionalStream_GetStream_CompletesSuccessfully() { + stream = createBidirectionalStreamBuilder(URL).setHttpMethod("GET").build() + stream!!.start() + // We call to a real server and hence the server may not be reachable, cancel this stream + // and rethrow the exception before tearDown, + // otherwise shutdown would fail with active request error. + try { + callback.assumeCallback(ResponseStep.ON_SUCCEEDED) + } catch (e: AssumptionViolatedException) { + stream!!.cancel() + callback.blockForDone() + throw e + } + + val info = callback.mResponseInfo + assumeOKStatusCode(info) + MatcherAssert.assertThat( + "Received byte count must be > 0", info.receivedByteCount, Matchers.greaterThan(0L)) + assertEquals("h2", info.negotiatedProtocol) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_getHttpMethod() { + val builder = createBidirectionalStreamBuilder(URL) + val method = "GET" + + builder.setHttpMethod(method) + stream = builder.build() + assertThat(stream!!.getHttpMethod()).isEqualTo(method) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_hasTrafficStatsTag() { + val builder = createBidirectionalStreamBuilder(URL) + + builder.setTrafficStatsTag(10) + stream = builder.build() + assertThat(stream!!.hasTrafficStatsTag()).isTrue() + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_getTrafficStatsTag() { + val builder = createBidirectionalStreamBuilder(URL) + val trafficStatsTag = 10 + + builder.setTrafficStatsTag(trafficStatsTag) + stream = builder.build() + assertThat(stream!!.getTrafficStatsTag()).isEqualTo(trafficStatsTag) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_hasTrafficStatsUid() { + val builder = createBidirectionalStreamBuilder(URL) + + builder.setTrafficStatsUid(10) + stream = builder.build() + assertThat(stream!!.hasTrafficStatsUid()).isTrue() + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_getTrafficStatsUid() { + val builder = createBidirectionalStreamBuilder(URL) + val trafficStatsUid = 10 + + builder.setTrafficStatsUid(trafficStatsUid) + stream = builder.build() + assertThat(stream!!.getTrafficStatsUid()).isEqualTo(trafficStatsUid) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_getHeaders_asList() { + val builder = createBidirectionalStreamBuilder(URL) + val expectedHeaders = mapOf( + "Authorization" to "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==", + "Max-Forwards" to "10", + "X-Client-Data" to "random custom header content").entries.toList() + + for (header in expectedHeaders) { + builder.addHeader(header.key, header.value) + } + + stream = builder.build() + assertThat(stream!!.getHeaders().getAsList()).containsAtLeastElementsIn(expectedHeaders) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_getHeaders_asMap() { + val builder = createBidirectionalStreamBuilder(URL) + val expectedHeaders = mapOf( + "Authorization" to listOf("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="), + "Max-Forwards" to listOf("10"), + "X-Client-Data" to listOf("random custom header content")) + + for (header in expectedHeaders) { + builder.addHeader(header.key, header.value.get(0)) + } + + stream = builder.build() + assertThat(stream!!.getHeaders().getAsMap()).containsAtLeastEntriesIn(expectedHeaders) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_getPriority() { + val builder = createBidirectionalStreamBuilder(URL) + val priority = BidirectionalStream.STREAM_PRIORITY_LOW + + builder.setPriority(priority) + stream = builder.build() + assertThat(stream!!.getPriority()).isEqualTo(priority) + } + + @Test + @Throws(Exception::class) + fun testBidirectionalStream_isDelayRequestHeadersUntilFirstFlushEnabled() { + val builder = createBidirectionalStreamBuilder(URL) + + builder.setDelayRequestHeadersUntilFirstFlushEnabled(true) + stream = builder.build() + assertThat(stream!!.isDelayRequestHeadersUntilFirstFlushEnabled()).isTrue() + } +} diff --git a/android/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt b/android/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt new file mode 100644 index 000000000..1405ed90c --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/CallbackExceptionTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.content.Context +import android.net.http.CallbackException +import android.net.http.HttpEngine +import android.net.http.cts.util.HttpCtsTestServer +import android.net.http.cts.util.TestUrlRequestCallback +import android.net.http.cts.util.TestUrlRequestCallback.FailureType +import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertSame +import kotlin.test.assertTrue +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class CallbackExceptionTest { + + @Test + fun testCallbackException_returnsInputParameters() { + val message = "failed" + val cause = Throwable("exception") + val callbackException = object : CallbackException(message, cause) {} + + assertEquals(message, callbackException.message) + assertSame(cause, callbackException.cause) + } + + @Test + fun testCallbackException_thrownFromUrlRequest() { + val context: Context = ApplicationProvider.getApplicationContext() + val server = HttpCtsTestServer(context) + val httpEngine = HttpEngine.Builder(context).build() + val callback = TestUrlRequestCallback() + callback.setFailure(FailureType.THROW_SYNC, ResponseStep.ON_RESPONSE_STARTED) + val request = httpEngine + .newUrlRequestBuilder(server.successUrl, callback.executor, callback) + .build() + + request.start() + callback.blockForDone() + + assertTrue(request.isDone) + assertIs<CallbackException>(callback.mError) + server.shutdown() + httpEngine.shutdown() + } +} diff --git a/android/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt b/android/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt new file mode 100644 index 000000000..10c7f3c30 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.net.http.ConnectionMigrationOptions +import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED +import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED +import android.os.Build +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class ConnectionMigrationOptionsTest { + + @Test + fun testConnectionMigrationOptions_defaultValues() { + val options = + ConnectionMigrationOptions.Builder().build() + + assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.allowNonDefaultNetworkUsage) + assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.defaultNetworkMigration) + assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.pathDegradationMigration) + } + + @Test + fun testConnectionMigrationOptions_enableDefaultNetworkMigration_returnSetValue() { + val options = + ConnectionMigrationOptions.Builder() + .setDefaultNetworkMigration(MIGRATION_OPTION_ENABLED) + .build() + + assertEquals(MIGRATION_OPTION_ENABLED, options.defaultNetworkMigration) + } + + @Test + fun testConnectionMigrationOptions_enablePathDegradationMigration_returnSetValue() { + val options = + ConnectionMigrationOptions.Builder() + .setPathDegradationMigration(MIGRATION_OPTION_ENABLED) + .build() + + assertEquals(MIGRATION_OPTION_ENABLED, options.pathDegradationMigration) + } + + @Test + fun testConnectionMigrationOptions_allowNonDefaultNetworkUsage_returnSetValue() { + val options = + ConnectionMigrationOptions.Builder() + .setAllowNonDefaultNetworkUsage(MIGRATION_OPTION_ENABLED).build() + + assertEquals(MIGRATION_OPTION_ENABLED, options.allowNonDefaultNetworkUsage) + } +} diff --git a/android/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt b/android/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt new file mode 100644 index 000000000..56802c6b2 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/DnsOptionsTest.kt @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.net.http.DnsOptions +import android.net.http.DnsOptions.DNS_OPTION_ENABLED +import android.net.http.DnsOptions.DNS_OPTION_UNSPECIFIED +import android.os.Build +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import java.time.Duration +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class DnsOptionsTest { + + @Test + fun testDnsOptions_defaultValues() { + val options = DnsOptions.Builder().build() + + assertEquals(DNS_OPTION_UNSPECIFIED, options.persistHostCache) + assertNull(options.persistHostCachePeriod) + assertEquals(DNS_OPTION_UNSPECIFIED, options.staleDns) + assertNull(options.staleDnsOptions) + assertEquals(DNS_OPTION_UNSPECIFIED, options.useHttpStackDnsResolver) + assertEquals(DNS_OPTION_UNSPECIFIED, + options.preestablishConnectionsToStaleDnsResults) + } + + @Test + fun testDnsOptions_persistHostCache_returnSetValue() { + val options = DnsOptions.Builder() + .setPersistHostCache(DNS_OPTION_ENABLED) + .build() + + assertEquals(DNS_OPTION_ENABLED, options.persistHostCache) + } + + @Test + fun testDnsOptions_persistHostCachePeriod_returnSetValue() { + val period = Duration.ofMillis(12345) + val options = DnsOptions.Builder().setPersistHostCachePeriod(period).build() + + assertEquals(period, options.persistHostCachePeriod) + } + + @Test + fun testDnsOptions_enableStaleDns_returnSetValue() { + val options = DnsOptions.Builder() + .setStaleDns(DNS_OPTION_ENABLED) + .build() + + assertEquals(DNS_OPTION_ENABLED, options.staleDns) + } + + @Test + fun testDnsOptions_useHttpStackDnsResolver_returnsSetValue() { + val options = DnsOptions.Builder() + .setUseHttpStackDnsResolver(DNS_OPTION_ENABLED) + .build() + + assertEquals(DNS_OPTION_ENABLED, options.useHttpStackDnsResolver) + } + + @Test + fun testDnsOptions_preestablishConnectionsToStaleDnsResults_returnsSetValue() { + val options = DnsOptions.Builder() + .setPreestablishConnectionsToStaleDnsResults(DNS_OPTION_ENABLED) + .build() + + assertEquals(DNS_OPTION_ENABLED, + options.preestablishConnectionsToStaleDnsResults) + } + + @Test + fun testDnsOptions_setStaleDnsOptions_returnsSetValues() { + val staleOptions = DnsOptions.StaleDnsOptions.Builder() + .setAllowCrossNetworkUsage(DNS_OPTION_ENABLED) + .setFreshLookupTimeout(Duration.ofMillis(1234)) + .build() + val options = DnsOptions.Builder() + .setStaleDns(DNS_OPTION_ENABLED) + .setStaleDnsOptions(staleOptions) + .build() + + assertEquals(DNS_OPTION_ENABLED, options.staleDns) + assertEquals(staleOptions, options.staleDnsOptions) + } + + @Test + fun testStaleDnsOptions_defaultValues() { + val options = DnsOptions.StaleDnsOptions.Builder().build() + + assertEquals(DNS_OPTION_UNSPECIFIED, options.allowCrossNetworkUsage) + assertNull(options.freshLookupTimeout) + assertNull(options.maxExpiredDelay) + assertEquals(DNS_OPTION_UNSPECIFIED, options.useStaleOnNameNotResolved) + } + + @Test + fun testStaleDnsOptions_allowCrossNetworkUsage_returnsSetValue() { + val options = DnsOptions.StaleDnsOptions.Builder() + .setAllowCrossNetworkUsage(DNS_OPTION_ENABLED).build() + + assertEquals(DNS_OPTION_ENABLED, options.allowCrossNetworkUsage) + } + + @Test + fun testStaleDnsOptions_freshLookupTimeout_returnsSetValue() { + val duration = Duration.ofMillis(12345) + val options = DnsOptions.StaleDnsOptions.Builder().setFreshLookupTimeout(duration).build() + + assertNotNull(options.freshLookupTimeout) + assertEquals(duration, options.freshLookupTimeout!!) + } + + @Test + fun testStaleDnsOptions_useStaleOnNameNotResolved_returnsSetValue() { + val options = DnsOptions.StaleDnsOptions.Builder() + .setUseStaleOnNameNotResolved(DNS_OPTION_ENABLED) + .build() + + assertEquals(DNS_OPTION_ENABLED, options.useStaleOnNameNotResolved) + } + + @Test + fun testStaleDnsOptions_maxExpiredDelayMillis_returnsSetValue() { + val duration = Duration.ofMillis(12345) + val options = DnsOptions.StaleDnsOptions.Builder().setMaxExpiredDelay(duration).build() + + assertNotNull(options.maxExpiredDelay) + assertEquals(duration, options.maxExpiredDelay!!) + } +} diff --git a/android/tests/cts/src/android/net/http/cts/HttpEngineTest.java b/android/tests/cts/src/android/net/http/cts/HttpEngineTest.java new file mode 100644 index 000000000..f86ac2990 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/HttpEngineTest.java @@ -0,0 +1,402 @@ +/* + * Copyright (C) 2023 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.http.cts; + +import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode; +import static android.net.http.cts.util.TestUtilsKt.assumeOKStatusCode; +import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection; + +import static com.google.common.truth.Truth.assertThat; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.net.Network; +import android.net.http.ConnectionMigrationOptions; +import android.net.http.DnsOptions; +import android.net.http.HttpEngine; +import android.net.http.QuicOptions; +import android.net.http.UrlRequest; +import android.net.http.UrlResponseInfo; +import android.net.http.cts.util.HttpCtsTestServer; +import android.net.http.cts.util.TestUrlRequestCallback; +import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep; +import android.os.Build; + +import androidx.test.core.app.ApplicationProvider; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mockito; + +import java.time.Instant; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Set; + +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +public class HttpEngineTest { + private static final String HOST = "source.android.com"; + private static final String URL = "https://" + HOST; + + private HttpEngine.Builder mEngineBuilder; + private TestUrlRequestCallback mCallback; + private HttpCtsTestServer mTestServer; + private UrlRequest mRequest; + private HttpEngine mEngine; + private Context mContext; + + @Before + public void setUp() throws Exception { + mContext = ApplicationProvider.getApplicationContext(); + skipIfNoInternetConnection(mContext); + mEngineBuilder = new HttpEngine.Builder(mContext); + mCallback = new TestUrlRequestCallback(); + mTestServer = new HttpCtsTestServer(mContext); + } + + @After + public void tearDown() throws Exception { + if (mRequest != null) { + mRequest.cancel(); + mCallback.blockForDone(); + } + if (mEngine != null) { + mEngine.shutdown(); + } + if (mTestServer != null) { + mTestServer.shutdown(); + } + } + + private boolean isQuic(String negotiatedProtocol) { + return negotiatedProtocol.startsWith("http/2+quic") || negotiatedProtocol.startsWith("h3"); + } + + @Test + public void testHttpEngine_Default() throws Exception { + mEngine = mEngineBuilder.build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + // This tests uses a non-hermetic server. Instead of asserting, assume the next callback. + // This way, if the request were to fail, the test would just be skipped instead of failing. + mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + assertEquals("h2", info.getNegotiatedProtocol()); + } + + @Test + public void testHttpEngine_EnableHttpCache() { + String url = mTestServer.getCacheableTestDownloadUrl( + /* downloadId */ "cacheable-download", + /* numBytes */ 10); + mEngine = + mEngineBuilder + .setStoragePath(mContext.getApplicationInfo().dataDir) + .setEnableHttpCache( + HttpEngine.Builder.HTTP_CACHE_DISK, /* maxSize */ 100 * 1024) + .build(); + + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assumeOKStatusCode(info); + assertFalse(info.wasCached()); + + mCallback = new TestUrlRequestCallback(); + builder = mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + info = mCallback.mResponseInfo; + assertOKStatusCode(info); + assertTrue(info.wasCached()); + } + + @Test + public void testHttpEngine_DisableHttp2() throws Exception { + mEngine = mEngineBuilder.setEnableHttp2(false).build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + // This tests uses a non-hermetic server. Instead of asserting, assume the next callback. + // This way, if the request were to fail, the test would just be skipped instead of failing. + mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + assertEquals("http/1.1", info.getNegotiatedProtocol()); + } + + @Test + public void testHttpEngine_EnablePublicKeyPinningBypassForLocalTrustAnchors() { + String url = mTestServer.getSuccessUrl(); + // For known hosts, requests should succeed whether we're bypassing the local trust anchor + // or not. + mEngine = mEngineBuilder.setEnablePublicKeyPinningBypassForLocalTrustAnchors(false).build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + + mEngine.shutdown(); + mEngine = mEngineBuilder.setEnablePublicKeyPinningBypassForLocalTrustAnchors(true).build(); + mCallback = new TestUrlRequestCallback(); + builder = mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + + // TODO(b/270918920): We should also test with a certificate not present in the device's + // trusted store. + // This requires either: + // * Mocking the underlying CertificateVerifier. + // * Or, having the server return a root certificate not present in the device's trusted + // store. + // The former doesn't make sense for a CTS test as it would depend on the underlying + // implementation. The latter is something we should support once we write a proper test + // server. + } + + private byte[] generateSha256() { + byte[] sha256 = new byte[32]; + Arrays.fill(sha256, (byte) 58); + return sha256; + } + + private Instant instantInFuture(int secondsIntoFuture) { + Calendar cal = Calendar.getInstance(); + cal.add(Calendar.SECOND, secondsIntoFuture); + return cal.getTime().toInstant(); + } + + @Test + public void testHttpEngine_AddPublicKeyPins() { + // CtsTestServer, when set in SslMode.NO_CLIENT_AUTH (required to trigger + // certificate verification, needed by this test), uses a certificate that + // doesn't match the hostname. For this reason, CtsTestServer cannot be used + // by this test. + Instant expirationInstant = instantInFuture(/* secondsIntoFuture */ 100); + boolean includeSubdomains = true; + Set<byte[]> pinsSha256 = Set.of(generateSha256()); + mEngine = mEngineBuilder.addPublicKeyPins( + HOST, pinsSha256, includeSubdomains, expirationInstant).build(); + + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(URL, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + mCallback.expectCallback(ResponseStep.ON_FAILED); + assertNotNull("Expected an error", mCallback.mError); + } + + @Test + public void testHttpEngine_EnableQuic() throws Exception { + String url = mTestServer.getSuccessUrl(); + mEngine = mEngineBuilder.setEnableQuic(true).addQuicHint(HOST, 443, 443).build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + } + + @Test + public void testHttpEngine_GetDefaultUserAgent() throws Exception { + assertThat(mEngineBuilder.getDefaultUserAgent(), containsString("AndroidHttpClient")); + assertThat(mEngineBuilder.getDefaultUserAgent()).contains(HttpEngine.getVersionString()); + } + + @Test + public void testHttpEngine_requestUsesDefaultUserAgent() throws Exception { + mEngine = mEngineBuilder.build(); + + String url = mTestServer.getUserAgentUrl(); + UrlRequest request = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build(); + request.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + String receivedUserAgent = extractUserAgent(mCallback.mResponseAsString); + + assertThat(receivedUserAgent).isEqualTo(mEngineBuilder.getDefaultUserAgent()); + } + + @Test + public void testHttpEngine_requestUsesCustomUserAgent() throws Exception { + String userAgent = "CtsTests User Agent"; + mEngine = + new HttpEngine.Builder(ApplicationProvider.getApplicationContext()) + .setUserAgent(userAgent) + .build(); + + String url = mTestServer.getUserAgentUrl(); + UrlRequest request = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback).build(); + request.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + String receivedUserAgent = extractUserAgent(mCallback.mResponseAsString); + + assertThat(receivedUserAgent).isEqualTo(userAgent); + } + + private static String extractUserAgent(String userAgentResponseBody) { + // If someone wants to be evil and have the title HTML tag a part of the user agent, + // they'll have to fix this method :) + return userAgentResponseBody.replaceFirst(".*<title>", "").replaceFirst("</title>.*", ""); + } + + @Test + public void testHttpEngine_bindToNetwork() throws Exception { + // Create a fake Android.net.Network. Since that network doesn't exist, binding to + // that should end up in a failed request. + Network mockNetwork = Mockito.mock(Network.class); + Mockito.when(mockNetwork.getNetworkHandle()).thenReturn(123L); + String url = mTestServer.getSuccessUrl(); + + mEngine = mEngineBuilder.build(); + mEngine.bindToNetwork(mockNetwork); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + mCallback.expectCallback(ResponseStep.ON_FAILED); + } + + @Test + public void testHttpEngine_unbindFromNetwork() throws Exception { + // Create a fake Android.net.Network. Since that network doesn't exist, binding to + // that should end up in a failed request. + Network mockNetwork = Mockito.mock(Network.class); + Mockito.when(mockNetwork.getNetworkHandle()).thenReturn(123L); + String url = mTestServer.getSuccessUrl(); + + mEngine = mEngineBuilder.build(); + // Bind to the fake network but then unbind. This should result in a successful + // request. + mEngine.bindToNetwork(mockNetwork); + mEngine.bindToNetwork(null); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + } + + @Test + public void testHttpEngine_setConnectionMigrationOptions_requestSucceeds() { + ConnectionMigrationOptions options = new ConnectionMigrationOptions.Builder().build(); + mEngine = mEngineBuilder.setConnectionMigrationOptions(options).build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder( + mTestServer.getSuccessUrl(), mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + } + + @Test + public void testHttpEngine_setDnsOptions_requestSucceeds() { + DnsOptions options = new DnsOptions.Builder().build(); + mEngine = mEngineBuilder.setDnsOptions(options).build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder( + mTestServer.getSuccessUrl(), mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + } + + @Test + public void getVersionString_notEmpty() { + assertThat(HttpEngine.getVersionString()).isNotEmpty(); + } + + @Test + public void testHttpEngine_SetQuicOptions_RequestSucceedsWithQuic() throws Exception { + String url = mTestServer.getSuccessUrl(); + QuicOptions options = new QuicOptions.Builder().build(); + mEngine = mEngineBuilder + .setEnableQuic(true) + .addQuicHint(HOST, 443, 443) + .setQuicOptions(options) + .build(); + UrlRequest.Builder builder = + mEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + mRequest = builder.build(); + mRequest.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + + } + + @Test + public void testHttpEngine_enableBrotli_brotliAdvertised() { + mEngine = mEngineBuilder.setEnableBrotli(true).build(); + mRequest = + mEngine.newUrlRequestBuilder( + mTestServer.getEchoHeadersUrl(), mCallback.getExecutor(), mCallback) + .build(); + mRequest.start(); + + mCallback.assumeCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertThat(info.getHeaders().getAsMap().get("x-request-header-Accept-Encoding").toString()) + .contains("br"); + assertOKStatusCode(info); + } +} diff --git a/android/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt b/android/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt new file mode 100644 index 000000000..cff54b364 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/NetworkExceptionTest.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.net.http.HttpEngine +import android.net.http.NetworkException +import android.net.http.cts.util.TestUrlRequestCallback +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertSame +import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class NetworkExceptionTest { + + @Test + fun testNetworkException_returnsInputParameters() { + val message = "failed" + val cause = Throwable("thrown") + val networkException = + object : NetworkException(message, cause) { + override fun getErrorCode() = 0 + override fun isImmediatelyRetryable() = false + } + + assertEquals(message, networkException.message) + assertSame(cause, networkException.cause) + } + + @Test + fun testNetworkException_thrownFromUrlRequest() { + val httpEngine = HttpEngine.Builder(ApplicationProvider.getApplicationContext()).build() + val callback = TestUrlRequestCallback() + val request = + httpEngine.newUrlRequestBuilder("http://localhost", callback.executor, callback).build() + + request.start() + callback.blockForDone() + + assertTrue(request.isDone) + assertIs<NetworkException>(callback.mError) + httpEngine.shutdown() + } +} diff --git a/android/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt b/android/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt new file mode 100644 index 000000000..2705fc367 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/QuicExceptionTest.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.net.http.QuicException +import android.os.Build +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class QuicExceptionTest { + + @Test + fun testQuicException_returnsInputParameters() { + val message = "failed" + val cause = Throwable("thrown") + val quicException = + object : QuicException(message, cause) { + override fun getErrorCode() = 0 + override fun isImmediatelyRetryable() = false + } + + assertEquals(message, quicException.message) + assertEquals(cause, quicException.cause) + } + + // TODO: add test for QuicException triggered from HttpEngine +} diff --git a/android/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt b/android/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt new file mode 100644 index 000000000..da0b15c52 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/QuicOptionsTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.net.http.QuicOptions +import android.os.Build +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import com.google.common.truth.Truth.assertThat +import java.time.Duration +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class QuicOptionsTest { + @Test + fun testQuicOptions_defaultValues() { + val quicOptions = QuicOptions.Builder().build() + assertThat(quicOptions.allowedQuicHosts).isEmpty() + assertThat(quicOptions.handshakeUserAgent).isNull() + assertThat(quicOptions.idleConnectionTimeout).isNull() + assertFalse(quicOptions.hasInMemoryServerConfigsCacheSize()) + assertFailsWith(IllegalStateException::class) { + quicOptions.inMemoryServerConfigsCacheSize + } + } + + @Test + fun testQuicOptions_quicHostAllowlist_returnsAddedValues() { + val quicOptions = QuicOptions.Builder() + .addAllowedQuicHost("foo") + .addAllowedQuicHost("bar") + .addAllowedQuicHost("foo") + .addAllowedQuicHost("baz") + .build() + assertThat(quicOptions.allowedQuicHosts) + .containsExactly("foo", "bar", "baz") + .inOrder() + } + + @Test + fun testQuicOptions_idleConnectionTimeout_returnsSetValue() { + val timeout = Duration.ofMinutes(10) + val quicOptions = QuicOptions.Builder() + .setIdleConnectionTimeout(timeout) + .build() + assertThat(quicOptions.idleConnectionTimeout) + .isEqualTo(timeout) + } + + @Test + fun testQuicOptions_inMemoryServerConfigsCacheSize_returnsSetValue() { + val quicOptions = QuicOptions.Builder() + .setInMemoryServerConfigsCacheSize(42) + .build() + assertTrue(quicOptions.hasInMemoryServerConfigsCacheSize()) + assertThat(quicOptions.inMemoryServerConfigsCacheSize) + .isEqualTo(42) + } + + @Test + fun testQuicOptions_handshakeUserAgent_returnsSetValue() { + val userAgent = "test" + val quicOptions = QuicOptions.Builder() + .setHandshakeUserAgent(userAgent) + .build() + assertThat(quicOptions.handshakeUserAgent) + .isEqualTo(userAgent) + } +} diff --git a/android/tests/cts/src/android/net/http/cts/UrlRequestTest.java b/android/tests/cts/src/android/net/http/cts/UrlRequestTest.java new file mode 100644 index 000000000..3c4d134da --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/UrlRequestTest.java @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2019 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.http.cts; + +import static android.net.http.cts.util.TestUtilsKt.assertOKStatusCode; +import static android.net.http.cts.util.TestUtilsKt.skipIfNoInternetConnection; + +import static com.google.common.truth.Truth.assertThat; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; + +import android.content.Context; +import android.net.http.HeaderBlock; +import android.net.http.HttpEngine; +import android.net.http.HttpException; +import android.net.http.InlineExecutionProhibitedException; +import android.net.http.UploadDataProvider; +import android.net.http.UrlRequest; +import android.net.http.UrlRequest.Status; +import android.net.http.UrlResponseInfo; +import android.net.http.cts.util.HttpCtsTestServer; +import android.net.http.cts.util.TestStatusListener; +import android.net.http.cts.util.TestUrlRequestCallback; +import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep; +import android.net.http.cts.util.UploadDataProviders; +import android.os.Build; +import android.webkit.cts.CtsTestServer; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.test.core.app.ApplicationProvider; + +import com.android.testutils.DevSdkIgnoreRule; +import com.android.testutils.DevSdkIgnoreRunner; + +import com.google.common.base.Strings; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.net.URLEncoder; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +@RunWith(DevSdkIgnoreRunner.class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +public class UrlRequestTest { + private static final Executor DIRECT_EXECUTOR = Runnable::run; + + private TestUrlRequestCallback mCallback; + private HttpCtsTestServer mTestServer; + private HttpEngine mHttpEngine; + + @Before + public void setUp() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + skipIfNoInternetConnection(context); + HttpEngine.Builder builder = new HttpEngine.Builder(context); + mHttpEngine = builder.build(); + mCallback = new TestUrlRequestCallback(); + mTestServer = new HttpCtsTestServer(context); + } + + @After + public void tearDown() throws Exception { + if (mHttpEngine != null) { + mHttpEngine.shutdown(); + } + if (mTestServer != null) { + mTestServer.shutdown(); + } + } + + private UrlRequest.Builder createUrlRequestBuilder(String url) { + return mHttpEngine.newUrlRequestBuilder(url, mCallback.getExecutor(), mCallback); + } + + @Test + public void testUrlRequestGet_CompletesSuccessfully() throws Exception { + String url = mTestServer.getSuccessUrl(); + UrlRequest request = createUrlRequestBuilder(url).build(); + request.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + assertThat("Received byte count must be > 0", info.getReceivedByteCount(), greaterThan(0L)); + } + + @Test + public void testUrlRequestStatus_InvalidBeforeRequestStarts() throws Exception { + UrlRequest request = createUrlRequestBuilder(mTestServer.getSuccessUrl()).build(); + // Calling before request is started should give Status.INVALID, + // since the native adapter is not created. + TestStatusListener statusListener = new TestStatusListener(); + request.getStatus(statusListener); + statusListener.expectStatus(Status.INVALID); + } + + @Test + public void testUrlRequestCancel_CancelCalled() throws Exception { + UrlRequest request = createUrlRequestBuilder(mTestServer.getSuccessUrl()).build(); + mCallback.setAutoAdvance(false); + + request.start(); + mCallback.waitForNextStep(); + assertSame(mCallback.mResponseStep, ResponseStep.ON_RESPONSE_STARTED); + + request.cancel(); + mCallback.expectCallback(ResponseStep.ON_CANCELED); + } + + @Test + public void testUrlRequestPost_EchoRequestBody() { + String testData = "test"; + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getEchoBodyUrl()); + + UploadDataProvider dataProvider = UploadDataProviders.create(testData); + builder.setUploadDataProvider(dataProvider, mCallback.getExecutor()); + builder.addHeader("Content-Type", "text/html"); + builder.build().start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + + assertOKStatusCode(mCallback.mResponseInfo); + assertEquals(testData, mCallback.mResponseAsString); + } + + @Test + public void testUrlRequestFail_FailedCalled() { + createUrlRequestBuilder("http://0.0.0.0:0/").build().start(); + mCallback.expectCallback(ResponseStep.ON_FAILED); + } + + @Test + public void testUrlRequest_directExecutor_allowed() throws InterruptedException { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + callback.setAllowDirectExecutor(true); + UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder( + mTestServer.getEchoBodyUrl(), DIRECT_EXECUTOR, callback); + UploadDataProvider dataProvider = UploadDataProviders.create("test"); + builder.setUploadDataProvider(dataProvider, DIRECT_EXECUTOR); + builder.addHeader("Content-Type", "text/plain;charset=UTF-8"); + builder.setDirectExecutorAllowed(true); + builder.build().start(); + callback.blockForDone(); + + if (callback.mOnErrorCalled) { + throw new AssertionError("Expected no exception", callback.mError); + } + + assertEquals(200, callback.mResponseInfo.getHttpStatusCode()); + assertEquals("test", callback.mResponseAsString); + } + + @Test + public void testUrlRequest_directExecutor_disallowed_uploadDataProvider() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + // This applies just locally to the test callback, not to SUT + callback.setAllowDirectExecutor(true); + + UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder( + mTestServer.getEchoBodyUrl(), Executors.newSingleThreadExecutor(), callback); + UploadDataProvider dataProvider = UploadDataProviders.create("test"); + + builder.setUploadDataProvider(dataProvider, DIRECT_EXECUTOR) + .addHeader("Content-Type", "text/plain;charset=UTF-8") + .build() + .start(); + callback.blockForDone(); + + assertTrue(callback.mOnErrorCalled); + assertTrue(callback.mError.getCause() instanceof InlineExecutionProhibitedException); + } + + @Test + public void testUrlRequest_directExecutor_disallowed_responseCallback() throws Exception { + TestUrlRequestCallback callback = new TestUrlRequestCallback(); + // This applies just locally to the test callback, not to SUT + callback.setAllowDirectExecutor(true); + + UrlRequest.Builder builder = mHttpEngine.newUrlRequestBuilder( + mTestServer.getEchoBodyUrl(), DIRECT_EXECUTOR, callback); + UploadDataProvider dataProvider = UploadDataProviders.create("test"); + + builder.setUploadDataProvider(dataProvider, Executors.newSingleThreadExecutor()) + .addHeader("Content-Type", "text/plain;charset=UTF-8") + .build() + .start(); + callback.blockForDone(); + + assertTrue(callback.mOnErrorCalled); + assertTrue(callback.mError.getCause() instanceof InlineExecutionProhibitedException); + } + + @Test + public void testUrlRequest_nonDirectByteBuffer() throws Exception { + BlockingQueue<HttpException> onFailedException = new ArrayBlockingQueue<>(1); + + UrlRequest request = + mHttpEngine + .newUrlRequestBuilder( + mTestServer.getSuccessUrl(), + Executors.newSingleThreadExecutor(), + new StubUrlRequestCallback() { + @Override + public void onResponseStarted( + UrlRequest request, UrlResponseInfo info) { + // note: allocate, not allocateDirect + request.read(ByteBuffer.allocate(1024)); + } + + @Override + public void onFailed( + UrlRequest request, + UrlResponseInfo info, + HttpException error) { + onFailedException.add(error); + } + }) + .build(); + request.start(); + + HttpException e = onFailedException.poll(5, TimeUnit.SECONDS); + assertNotNull(e); + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertTrue(e.getCause().getMessage().contains("direct")); + } + + @Test + public void testUrlRequest_fullByteBuffer() throws Exception { + BlockingQueue<HttpException> onFailedException = new ArrayBlockingQueue<>(1); + + UrlRequest request = + mHttpEngine + .newUrlRequestBuilder( + mTestServer.getSuccessUrl(), + Executors.newSingleThreadExecutor(), + new StubUrlRequestCallback() { + @Override + public void onResponseStarted( + UrlRequest request, UrlResponseInfo info) { + ByteBuffer bb = ByteBuffer.allocateDirect(1024); + bb.position(bb.limit()); + request.read(bb); + } + + @Override + public void onFailed( + UrlRequest request, + UrlResponseInfo info, + HttpException error) { + onFailedException.add(error); + } + }) + .build(); + request.start(); + + HttpException e = onFailedException.poll(5, TimeUnit.SECONDS); + assertNotNull(e); + assertTrue(e.getCause() instanceof IllegalArgumentException); + assertTrue(e.getCause().getMessage().contains("full")); + } + + @Test + public void testUrlRequest_redirects() throws Exception { + int expectedNumRedirects = 5; + String url = + mTestServer.getRedirectingAssetUrl("html/hello_world.html", expectedNumRedirects); + + UrlRequest request = createUrlRequestBuilder(url).build(); + request.start(); + + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + UrlResponseInfo info = mCallback.mResponseInfo; + assertOKStatusCode(info); + assertThat(mCallback.mResponseAsString).contains("hello world"); + assertThat(info.getUrlChain()).hasSize(expectedNumRedirects + 1); + assertThat(info.getUrlChain().get(0)).isEqualTo(url); + assertThat(info.getUrlChain().get(expectedNumRedirects)).isEqualTo(info.getUrl()); + } + + @Test + public void testUrlRequestPost_withRedirect() throws Exception { + String body = Strings.repeat( + "Hello, this is a really interesting body, so write this 100 times.", 100); + + String redirectUrlParameter = + URLEncoder.encode(mTestServer.getEchoBodyUrl(), "UTF-8"); + createUrlRequestBuilder( + String.format( + "%s/alt_redirect?dest=%s&statusCode=307", + mTestServer.getBaseUri(), + redirectUrlParameter)) + .setHttpMethod("POST") + .addHeader("Content-Type", "text/plain") + .setUploadDataProvider( + UploadDataProviders.create(body.getBytes(StandardCharsets.UTF_8)), + mCallback.getExecutor()) + .build() + .start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + + assertOKStatusCode(mCallback.mResponseInfo); + assertThat(mCallback.mResponseAsString).isEqualTo(body); + } + + @Test + public void testUrlRequest_customHeaders() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getEchoHeadersUrl()); + + List<Map.Entry<String, String>> expectedHeaders = Arrays.asList( + Map.entry("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="), + Map.entry("Max-Forwards", "10"), + Map.entry("X-Client-Data", "random custom header content")); + + for (Map.Entry<String, String> header : expectedHeaders) { + builder.addHeader(header.getKey(), header.getValue()); + } + + builder.build().start(); + mCallback.expectCallback(ResponseStep.ON_SUCCEEDED); + + assertOKStatusCode(mCallback.mResponseInfo); + + List<Map.Entry<String, String>> echoedHeaders = + extractEchoedHeaders(mCallback.mResponseInfo.getHeaders()); + + // The implementation might decide to add more headers like accepted encodings it handles + // internally so the server is likely to see more headers than explicitly set + // by the developer. + assertThat(echoedHeaders) + .containsAtLeastElementsIn(expectedHeaders); + } + + @Test + public void testUrlRequest_getHttpMethod() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final String method = "POST"; + + builder.setHttpMethod(method); + UrlRequest request = builder.build(); + assertThat(request.getHttpMethod()).isEqualTo(method); + } + + @Test + public void testUrlRequest_getHeaders_asList() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final List<Map.Entry<String, String>> expectedHeaders = Arrays.asList( + Map.entry("Authorization", "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="), + Map.entry("Max-Forwards", "10"), + Map.entry("X-Client-Data", "random custom header content")); + + for (Map.Entry<String, String> header : expectedHeaders) { + builder.addHeader(header.getKey(), header.getValue()); + } + + UrlRequest request = builder.build(); + assertThat(request.getHeaders().getAsList()).containsAtLeastElementsIn(expectedHeaders); + } + + @Test + public void testUrlRequest_getHeaders_asMap() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final Map<String, List<String>> expectedHeaders = Map.of( + "Authorization", Arrays.asList("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="), + "Max-Forwards", Arrays.asList("10"), + "X-Client-Data", Arrays.asList("random custom header content")); + + for (Map.Entry<String, List<String>> header : expectedHeaders.entrySet()) { + builder.addHeader(header.getKey(), header.getValue().get(0)); + } + + UrlRequest request = builder.build(); + assertThat(request.getHeaders().getAsMap()).containsAtLeastEntriesIn(expectedHeaders); + } + + @Test + public void testUrlRequest_isCacheDisabled() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final boolean isCacheDisabled = true; + + builder.setCacheDisabled(isCacheDisabled); + UrlRequest request = builder.build(); + assertThat(request.isCacheDisabled()).isEqualTo(isCacheDisabled); + } + + @Test + public void testUrlRequest_isDirectExecutorAllowed() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final boolean isDirectExecutorAllowed = true; + + builder.setDirectExecutorAllowed(isDirectExecutorAllowed); + UrlRequest request = builder.build(); + assertThat(request.isDirectExecutorAllowed()).isEqualTo(isDirectExecutorAllowed); + } + + @Test + public void testUrlRequest_getPriority() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final int priority = UrlRequest.REQUEST_PRIORITY_LOW; + + builder.setPriority(priority); + UrlRequest request = builder.build(); + assertThat(request.getPriority()).isEqualTo(priority); + } + + @Test + public void testUrlRequest_hasTrafficStatsTag() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + + builder.setTrafficStatsTag(10); + UrlRequest request = builder.build(); + assertThat(request.hasTrafficStatsTag()).isEqualTo(true); + } + + @Test + public void testUrlRequest_getTrafficStatsTag() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final int trafficStatsTag = 10; + + builder.setTrafficStatsTag(trafficStatsTag); + UrlRequest request = builder.build(); + assertThat(request.getTrafficStatsTag()).isEqualTo(trafficStatsTag); + } + + @Test + public void testUrlRequest_hasTrafficStatsUid() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + + builder.setTrafficStatsUid(10); + UrlRequest request = builder.build(); + assertThat(request.hasTrafficStatsUid()).isEqualTo(true); + } + + @Test + public void testUrlRequest_getTrafficStatsUid() throws Exception { + UrlRequest.Builder builder = createUrlRequestBuilder(mTestServer.getSuccessUrl()); + final int trafficStatsUid = 10; + + builder.setTrafficStatsUid(trafficStatsUid); + UrlRequest request = builder.build(); + assertThat(request.getTrafficStatsUid()).isEqualTo(trafficStatsUid); + } + + private static List<Map.Entry<String, String>> extractEchoedHeaders(HeaderBlock headers) { + return headers.getAsList() + .stream() + .flatMap(input -> { + if (input.getKey().startsWith(CtsTestServer.ECHOED_RESPONSE_HEADER_PREFIX)) { + String strippedKey = + input.getKey().substring( + CtsTestServer.ECHOED_RESPONSE_HEADER_PREFIX.length()); + return Stream.of(Map.entry(strippedKey, input.getValue())); + } else { + return Stream.empty(); + } + }) + .collect(Collectors.toList()); + } + + private static class StubUrlRequestCallback implements UrlRequest.Callback { + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + throw new UnsupportedOperationException(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + throw new UnsupportedOperationException(); + } + + @Override + public void onReadCompleted( + UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + throw new UnsupportedOperationException(); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + throw new UnsupportedOperationException(); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, HttpException error) { + throw new UnsupportedOperationException(error); + } + + @Override + public void onCanceled(@NonNull UrlRequest request, @Nullable UrlResponseInfo info) { + throw new UnsupportedOperationException(); + } + } +} diff --git a/android/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt b/android/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt new file mode 100644 index 000000000..f1b57c649 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/UrlResponseInfoTest.kt @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2023 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.http.cts + +import android.content.Context +import android.net.http.HttpEngine +import android.net.http.cts.util.HttpCtsTestServer +import android.net.http.cts.util.TestUrlRequestCallback +import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import com.android.testutils.DevSdkIgnoreRule +import com.android.testutils.DevSdkIgnoreRunner +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.junit.runner.RunWith + +@RunWith(DevSdkIgnoreRunner::class) +@DevSdkIgnoreRule.IgnoreUpTo(Build.VERSION_CODES.R) +class UrlResponseInfoTest { + + @Test + fun testUrlResponseInfo_apisReturnCorrectInfo() { + // start the engine and send a request + val context: Context = ApplicationProvider.getApplicationContext() + val server = HttpCtsTestServer(context) + val httpEngine = HttpEngine.Builder(context).build() + val callback = TestUrlRequestCallback() + val url = server.successUrl + val request = httpEngine.newUrlRequestBuilder(url, callback.executor, callback).build() + + request.start() + callback.expectCallback(ResponseStep.ON_SUCCEEDED) + + val info = callback.mResponseInfo + assertFalse(info.headers.asList.isEmpty()) + assertEquals(200, info.httpStatusCode) + assertTrue(info.receivedByteCount > 0) + assertEquals(url, info.url) + assertEquals(listOf(url), info.urlChain) + assertFalse(info.wasCached()) + + // TODO Current test server does not set these values. Uncomment when we use one that does. + // assertEquals("OK", info.httpStatusText) + // assertEquals("http/1.1", info.negotiatedProtocol) + + // cronet defaults to port 0 when no proxy is specified. + // This is not a behaviour we want to enforce since null is reasonable too. + // assertEquals(":0", info.proxyServer) + + server.shutdown() + httpEngine.shutdown() + } +} diff --git a/android/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt b/android/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt new file mode 100644 index 000000000..51965445f --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/util/HttpCtsTestServer.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2022 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.http.cts.util + +import android.content.Context +import android.webkit.cts.CtsTestServer +import java.net.URI +import org.apache.http.HttpEntityEnclosingRequest +import org.apache.http.HttpRequest +import org.apache.http.HttpResponse +import org.apache.http.HttpStatus +import org.apache.http.HttpVersion +import org.apache.http.message.BasicHttpResponse + +private const val ECHO_BODY_PATH = "/echo_body" + +/** Extends CtsTestServer to handle POST requests and other test specific requests */ +class HttpCtsTestServer(context: Context) : CtsTestServer(context) { + + val echoBodyUrl: String = baseUri + ECHO_BODY_PATH + val successUrl: String = getAssetUrl("html/hello_world.html") + + override fun onPost(req: HttpRequest): HttpResponse? { + val path = URI.create(req.requestLine.uri).path + var response: HttpResponse? = null + + if (path.startsWith(ECHO_BODY_PATH)) { + if (req !is HttpEntityEnclosingRequest) { + return BasicHttpResponse( + HttpVersion.HTTP_1_0, + HttpStatus.SC_INTERNAL_SERVER_ERROR, + "Expected req to be of type HttpEntityEnclosingRequest but got ${req.javaClass}" + ) + } + + response = BasicHttpResponse(HttpVersion.HTTP_1_0, HttpStatus.SC_OK, null) + response.entity = req.entity + response.addHeader("Content-Length", req.entity.contentLength.toString()) + } + + return response + } +} diff --git a/android/tests/cts/src/android/net/http/cts/util/TestBidirectionalStreamCallback.java b/android/tests/cts/src/android/net/http/cts/util/TestBidirectionalStreamCallback.java new file mode 100644 index 000000000..1e7333c75 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/util/TestBidirectionalStreamCallback.java @@ -0,0 +1,485 @@ +/* + * Copyright (C) 2023 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.http.cts.util; + +import static org.hamcrest.Matchers.equalTo; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeThat; +import static org.junit.Assume.assumeTrue; + +import android.net.http.BidirectionalStream; +import android.net.http.HeaderBlock; +import android.net.http.HttpException; +import android.net.http.UrlResponseInfo; +import android.os.ConditionVariable; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; + +/** + * Callback that tracks information from different callbacks and has a method to block thread until + * the stream completes on another thread. Allows to cancel, block stream or throw an exception from + * an arbitrary step. + */ +public class TestBidirectionalStreamCallback implements BidirectionalStream.Callback { + private static final int TIMEOUT_MS = 12_000; + public UrlResponseInfo mResponseInfo; + public HttpException mError; + + public ResponseStep mResponseStep = ResponseStep.NOTHING; + + public boolean mOnErrorCalled; + public boolean mOnCanceledCalled; + + public int mHttpResponseDataLength; + public String mResponseAsString = ""; + + public HeaderBlock mTrailers; + + private static final int READ_BUFFER_SIZE = 32 * 1024; + + // When false, the consumer is responsible for all calls into the stream + // that advance it. + private boolean mAutoAdvance = true; + + // Conditionally fail on certain steps. + private FailureType mFailureType = FailureType.NONE; + private ResponseStep mFailureStep = ResponseStep.NOTHING; + + // Signals when the stream is done either successfully or not. + private final ConditionVariable mDone = new ConditionVariable(); + + // Signaled on each step when mAutoAdvance is false. + private final ConditionVariable mReadStepBlock = new ConditionVariable(); + private final ConditionVariable mWriteStepBlock = new ConditionVariable(); + + // Executor Service for Cronet callbacks. + private final ExecutorService mExecutorService = + Executors.newSingleThreadExecutor(new ExecutorThreadFactory()); + private Thread mExecutorThread; + + // position() of ByteBuffer prior to read() call. + private int mBufferPositionBeforeRead; + + // Data to write. + private final ArrayList<WriteBuffer> mWriteBuffers = new ArrayList<WriteBuffer>(); + + // Buffers that we yet to receive the corresponding onWriteCompleted callback. + private final ArrayList<WriteBuffer> mWriteBuffersToBeAcked = new ArrayList<WriteBuffer>(); + + // Whether to use a direct executor. + private final boolean mUseDirectExecutor; + private final DirectExecutor mDirectExecutor; + + private class ExecutorThreadFactory implements ThreadFactory { + @Override + public Thread newThread(Runnable r) { + mExecutorThread = new Thread(r); + return mExecutorThread; + } + } + + private static class WriteBuffer { + final ByteBuffer mBuffer; + final boolean mFlush; + + WriteBuffer(ByteBuffer buffer, boolean flush) { + mBuffer = buffer; + mFlush = flush; + } + } + + private static class DirectExecutor implements Executor { + @Override + public void execute(Runnable task) { + task.run(); + } + } + + public enum ResponseStep { + NOTHING, + ON_STREAM_READY, + ON_RESPONSE_STARTED, + ON_READ_COMPLETED, + ON_WRITE_COMPLETED, + ON_TRAILERS, + ON_CANCELED, + ON_FAILED, + ON_SUCCEEDED, + } + + public enum FailureType { + NONE, + CANCEL_SYNC, + CANCEL_ASYNC, + // Same as above, but continues to advance the stream after posting + // the cancellation task. + CANCEL_ASYNC_WITHOUT_PAUSE, + THROW_SYNC + } + + private boolean isTerminalCallback(ResponseStep step) { + switch (step) { + case ON_SUCCEEDED: + case ON_CANCELED: + case ON_FAILED: + return true; + default: + return false; + } + } + + public TestBidirectionalStreamCallback() { + mUseDirectExecutor = false; + mDirectExecutor = null; + } + + public TestBidirectionalStreamCallback(boolean useDirectExecutor) { + mUseDirectExecutor = useDirectExecutor; + mDirectExecutor = new DirectExecutor(); + } + + public void setAutoAdvance(boolean autoAdvance) { + mAutoAdvance = autoAdvance; + } + + public void setFailure(FailureType failureType, ResponseStep failureStep) { + mFailureStep = failureStep; + mFailureType = failureType; + } + + public boolean blockForDone() { + return mDone.block(TIMEOUT_MS); + } + + /** + * Waits for a terminal callback to complete execution before failing if the callback is not the + * expected one + * + * @param expectedStep the expected callback step + */ + public void expectCallback(ResponseStep expectedStep) { + if (isTerminalCallback(expectedStep)) { + assertTrue(String.format( + "Request timed out. Expected %s callback. Current callback is %s", + expectedStep, mResponseStep), + blockForDone()); + } + assertSame(expectedStep, mResponseStep); + } + + /** + * Waits for a terminal callback to complete execution before skipping the test if the callback + * is not the expected one + * + * @param expectedStep the expected callback step + */ + public void assumeCallback(ResponseStep expectedStep) { + if (isTerminalCallback(expectedStep)) { + assumeTrue( + String.format( + "Request timed out. Expected %s callback. Current callback is %s", + expectedStep, mResponseStep), + blockForDone()); + } + assumeThat(expectedStep, equalTo(mResponseStep)); + } + + public void waitForNextReadStep() { + mReadStepBlock.block(); + mReadStepBlock.close(); + } + + public void waitForNextWriteStep() { + mWriteStepBlock.block(); + mWriteStepBlock.close(); + } + + public Executor getExecutor() { + if (mUseDirectExecutor) { + return mDirectExecutor; + } + return mExecutorService; + } + + public void shutdownExecutor() { + if (mUseDirectExecutor) { + throw new UnsupportedOperationException("DirectExecutor doesn't support shutdown"); + } + mExecutorService.shutdown(); + } + + public void addWriteData(byte[] data) { + addWriteData(data, true); + } + + public void addWriteData(byte[] data, boolean flush) { + ByteBuffer writeBuffer = ByteBuffer.allocateDirect(data.length); + writeBuffer.put(data); + writeBuffer.flip(); + mWriteBuffers.add(new WriteBuffer(writeBuffer, flush)); + mWriteBuffersToBeAcked.add(new WriteBuffer(writeBuffer, flush)); + } + + @Override + public void onStreamReady(BidirectionalStream stream) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertEquals(ResponseStep.NOTHING, mResponseStep); + assertNull(mError); + mResponseStep = ResponseStep.ON_STREAM_READY; + if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) { + return; + } + startNextWrite(stream); + } + + @Override + public void onResponseHeadersReceived(BidirectionalStream stream, UrlResponseInfo info) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertTrue( + mResponseStep == ResponseStep.NOTHING + || mResponseStep == ResponseStep.ON_STREAM_READY + || mResponseStep == ResponseStep.ON_WRITE_COMPLETED); + assertNull(mError); + + mResponseStep = ResponseStep.ON_RESPONSE_STARTED; + mResponseInfo = info; + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + startNextRead(stream); + } + + @Override + public void onReadCompleted( + BidirectionalStream stream, + UrlResponseInfo info, + ByteBuffer byteBuffer, + boolean endOfStream) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertTrue( + mResponseStep == ResponseStep.ON_RESPONSE_STARTED + || mResponseStep == ResponseStep.ON_READ_COMPLETED + || mResponseStep == ResponseStep.ON_WRITE_COMPLETED + || mResponseStep == ResponseStep.ON_TRAILERS); + assertNull(mError); + + mResponseStep = ResponseStep.ON_READ_COMPLETED; + mResponseInfo = info; + + final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead; + mHttpResponseDataLength += bytesRead; + final byte[] lastDataReceivedAsBytes = new byte[bytesRead]; + // Rewind byteBuffer.position() to pre-read() position. + byteBuffer.position(mBufferPositionBeforeRead); + // This restores byteBuffer.position() to its value on entrance to + // this function. + byteBuffer.get(lastDataReceivedAsBytes); + + mResponseAsString += new String(lastDataReceivedAsBytes); + + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + // Do not read if EOF has been reached. + if (!endOfStream) { + startNextRead(stream); + } + } + + @Override + public void onWriteCompleted( + BidirectionalStream stream, + UrlResponseInfo info, + ByteBuffer buffer, + boolean endOfStream) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertNull(mError); + mResponseStep = ResponseStep.ON_WRITE_COMPLETED; + mResponseInfo = info; + if (!mWriteBuffersToBeAcked.isEmpty()) { + assertEquals(buffer, mWriteBuffersToBeAcked.get(0).mBuffer); + mWriteBuffersToBeAcked.remove(0); + } + if (maybeThrowCancelOrPause(stream, mWriteStepBlock)) { + return; + } + startNextWrite(stream); + } + + @Override + public void onResponseTrailersReceived( + BidirectionalStream stream, + UrlResponseInfo info, + HeaderBlock trailers) { + checkOnValidThread(); + assertFalse(stream.isDone()); + assertNull(mError); + mResponseStep = ResponseStep.ON_TRAILERS; + mResponseInfo = info; + mTrailers = trailers; + if (maybeThrowCancelOrPause(stream, mReadStepBlock)) { + return; + } + } + + @Override + public void onSucceeded(BidirectionalStream stream, UrlResponseInfo info) { + checkOnValidThread(); + assertTrue(stream.isDone()); + assertTrue( + mResponseStep == ResponseStep.ON_RESPONSE_STARTED + || mResponseStep == ResponseStep.ON_READ_COMPLETED + || mResponseStep == ResponseStep.ON_WRITE_COMPLETED + || mResponseStep == ResponseStep.ON_TRAILERS); + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + assertEquals(0, mWriteBuffers.size()); + assertEquals(0, mWriteBuffersToBeAcked.size()); + + mResponseStep = ResponseStep.ON_SUCCEEDED; + mResponseInfo = info; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + @Override + public void onFailed(BidirectionalStream stream, UrlResponseInfo info, HttpException error) { + checkOnValidThread(); + assertTrue(stream.isDone()); + // Shouldn't happen after success. + assertTrue(mResponseStep != ResponseStep.ON_SUCCEEDED); + // Should happen at most once for a single stream. + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + mResponseStep = ResponseStep.ON_FAILED; + mResponseInfo = info; + + mOnErrorCalled = true; + mError = error; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + @Override + public void onCanceled(BidirectionalStream stream, UrlResponseInfo info) { + checkOnValidThread(); + assertTrue(stream.isDone()); + // Should happen at most once for a single stream. + assertFalse(mOnCanceledCalled); + assertFalse(mOnErrorCalled); + assertNull(mError); + mResponseStep = ResponseStep.ON_CANCELED; + mResponseInfo = info; + + mOnCanceledCalled = true; + openDone(); + maybeThrowCancelOrPause(stream, mReadStepBlock); + } + + public void startNextRead(BidirectionalStream stream) { + startNextRead(stream, ByteBuffer.allocateDirect(READ_BUFFER_SIZE)); + } + + public void startNextRead(BidirectionalStream stream, ByteBuffer buffer) { + mBufferPositionBeforeRead = buffer.position(); + stream.read(buffer); + } + + public void startNextWrite(BidirectionalStream stream) { + if (!mWriteBuffers.isEmpty()) { + Iterator<WriteBuffer> iterator = mWriteBuffers.iterator(); + while (iterator.hasNext()) { + WriteBuffer b = iterator.next(); + stream.write(b.mBuffer, !iterator.hasNext()); + iterator.remove(); + if (b.mFlush) { + stream.flush(); + break; + } + } + } + } + + public boolean isDone() { + // It's not mentioned by the Android docs, but block(0) seems to block + // indefinitely, so have to block for one millisecond to get state + // without blocking. + return mDone.block(1); + } + + /** Returns the number of pending Writes. */ + public int numPendingWrites() { + return mWriteBuffers.size(); + } + + protected void openDone() { + mDone.open(); + } + + /** Returns {@code false} if the callback should continue to advance the stream. */ + private boolean maybeThrowCancelOrPause( + final BidirectionalStream stream, ConditionVariable stepBlock) { + if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) { + if (!mAutoAdvance) { + stepBlock.open(); + return true; + } + return false; + } + + if (mFailureType == FailureType.THROW_SYNC) { + throw new IllegalStateException("Callback Exception."); + } + Runnable task = + new Runnable() { + @Override + public void run() { + stream.cancel(); + } + }; + if (mFailureType == FailureType.CANCEL_ASYNC + || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) { + getExecutor().execute(task); + } else { + task.run(); + } + return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE; + } + + /** Checks whether callback methods are invoked on the correct thread. */ + private void checkOnValidThread() { + if (!mUseDirectExecutor) { + assertEquals(mExecutorThread, Thread.currentThread()); + } + } +} diff --git a/android/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt b/android/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt new file mode 100644 index 000000000..3a4486fc4 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/util/TestStatusListener.kt @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 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.http.cts.util + +import android.net.http.UrlRequest.StatusListener +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit +import org.junit.Assert.assertSame + +private const val TIMEOUT_MS = 12000L + +/** Test status listener for requests */ +class TestStatusListener : StatusListener { + private val statusFuture = CompletableFuture<Int>() + + override fun onStatus(status: Int) { + statusFuture.complete(status) + } + + /** Fails if the expected status is not the returned status */ + fun expectStatus(expected: Int) { + assertSame(expected, statusFuture.get(TIMEOUT_MS, TimeUnit.MILLISECONDS)) + } +} diff --git a/android/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java b/android/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java new file mode 100644 index 000000000..28443b787 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/util/TestUrlRequestCallback.java @@ -0,0 +1,481 @@ +/* + * Copyright (C) 2022 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.http.cts.util; + +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.core.AnyOf.anyOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertSame; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeThat; +import static org.junit.Assume.assumeTrue; + +import android.net.http.CallbackException; +import android.net.http.HttpException; +import android.net.http.InlineExecutionProhibitedException; +import android.net.http.UrlRequest; +import android.net.http.UrlResponseInfo; +import android.os.ConditionVariable; +import android.os.StrictMode; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.TimeUnit; + +/** + * Callback that tracks information from different callbacks and has a + * method to block thread until the request completes on another thread. + * Allows us to cancel, block request or throw an exception from an arbitrary step. + */ +public class TestUrlRequestCallback implements UrlRequest.Callback { + private static final int TIMEOUT_MS = 12_000; + public ArrayList<UrlResponseInfo> mRedirectResponseInfoList = new ArrayList<>(); + public ArrayList<String> mRedirectUrlList = new ArrayList<>(); + public UrlResponseInfo mResponseInfo; + public HttpException mError; + + public ResponseStep mResponseStep = ResponseStep.NOTHING; + + public int mRedirectCount; + public boolean mOnErrorCalled; + public boolean mOnCanceledCalled; + + public int mHttpResponseDataLength; + public String mResponseAsString = ""; + + public int mReadBufferSize = 32 * 1024; + + // When false, the consumer is responsible for all calls into the request + // that advance it. + private boolean mAutoAdvance = true; + // Whether an exception is thrown by maybeThrowCancelOrPause(). + private boolean mCallbackExceptionThrown; + + // Whether to permit calls on the network thread. + private boolean mAllowDirectExecutor; + + // Whether to stop the executor thread after reaching a terminal method. + // Terminal methods are (onSucceeded, onFailed or onCancelled) + private boolean mBlockOnTerminalState; + + // Conditionally fail on certain steps. + private FailureType mFailureType = FailureType.NONE; + private ResponseStep mFailureStep = ResponseStep.NOTHING; + + // Signals when request is done either successfully or not. + private final ConditionVariable mDone = new ConditionVariable(); + + // Hangs the calling thread until a terminal method has started executing. + private final ConditionVariable mWaitForTerminalToStart = new ConditionVariable(); + + // Signaled on each step when mAutoAdvance is false. + private final ConditionVariable mStepBlock = new ConditionVariable(); + + // Executor Service for Http callbacks. + private final ExecutorService mExecutorService; + private Thread mExecutorThread; + + // position() of ByteBuffer prior to read() call. + private int mBufferPositionBeforeRead; + + private static class ExecutorThreadFactory implements ThreadFactory { + @Override + public Thread newThread(final Runnable r) { + return new Thread(new Runnable() { + @Override + public void run() { + StrictMode.ThreadPolicy threadPolicy = StrictMode.getThreadPolicy(); + try { + StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() + .detectNetwork() + .penaltyLog() + .penaltyDeath() + .build()); + r.run(); + } finally { + StrictMode.setThreadPolicy(threadPolicy); + } + } + }); + } + } + + public enum ResponseStep { + NOTHING, + ON_RECEIVED_REDIRECT, + ON_RESPONSE_STARTED, + ON_READ_COMPLETED, + ON_SUCCEEDED, + ON_FAILED, + ON_CANCELED, + } + + public enum FailureType { + NONE, + CANCEL_SYNC, + CANCEL_ASYNC, + // Same as above, but continues to advance the request after posting + // the cancellation task. + CANCEL_ASYNC_WITHOUT_PAUSE, + THROW_SYNC + } + + private static void assertContains(String expectedSubstring, String actualString) { + assertNotNull(actualString); + assertTrue("String [" + actualString + "] doesn't contain substring [" + expectedSubstring + + "]", actualString.contains(expectedSubstring)); + + } + + /** + * Set {@code mExecutorThread}. + */ + private void fillInExecutorThread() { + mExecutorService.execute(new Runnable() { + @Override + public void run() { + mExecutorThread = Thread.currentThread(); + } + }); + } + + private boolean isTerminalCallback(ResponseStep step) { + switch (step) { + case ON_SUCCEEDED: + case ON_CANCELED: + case ON_FAILED: + return true; + default: + return false; + } + } + + /** + * Create a {@link TestUrlRequestCallback} with a new single-threaded executor. + */ + public TestUrlRequestCallback() { + this(Executors.newSingleThreadExecutor(new ExecutorThreadFactory())); + } + + /** + * Create a {@link TestUrlRequestCallback} using a custom single-threaded executor. + */ + public TestUrlRequestCallback(ExecutorService executorService) { + mExecutorService = executorService; + fillInExecutorThread(); + } + + /** + * This blocks the callback executor thread once it has reached a final state callback. + * In order to continue execution, this method must be called again and providing {@code false} + * to continue execution. + * + * @param blockOnTerminalState the state to set for the executor thread + */ + public void setBlockOnTerminalState(boolean blockOnTerminalState) { + mBlockOnTerminalState = blockOnTerminalState; + if (!blockOnTerminalState) { + mDone.open(); + } + } + + public void setAutoAdvance(boolean autoAdvance) { + mAutoAdvance = autoAdvance; + } + + public void setAllowDirectExecutor(boolean allowed) { + mAllowDirectExecutor = allowed; + } + + public void setFailure(FailureType failureType, ResponseStep failureStep) { + mFailureStep = failureStep; + mFailureType = failureType; + } + + /** + * Blocks the calling thread till callback execution is done + * + * @return true if the condition was opened, false if the call returns because of the timeout. + */ + public boolean blockForDone() { + return mDone.block(TIMEOUT_MS); + } + + /** + * Waits for a terminal callback to complete execution before failing if the callback + * is not the expected one + * + * @param expectedStep the expected callback step + */ + public void expectCallback(ResponseStep expectedStep) { + if (isTerminalCallback(expectedStep)) { + assertTrue("Did not receive terminal callback before timeout", blockForDone()); + } + assertSame(expectedStep, mResponseStep); + } + + /** + * Waits for a terminal callback to complete execution before skipping the test if the + * callback is not the expected one + * + * @param expectedStep the expected callback step + */ + public void assumeCallback(ResponseStep expectedStep) { + if (isTerminalCallback(expectedStep)) { + assumeTrue("Did not receive terminal callback before timeout", blockForDone()); + } + assumeThat(expectedStep, equalTo(mResponseStep)); + } + + /** + * Blocks the calling thread until one of the final states has been called. + * This is called before the callback has finished executed. + */ + public void waitForTerminalToStart() { + mWaitForTerminalToStart.block(); + } + + public void waitForNextStep() { + mStepBlock.block(); + mStepBlock.close(); + } + + public ExecutorService getExecutor() { + return mExecutorService; + } + + public void shutdownExecutor() { + mExecutorService.shutdown(); + } + + /** + * Shuts down the ExecutorService and waits until it executes all posted + * tasks. + */ + public void shutdownExecutorAndWait() { + mExecutorService.shutdown(); + try { + // Termination shouldn't take long. Use 1 min which should be more than enough. + mExecutorService.awaitTermination(1, TimeUnit.MINUTES); + } catch (InterruptedException e) { + fail("ExecutorService is interrupted while waiting for termination"); + } + assertTrue(mExecutorService.isTerminated()); + } + + @Override + public void onRedirectReceived( + UrlRequest request, UrlResponseInfo info, String newLocationUrl) { + checkExecutorThread(); + assertFalse(request.isDone()); + assertThat(mResponseStep, anyOf( + equalTo(ResponseStep.NOTHING), + equalTo(ResponseStep.ON_RECEIVED_REDIRECT))); + assertNull(mError); + + mResponseStep = ResponseStep.ON_RECEIVED_REDIRECT; + mRedirectUrlList.add(newLocationUrl); + mRedirectResponseInfoList.add(info); + ++mRedirectCount; + if (maybeThrowCancelOrPause(request)) { + return; + } + request.followRedirect(); + } + + @Override + public void onResponseStarted(UrlRequest request, UrlResponseInfo info) { + checkExecutorThread(); + assertFalse(request.isDone()); + assertThat(mResponseStep, anyOf( + equalTo(ResponseStep.NOTHING), + equalTo(ResponseStep.ON_RECEIVED_REDIRECT))); + assertNull(mError); + + mResponseStep = ResponseStep.ON_RESPONSE_STARTED; + mResponseInfo = info; + if (maybeThrowCancelOrPause(request)) { + return; + } + startNextRead(request); + } + + @Override + public void onReadCompleted(UrlRequest request, UrlResponseInfo info, ByteBuffer byteBuffer) { + checkExecutorThread(); + assertFalse(request.isDone()); + assertThat(mResponseStep, anyOf( + equalTo(ResponseStep.ON_RESPONSE_STARTED), + equalTo(ResponseStep.ON_READ_COMPLETED))); + assertNull(mError); + + mResponseStep = ResponseStep.ON_READ_COMPLETED; + + final byte[] lastDataReceivedAsBytes; + final int bytesRead = byteBuffer.position() - mBufferPositionBeforeRead; + mHttpResponseDataLength += bytesRead; + lastDataReceivedAsBytes = new byte[bytesRead]; + // Rewind |byteBuffer.position()| to pre-read() position. + byteBuffer.position(mBufferPositionBeforeRead); + // This restores |byteBuffer.position()| to its value on entrance to + // this function. + byteBuffer.get(lastDataReceivedAsBytes); + mResponseAsString += new String(lastDataReceivedAsBytes); + + if (maybeThrowCancelOrPause(request)) { + return; + } + startNextRead(request); + } + + @Override + public void onSucceeded(UrlRequest request, UrlResponseInfo info) { + checkExecutorThread(); + assertTrue(request.isDone()); + assertThat(mResponseStep, anyOf( + equalTo(ResponseStep.ON_RESPONSE_STARTED), + equalTo(ResponseStep.ON_READ_COMPLETED))); + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + + mResponseStep = ResponseStep.ON_SUCCEEDED; + mResponseInfo = info; + mWaitForTerminalToStart.open(); + if (mBlockOnTerminalState) mDone.block(); + openDone(); + maybeThrowCancelOrPause(request); + } + + @Override + public void onFailed(UrlRequest request, UrlResponseInfo info, HttpException error) { + // If the failure is because of prohibited direct execution, the test shouldn't fail + // since the request already did. + if (error.getCause() instanceof InlineExecutionProhibitedException) { + mAllowDirectExecutor = true; + } + checkExecutorThread(); + assertTrue(request.isDone()); + // Shouldn't happen after success. + assertNotEquals(ResponseStep.ON_SUCCEEDED, mResponseStep); + // Should happen at most once for a single request. + assertFalse(mOnErrorCalled); + assertFalse(mOnCanceledCalled); + assertNull(mError); + if (mCallbackExceptionThrown) { + assertTrue(error instanceof CallbackException); + assertContains("Exception received from UrlRequest.Callback", error.getMessage()); + assertNotNull(error.getCause()); + assertTrue(error.getCause() instanceof IllegalStateException); + assertContains("Listener Exception.", error.getCause().getMessage()); + } + + mResponseStep = ResponseStep.ON_FAILED; + mOnErrorCalled = true; + mError = error; + mWaitForTerminalToStart.open(); + if (mBlockOnTerminalState) mDone.block(); + openDone(); + maybeThrowCancelOrPause(request); + } + + @Override + public void onCanceled(UrlRequest request, UrlResponseInfo info) { + checkExecutorThread(); + assertTrue(request.isDone()); + // Should happen at most once for a single request. + assertFalse(mOnCanceledCalled); + assertFalse(mOnErrorCalled); + assertNull(mError); + + mResponseStep = ResponseStep.ON_CANCELED; + mOnCanceledCalled = true; + mWaitForTerminalToStart.open(); + if (mBlockOnTerminalState) mDone.block(); + openDone(); + maybeThrowCancelOrPause(request); + } + + public void startNextRead(UrlRequest request) { + startNextRead(request, ByteBuffer.allocateDirect(mReadBufferSize)); + } + + public void startNextRead(UrlRequest request, ByteBuffer buffer) { + mBufferPositionBeforeRead = buffer.position(); + request.read(buffer); + } + + public boolean isDone() { + // It's not mentioned by the Android docs, but block(0) seems to block + // indefinitely, so have to block for one millisecond to get state + // without blocking. + return mDone.block(1); + } + + protected void openDone() { + mDone.open(); + } + + private void checkExecutorThread() { + if (!mAllowDirectExecutor) { + assertEquals(mExecutorThread, Thread.currentThread()); + } + } + + /** + * Returns {@code false} if the listener should continue to advance the + * request. + */ + private boolean maybeThrowCancelOrPause(final UrlRequest request) { + checkExecutorThread(); + if (mResponseStep != mFailureStep || mFailureType == FailureType.NONE) { + if (!mAutoAdvance) { + mStepBlock.open(); + return true; + } + return false; + } + + if (mFailureType == FailureType.THROW_SYNC) { + assertFalse(mCallbackExceptionThrown); + mCallbackExceptionThrown = true; + throw new IllegalStateException("Listener Exception."); + } + Runnable task = new Runnable() { + @Override + public void run() { + request.cancel(); + } + }; + if (mFailureType == FailureType.CANCEL_ASYNC + || mFailureType == FailureType.CANCEL_ASYNC_WITHOUT_PAUSE) { + getExecutor().execute(task); + } else { + task.run(); + } + return mFailureType != FailureType.CANCEL_ASYNC_WITHOUT_PAUSE; + } +} diff --git a/android/tests/cts/src/android/net/http/cts/util/TestUtils.kt b/android/tests/cts/src/android/net/http/cts/util/TestUtils.kt new file mode 100644 index 000000000..7fc005a11 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/util/TestUtils.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2023 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.http.cts.util + +import android.content.Context +import android.net.ConnectivityManager +import android.net.http.UrlResponseInfo +import org.hamcrest.Matchers.equalTo +import org.junit.Assert.assertEquals +import org.junit.Assume.assumeNotNull +import org.junit.Assume.assumeThat + +fun skipIfNoInternetConnection(context: Context) { + val connectivityManager = context.getSystemService(ConnectivityManager::class.java) + assumeNotNull( + "This test requires a working Internet connection", connectivityManager!!.activeNetwork + ) +} + +fun assertOKStatusCode(info: UrlResponseInfo) { + assertEquals("Status code must be 200 OK", 200, info.httpStatusCode) +} + +fun assumeOKStatusCode(info: UrlResponseInfo) { + assumeThat("Status code must be 200 OK", info.httpStatusCode, equalTo(200)) +} diff --git a/android/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java b/android/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java new file mode 100644 index 000000000..3b90fa0f0 --- /dev/null +++ b/android/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2023 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.http.cts.util; + +import android.net.http.UploadDataProvider; +import android.net.http.UploadDataSink; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; + +/** + * Provides implementations of {@link UploadDataProvider} for common use cases. Corresponds to + * {@code android.net.http.apihelpers.UploadDataProviders} which is not an exposed API. + */ +public final class UploadDataProviders { + /** + * Uploads an entire file. + * + * @param file The file to upload + * @return A new UploadDataProvider for the given file + */ + public static UploadDataProvider create(final File file) { + return new FileUploadProvider(() -> new FileInputStream(file).getChannel()); + } + + /** + * Uploads an entire file, closing the descriptor when it is no longer needed. + * + * @param fd The file descriptor to upload + * @throws IllegalArgumentException if {@code fd} is not a file. + * @return A new UploadDataProvider for the given file descriptor + */ + public static UploadDataProvider create(final ParcelFileDescriptor fd) { + return new FileUploadProvider(() -> { + if (fd.getStatSize() != -1) { + return new ParcelFileDescriptor.AutoCloseInputStream(fd).getChannel(); + } else { + fd.close(); + throw new IllegalArgumentException("Not a file: " + fd); + } + }); + } + + /** + * Uploads a ByteBuffer, from the current {@code buffer.position()} to {@code buffer.limit()} + * + * @param buffer The data to upload + * @return A new UploadDataProvider for the given buffer + */ + public static UploadDataProvider create(ByteBuffer buffer) { + return new ByteBufferUploadProvider(buffer.slice()); + } + + /** + * Uploads {@code length} bytes from {@code data}, starting from {@code offset} + * + * @param data Array containing data to upload + * @param offset Offset within data to start with + * @param length Number of bytes to upload + * @return A new UploadDataProvider for the given data + */ + public static UploadDataProvider create(byte[] data, int offset, int length) { + return new ByteBufferUploadProvider(ByteBuffer.wrap(data, offset, length).slice()); + } + + /** + * Uploads the contents of {@code data} + * + * @param data Array containing data to upload + * @return A new UploadDataProvider for the given data + */ + public static UploadDataProvider create(byte[] data) { + return create(data, 0, data.length); + } + + /** + * Uploads the UTF-8 representation of {@code data} + * + * @param data String containing data to upload + * @return A new UploadDataProvider for the given data + */ + public static UploadDataProvider create(String data) { + return create(data.getBytes(StandardCharsets.UTF_8)); + } + + private interface FileChannelProvider { + FileChannel getChannel() throws IOException; + } + + private static final class FileUploadProvider extends UploadDataProvider { + private volatile FileChannel mChannel; + private final FileChannelProvider mProvider; + /** Guards initialization of {@code mChannel} */ + private final Object mLock = new Object(); + + private FileUploadProvider(FileChannelProvider provider) { + this.mProvider = provider; + } + + @Override + public long getLength() throws IOException { + return getChannel().size(); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) throws IOException { + if (!byteBuffer.hasRemaining()) { + throw new IllegalStateException("Cronet passed a buffer with no bytes remaining"); + } + FileChannel channel = getChannel(); + int bytesRead = 0; + while (bytesRead == 0) { + int read = channel.read(byteBuffer); + if (read == -1) { + break; + } else { + bytesRead += read; + } + } + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) throws IOException { + getChannel().position(0); + uploadDataSink.onRewindSucceeded(); + } + + /** + * Lazily initializes the channel so that a blocking operation isn't performed + * on a non-executor thread. + */ + private FileChannel getChannel() throws IOException { + if (mChannel == null) { + synchronized (mLock) { + if (mChannel == null) { + mChannel = mProvider.getChannel(); + } + } + } + return mChannel; + } + + @Override + public void close() throws IOException { + FileChannel channel = mChannel; + if (channel != null) { + channel.close(); + } + } + } + + private static final class ByteBufferUploadProvider extends UploadDataProvider { + private final ByteBuffer mUploadBuffer; + + private ByteBufferUploadProvider(ByteBuffer uploadBuffer) { + this.mUploadBuffer = uploadBuffer; + } + + @Override + public long getLength() { + return mUploadBuffer.limit(); + } + + @Override + public void read(UploadDataSink uploadDataSink, ByteBuffer byteBuffer) { + if (!byteBuffer.hasRemaining()) { + throw new IllegalStateException("Cronet passed a buffer with no bytes remaining"); + } + if (byteBuffer.remaining() >= mUploadBuffer.remaining()) { + byteBuffer.put(mUploadBuffer); + } else { + int oldLimit = mUploadBuffer.limit(); + mUploadBuffer.limit(mUploadBuffer.position() + byteBuffer.remaining()); + byteBuffer.put(mUploadBuffer); + mUploadBuffer.limit(oldLimit); + } + uploadDataSink.onReadSucceeded(false); + } + + @Override + public void rewind(UploadDataSink uploadDataSink) { + mUploadBuffer.position(0); + uploadDataSink.onRewindSucceeded(); + } + } + + // Prevent instantiation + private UploadDataProviders() {} +} |