chromium/url/android/javatests/src/org/chromium/url/GURLJavaTest.java

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.url;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;

import androidx.test.filters.SmallTest;

import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import org.chromium.base.test.BaseJUnit4ClassRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.build.BuildConfig;
import org.chromium.content_public.browser.test.NativeLibraryTestUtils;

import java.net.URISyntaxException;

/**
 * Tests for {@link GURL}. GURL relies heavily on the native implementation, and the lion's share of
 * the logic is tested there. This test is primarily to make sure everything is plumbed through
 * correctly.
 */
@RunWith(BaseJUnit4ClassRunner.class)
@Batch(Batch.UNIT_TESTS)
public class GURLJavaTest {
    @Mock GURL.Natives mGURLMocks;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);

        NativeLibraryTestUtils.loadNativeLibraryNoBrowserProcess();
        GURLJavaTestHelper.nativeInitializeICU();
    }

    /* package */ static void deepAssertEquals(GURL expected, GURL actual) {
        Assert.assertEquals(expected, actual);
        Assert.assertEquals(expected.getScheme(), actual.getScheme());
        Assert.assertEquals(expected.getUsername(), actual.getUsername());
        Assert.assertEquals(expected.getPassword(), actual.getPassword());
        Assert.assertEquals(expected.getHost(), actual.getHost());
        Assert.assertEquals(expected.getPort(), actual.getPort());
        Assert.assertEquals(expected.getPath(), actual.getPath());
        Assert.assertEquals(expected.getQuery(), actual.getQuery());
        Assert.assertEquals(expected.getRef(), actual.getRef());
    }

    private String prependLengthToSerialization(String serialization) {
        return Integer.toString(serialization.length()) + GURL.SERIALIZER_DELIMITER + serialization;
    }

    @SmallTest
    @Test
    public void testGURLEquivalence() {
        GURLJavaTestHelper.nativeTestGURLEquivalence();
    }

    // Equivalent of GURLTest.Components
    @SmallTest
    @Test
    @SuppressWarnings(value = "AuthLeak")
    public void testComponents() {
        GURL empty = new GURL("");
        Assert.assertTrue(empty.isEmpty());
        Assert.assertFalse(empty.isValid());

        GURL url = new GURL("http://user:[email protected]:99/foo;bar?q=a#ref");
        Assert.assertFalse(url.isEmpty());
        Assert.assertTrue(url.isValid());
        Assert.assertTrue(url.getScheme().equals("http"));

        Assert.assertEquals("http://user:[email protected]:99/foo;bar?q=a#ref", url.getSpec());

        Assert.assertEquals("http", url.getScheme());
        Assert.assertEquals("user", url.getUsername());
        Assert.assertEquals("pass", url.getPassword());
        Assert.assertEquals("google.com", url.getHost());
        Assert.assertEquals("99", url.getPort());
        Assert.assertEquals("/foo;bar", url.getPath());
        Assert.assertEquals("q=a", url.getQuery());
        Assert.assertEquals("ref", url.getRef());

        // Test parsing userinfo with special characters.
        GURL urlSpecialPass = new GURL("http://user:%40!$&'()*+,;=:@google.com:12345");
        Assert.assertTrue(urlSpecialPass.isValid());
        // GURL canonicalizes some delimiters.
        Assert.assertEquals("%40!$&%27()*+,%3B%3D%3A", urlSpecialPass.getPassword());
        Assert.assertEquals("google.com", urlSpecialPass.getHost());
        Assert.assertEquals("12345", urlSpecialPass.getPort());
    }

    // Equivalent of GURLTest.Empty
    @SmallTest
    @Test
    public void testEmpty() {
        GURLJni.TEST_HOOKS.setInstanceForTesting(mGURLMocks);
        doThrow(new RuntimeException("Should not need to parse empty URL"))
                .when(mGURLMocks)
                .init(any(), any());
        GURL url = new GURL("");
        Assert.assertFalse(url.isValid());
        Assert.assertEquals("", url.getSpec());

        Assert.assertEquals("", url.getScheme());
        Assert.assertEquals("", url.getUsername());
        Assert.assertEquals("", url.getPassword());
        Assert.assertEquals("", url.getHost());
        Assert.assertEquals("", url.getPort());
        Assert.assertEquals("", url.getPath());
        Assert.assertEquals("", url.getQuery());
        Assert.assertEquals("", url.getRef());
        GURLJni.TEST_HOOKS.setInstanceForTesting(null);
    }

    // Test that GURL and URI return the correct Origin.
    @SmallTest
    @Test
    @SuppressWarnings(value = "AuthLeak")
    public void testOrigin() throws URISyntaxException {
        final String kExpectedOrigin1 = "http://google.com:21/";
        final String kExpectedOrigin2 = "";
        GURL url1 = new GURL("filesystem:http://user:[email protected]:21/blah#baz");
        GURL url2 = new GURL("javascript:window.alert(\"hello,world\");");
        URI uri = new URI("filesystem:http://user:[email protected]:21/blah#baz");

        Assert.assertEquals(kExpectedOrigin1, url1.getOrigin().getSpec());
        Assert.assertEquals(kExpectedOrigin2, url2.getOrigin().getSpec());
        URI origin = uri.getOrigin();
        Assert.assertEquals(kExpectedOrigin1, origin.getSpec());
    }

    @SmallTest
    @Test
    public void testWideInput() throws URISyntaxException {
        final String kExpectedSpec = "http://xn--1xa.com/";

        GURL url = new GURL("http://\u03C0.com");
        Assert.assertEquals(kExpectedSpec, url.getSpec());
        Assert.assertEquals("http", url.getScheme());
        Assert.assertEquals("", url.getUsername());
        Assert.assertEquals("", url.getPassword());
        Assert.assertEquals("xn--1xa.com", url.getHost());
        Assert.assertEquals("", url.getPort());
        Assert.assertEquals("/", url.getPath());
        Assert.assertEquals("", url.getQuery());
        Assert.assertEquals("", url.getRef());
    }

    @SmallTest
    @Test
    @SuppressWarnings(value = "AuthLeak")
    public void testSerialization() {
        GURL cases[] = {
            // Common Standard URLs.
            new GURL("https://www.google.com"),
            new GURL("https://www.google.com/"),
            new GURL("https://www.google.com/maps.htm"),
            new GURL("https://www.google.com/maps/"),
            new GURL("https://www.google.com/index.html"),
            new GURL("https://www.google.com/index.html?q=maps"),
            new GURL("https://www.google.com/index.html#maps/"),
            new GURL("https://foo:[email protected]/maps.htm"),
            new GURL("https://www.google.com/maps/au/index.html"),
            new GURL("https://www.google.com/maps/au/north"),
            new GURL("https://www.google.com/maps/au/north/"),
            new GURL("https://www.google.com/maps/au/index.html?q=maps#fragment/"),
            new GURL("http://www.google.com:8000/maps/au/index.html?q=maps#fragment/"),
            new GURL("https://www.google.com/maps/au/north/?q=maps#fragment"),
            new GURL("https://www.google.com/maps/au/north?q=maps#fragment"),
            // Less common standard URLs.
            new GURL("filesystem:http://www.google.com/temporary/bar.html?baz=22"),
            new GURL("file:///temporary/bar.html?baz=22"),
            new GURL("ftp://foo/test/index.html"),
            new GURL("gopher://foo/test/index.html"),
            new GURL("ws://foo/test/index.html"),
            // Non-standard,
            new GURL("chrome://foo/bar.html"),
            new GURL("httpa://foo/test/index.html"),
            new GURL("blob:https://foo.bar/test/index.html"),
            new GURL("about:blank"),
            new GURL("data:foobar"),
            new GURL("scheme:opaque_data"),
            // Invalid URLs.
            new GURL("foobar"),
            // URLs containing the delimiter
            new GURL("https://www.google.ca/" + GURL.SERIALIZER_DELIMITER + ",foo"),
            new GURL("https://www.foo" + GURL.SERIALIZER_DELIMITER + "bar.com"),
        };

        GURLJni.TEST_HOOKS.setInstanceForTesting(mGURLMocks);
        doThrow(
                        new RuntimeException(
                                "Should not re-initialize for deserialization when the "
                                        + "version hasn't changed."))
                .when(mGURLMocks)
                .init(any(), any());
        for (GURL url : cases) {
            GURL out = GURL.deserialize(url.serialize());
            deepAssertEquals(url, out);
        }
        GURLJni.TEST_HOOKS.setInstanceForTesting(null);
    }

    /**
     * Tests that we re-parse the URL from the spec, which must always be the last token in the
     * serialization, if the serialization version differs.
     */
    @SmallTest
    @Test
    public void testSerializationWithVersionSkew() {
        GURL url = new GURL("https://www.google.com");
        String serialization =
                (GURL.SERIALIZER_VERSION + 1)
                        + ",0,0,0,0,foo,https://url.bad,blah,0,"
                                .replace(',', GURL.SERIALIZER_DELIMITER)
                        + url.getSpec();
        serialization = prependLengthToSerialization(serialization);
        GURL out = GURL.deserialize(serialization);
        deepAssertEquals(url, out);
    }

    /** Tests that fields that aren't visible to java code are correctly serialized. */
    @SmallTest
    @Test
    public void testSerializationOfPrivateFields() {
        String serialization =
                GURL.SERIALIZER_VERSION
                        + ",true,"
                        // Outer Parsed.
                        + "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,false,true,"
                        // Inner Parsed.
                        + "17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,true,false,"
                        + "chrome://foo/bar.html";
        serialization = serialization.replace(',', GURL.SERIALIZER_DELIMITER);
        serialization = prependLengthToSerialization(serialization);
        GURL url = GURL.deserialize(serialization);
        Assert.assertEquals(url.serialize(), serialization);
    }

    /** Tests serialized GURL truncated by storage. */
    @SmallTest
    @Test
    public void testTruncatedDeserialization() {
        String serialization = "123,1,true,1,2,3,4,5,6,7,8,9,10";
        serialization = serialization.replace(',', GURL.SERIALIZER_DELIMITER);
        GURL url = GURL.deserialize(serialization);
        Assert.assertEquals(url, GURL.emptyGURL());
    }

    /** Tests serialized GURL truncated by storage. */
    @SmallTest
    @Test
    public void testCorruptedSerializations() {
        String serialization = new GURL("https://www.google.ca").serialize();
        // Replace the scheme length (5) with an extra delimiter.
        String corruptedParsed = serialization.replace('5', GURL.SERIALIZER_DELIMITER);
        GURL url = GURL.deserialize(corruptedParsed);
        Assert.assertEquals(GURL.emptyGURL(), url);

        String corruptedVersion =
                serialization.replaceFirst(Integer.toString(GURL.SERIALIZER_VERSION), "x");
        url = GURL.deserialize(corruptedVersion);
        Assert.assertEquals(GURL.emptyGURL(), url);
    }

    // Test that domainIs is hooked up correctly.
    @SmallTest
    @Test
    public void testDomainIs() {
        GURL url1 = new GURL("https://www.google.com");
        GURL url2 = new GURL("https://www.notgoogle.com");

        Assert.assertTrue(url1.domainIs("com"));
        Assert.assertTrue(url2.domainIs("com"));
        Assert.assertTrue(url1.domainIs("google.com"));
        Assert.assertFalse(url2.domainIs("google.com"));

        Assert.assertTrue(url1.domainIs("www.google.com"));
        Assert.assertFalse(url1.domainIs("images.google.com"));
    }

    // Test that replaceComponents is hooked up correctly.
    @SmallTest
    @Test
    @SuppressWarnings(value = "AuthLeak")
    public void testReplaceComponents() {
        GURL url = new GURL("http://user:[email protected]:99/foo;bar?q=a#ref");

        GURL unchanged = url.replaceComponents(null, false, null, false);
        Assert.assertEquals("user", unchanged.getUsername());
        Assert.assertEquals("pass", unchanged.getPassword());

        GURL cleared = url.replaceComponents(null, true, null, true);
        Assert.assertTrue(cleared.getUsername().isEmpty());
        Assert.assertTrue(cleared.getPassword().isEmpty());

        GURL changed = url.replaceComponents("newusername", false, "newpassword", false);
        Assert.assertEquals("newusername", changed.getUsername());
        Assert.assertEquals("newpassword", changed.getPassword());
    }

    // Tests Mojom conversion.
    @SmallTest
    @Test
    public void testMojomConvertion() {
        // Valid:
        Assert.assertEquals(
                "https://www.google.com/", new GURL("https://www.google.com/").toMojom().url);

        // Null:
        Assert.assertEquals("", new GURL(null).toMojom().url);

        // Empty:
        Assert.assertEquals("", new GURL("").toMojom().url);

        // Invalid:
        Assert.assertEquals("", new GURL(new String(new byte[] {1, 1, 1})).toMojom().url);

        // Too long.
        Assert.assertEquals(
                "",
                new GURL("https://www.google.com/".concat("a".repeat(2 * 1024 * 1024)))
                        .toMojom()
                        .url);
    }

    /**
     * Verifies that GURL can be used in assertEquals() statements and similar assertion statements.
     */
    @SmallTest
    @Test
    public void testSupportsAssertEqualsComparison() {
        Assert.assertEquals(
                "GURLs created from the same spec should be equal",
                new GURL("https://example.test/"),
                new GURL("https://example.test/"));
        Assert.assertNotEquals(
                "GURLs with different paths are not equal",
                new GURL("https://example.test/"),
                new GURL("https://example.test/this/has/a/path.html"));
        Assert.assertNotEquals(
                "GURLs with different domains are not equal",
                new GURL("https://example.test/"),
                new GURL("https://different.test/"));
        Assert.assertNotEquals(
                "GURLs with different schemes are not equal",
                new GURL("https://example.test/"),
                new GURL("http://example.test/"));

        // When an assertion does fail, make sure that the failure message is human readable.
        Throwable exception =
                Assert.assertThrows(
                        AssertionError.class,
                        () -> {
                            Assert.assertEquals(
                                    new GURL("https://example.test/"),
                                    new GURL("https://different.test/"));
                        });
        if (BuildConfig.ENABLE_ASSERTS) {
            String expectedMessage =
                    "expected:<GURL(https://example.test/)> but"
                            + " was:<GURL(https://different.test/)>";
            Assert.assertEquals(
                    "Assertion message was not what we expected.",
                    expectedMessage,
                    exception.getMessage());
        }
    }
}