Merge "Add more test coverage for UrlRequest and ConnectionMigrationOptions." am: a23a74a232

Original change: https://android-review.googlesource.com/c/platform/packages/modules/Connectivity/+/2472109

Change-Id: I2bfb057a39a8ae273aa8bfe0d291a9ea8089c185
Signed-off-by: Automerger Merge Worker <[email protected]>
diff --git a/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt b/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
index 279bcc8..77cb30e 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
+++ b/Cronet/tests/cts/src/android/net/http/cts/ConnectionMigrationOptionsTest.kt
@@ -18,6 +18,7 @@
 
 import android.net.http.ConnectionMigrationOptions
 import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_ENABLED
+import android.net.http.ConnectionMigrationOptions.MIGRATION_OPTION_UNSPECIFIED
 import androidx.test.ext.junit.runners.AndroidJUnit4
 import kotlin.test.Test
 import kotlin.test.assertEquals
@@ -27,6 +28,16 @@
 class ConnectionMigrationOptionsTest {
 
     @Test
+    fun testConnectionMigrationOptions_defaultValues() {
+        val options =
+                ConnectionMigrationOptions.Builder().build()
+
+        assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.allowNonDefaultNetworkUsageEnabled)
+        assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.defaultNetworkMigrationEnabled)
+        assertEquals(MIGRATION_OPTION_UNSPECIFIED, options.pathDegradationMigrationEnabled)
+    }
+
+    @Test
     fun testConnectionMigrationOptions_enableDefaultNetworkMigration_returnSetValue() {
         val options =
             ConnectionMigrationOptions.Builder()
@@ -45,4 +56,13 @@
 
         assertEquals(MIGRATION_OPTION_ENABLED, options.pathDegradationMigrationEnabled)
     }
+
+    @Test
+    fun testConnectionMigrationOptions_allowNonDefaultNetworkUsage_returnSetValue() {
+        val options =
+                ConnectionMigrationOptions.Builder()
+                        .setAllowNonDefaultNetworkUsageEnabled(MIGRATION_OPTION_ENABLED).build()
+
+        assertEquals(MIGRATION_OPTION_ENABLED, options.allowNonDefaultNetworkUsageEnabled)
+    }
 }
diff --git a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
index 19742e5..a364e29 100644
--- a/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
+++ b/Cronet/tests/cts/src/android/net/http/cts/UrlRequestTest.java
@@ -19,6 +19,8 @@
 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;
@@ -40,15 +42,19 @@
 import android.net.http.cts.util.TestUploadDataProvider;
 import android.net.http.cts.util.TestUrlRequestCallback;
 import android.net.http.cts.util.TestUrlRequestCallback.ResponseStep;
+import android.net.http.cts.util.UploadDataProviders;
 
 import androidx.test.core.app.ApplicationProvider;
 import androidx.test.ext.junit.runners.AndroidJUnit4;
 
+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.concurrent.ArrayBlockingQueue;
@@ -279,6 +285,31 @@
         assertTrue(e.getCause().getMessage().contains("full"));
     }
 
+    @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);
+    }
+
     private static class StubUrlRequestCallback extends UrlRequest.Callback {
 
         @Override
diff --git a/Cronet/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java b/Cronet/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java
new file mode 100644
index 0000000..889f8f2
--- /dev/null
+++ b/Cronet/tests/cts/src/android/net/http/cts/util/UploadDataProviders.java
@@ -0,0 +1,198 @@
+/*
+ * 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;
+
+/**
+ * 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);
+    }
+
+    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() {}
+}