chromium/chrome/android/junit/src/org/chromium/chrome/browser/omaha/ResponseParserTest.java

// Copyright 2014 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.chrome.browser.omaha;

import android.util.Xml;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.annotation.Config;
import org.xmlpull.v1.XmlSerializer;

import org.chromium.base.test.BaseRobolectricTestRunner;
import org.chromium.base.test.util.Batch;
import org.chromium.base.test.util.Feature;

import java.io.IOException;
import java.io.StringWriter;

/** Unit tests for the Omaha ResponseParser. */
@RunWith(BaseRobolectricTestRunner.class)
@Batch(Batch.UNIT_TESTS)
@Config(manifest = Config.NONE)
public class ResponseParserTest {
    // Note that the Omaha server appends "/" to the end of the URL codebase.
    private static final String STRIPPED_URL =
            "https://play.google.com/store/apps/details?id=com.google.android.apps.chrome";
    private static final String URL = STRIPPED_URL + "/";
    private static final String NEXT_VERSION = "1.2.3.4";

    private static final String APP_STATUS_OK = "ok";
    private static final String APP_STATUS_RESTRICTED = "restricted";
    private static final String APP_STATUS_ERROR = "error-whatever-else";

    private static final String UPDATE_STATUS_OK = "ok";
    private static final String UPDATE_STATUS_NOUPDATE = "noupdate";
    private static final String UPDATE_STATUS_ERROR = "error-osnotsupported";
    private static final String UPDATE_STATUS_WTF = "omgwtfbbq";

    /**
     * Create XML for testing.
     * @param xmlProtocol Version number of the protocol.  Expected to be "3.0" for valid XML.
     * @param elapsedSeconds Number of seconds since server midnight.
     * @param appStatus Status to use for the <app> element.
     * @param addInstall Whether or not to add an install event.
     * @param addPing Whether or not to add a ping event.
     * @param updateStatus Status to use for the <updatecheck> element.
     * @return The completed XML.
     */
    private static String createTestXML(
            String xmlProtocol,
            String elapsedDays,
            String elapsedSeconds,
            String appStatus,
            boolean addInstall,
            boolean addPing,
            String updateStatus,
            String updateUrl) {
        StringWriter writer = new StringWriter();
        try {
            XmlSerializer serializer = Xml.newSerializer();
            serializer.setOutput(writer);
            serializer.startDocument("UTF-8", true);

            // Set up <response ...>
            serializer.startTag(null, "response");
            serializer.attribute(null, "server", "prod");
            if (xmlProtocol != null) {
                serializer.attribute(null, "protocol", xmlProtocol);
            }

            // Create <daystart> element.
            if (elapsedSeconds != null || elapsedDays != null) {
                serializer.startTag(null, "daystart");
                if (elapsedDays != null) {
                    serializer.attribute(null, "elapsed_days", elapsedDays);
                }
                if (elapsedSeconds != null) {
                    serializer.attribute(null, "elapsed_seconds", elapsedSeconds);
                }
                serializer.endTag(null, "daystart");
            }

            // Create <app> element with unused attribute.
            serializer.startTag(null, "app");
            serializer.attribute(null, "appid", "{APP_ID}");
            serializer.attribute(null, "status", appStatus);
            serializer.attribute(null, "unused", "attribute");

            if (addInstall) {
                serializer.startTag(null, "event");
                serializer.attribute(null, "status", "ok");
                serializer.endTag(null, "event");
            }

            if (addPing) {
                serializer.startTag(null, "ping");
                serializer.attribute(null, "status", "ok");
                serializer.endTag(null, "ping");
            }

            if (updateStatus != null) {
                serializer.startTag(null, "updatecheck");
                serializer.attribute(null, "status", updateStatus);
                if (UPDATE_STATUS_OK.equals(updateStatus)) {
                    createUpdateXML(serializer, updateUrl);
                }
                serializer.endTag(null, "updatecheck");
            }
            serializer.endTag(null, "app");

            // Create extraneous tag.
            serializer.startTag(null, "extraneous");
            serializer.attribute(null, "useless", "yes");
            serializer.endTag(null, "extraneous");

            serializer.endTag(null, "response");
            serializer.endDocument();
        } catch (IOException e) {
            Assert.fail("Caught an IOException creating the XML: " + e);
        } catch (IllegalArgumentException e) {
            Assert.fail("Caught an IllegalArgumentException creating the XML: " + e);
        } catch (IllegalStateException e) {
            Assert.fail("Caught an IllegalStateException creating the XML: " + e);
        }

        return writer.toString();
    }

    private static void createUpdateXML(XmlSerializer serializer, String updateUrl)
            throws IOException {
        // End result should look something like:
        // <updatecheck status="ok">
        //  <urls>
        //    <url codebase="URL" />
        //  </urls>
        //  <manifest garbage="attribute" version="NEXT_VERSION">
        //    <packages>
        //      <package hash="0" name="dummy.apk" required="true" size="0" />
        //    </packages>
        //    <actions>
        //      <action event="install" run="dummy.apk" />
        //      <action event="postinstall" />
        //    </actions>
        //  </manifest>
        //  <better be="ignored" />
        // </updatecheck>

        // Create <urls> and its descendants.
        serializer.startTag(null, "urls");
        if (updateUrl != null) {
            serializer.startTag(null, "url");
            serializer.attribute(null, "codebase", updateUrl);
            serializer.endTag(null, "url");
        }
        serializer.endTag(null, "urls");

        // Create <manifest> and its descendants.
        serializer.startTag(null, "manifest");
        serializer.attribute(null, "garbage", "attribute");
        serializer.attribute(null, "version", NEXT_VERSION);

        // Create <packages> and its children.
        serializer.startTag(null, "packages");
        serializer.startTag(null, "package");
        serializer.attribute(null, "hash", "0");
        serializer.attribute(null, "name", "dummy.apk");
        serializer.attribute(null, "required", "true");
        serializer.attribute(null, "size", "0");
        serializer.endTag(null, "package");
        serializer.endTag(null, "packages");

        // Create <actions> and its children.
        serializer.startTag(null, "actions");
        serializer.startTag(null, "action");
        serializer.attribute(null, "event", "install");
        serializer.attribute(null, "run", "dummy.apk");
        serializer.endTag(null, "action");

        serializer.startTag(null, "action");
        serializer.attribute(null, "event", "postinstall");
        serializer.endTag(null, "action");
        serializer.endTag(null, "actions");
        serializer.endTag(null, "manifest");

        // Create a dummy element for testing to make sure it's ignored.
        serializer.startTag(null, "dummy");
        serializer.attribute(null, "hopefully", "ignored");
        serializer.endTag(null, "dummy");
    }

    /**
     * Runs a test that is expected to pass.
     * @param appStatus Status to use for the <app> element.
     * @param addInstall Whether or not to add an install event.
     * @param addPing Whether or not to add a ping.
     * @param updateStatus Status to use for the <updatecheck> element.
     * @throws RequestFailureException Thrown if the test fails.
     */
    private static void runSuccessTest(
            String appStatus, boolean addInstall, boolean addPing, String updateStatus)
            throws RequestFailureException {
        String xml =
                createTestXML(
                        "3.0", "4088", "12345", appStatus, addInstall, addPing, updateStatus, URL);
        ResponseParser parser =
                new ResponseParser(true, "{APP_ID}", addInstall, addPing, updateStatus != null);
        OmahaBase.VersionConfig versionConfig = parser.parseResponse(xml);

        Assert.assertEquals("elapsed_seconds doesn't match.", 12345, parser.getDaystartSeconds());
        Assert.assertEquals("elapsed_days doesn't match.", 4088, parser.getDaystartDays());
        Assert.assertEquals("<app> status doesn't match.", appStatus, parser.getAppStatus());
        Assert.assertEquals(
                "<updatecheck> status doesn't match.", updateStatus, parser.getUpdateStatus());
        if (UPDATE_STATUS_OK.equals(updateStatus)) {
            Assert.assertEquals("Version number doesn't match.", "1.2.3.4", parser.getNewVersion());
            Assert.assertEquals("Market URL doesn't match.", STRIPPED_URL, parser.getURL());
        } else {
            Assert.assertEquals("Version number doesn't match.", null, parser.getNewVersion());
            Assert.assertEquals("Market URL doesn't match.", null, parser.getURL());
        }
        Assert.assertEquals(
                "Version number doesn't match.",
                versionConfig.latestVersion,
                parser.getNewVersion());
        Assert.assertEquals("URL doesn't match.", versionConfig.downloadUrl, parser.getURL());
    }

    /**
     * Runs a test that is expected to fail in a particular way.
     * @param xml XML to parse.
     * @param expectedErrorCode Expected error code.
     * @param expectInstall Whether or not the parser should expect an install event.
     * @param expectPing Whether or not the parser should expect a ping element.
     * @param expectUpdate Whether or not the parser should expect an update check.
     */
    private static void runFailureTest(
            String xml,
            int expectedErrorCode,
            boolean expectInstall,
            boolean expectPing,
            boolean expectUpdate) {
        ResponseParser parser =
                new ResponseParser(true, "{APP_ID}", expectInstall, expectPing, expectUpdate);

        try {
            parser.parseResponse(xml);
        } catch (RequestFailureException e) {
            Assert.assertEquals("Incorrect error code received.", expectedErrorCode, e.errorCode);
            return;
        }

        Assert.fail("Failed to throw RequestFailureException for bad XML.");
    }

    @Test
    @Feature({"Omaha"})
    public void testValidAllTypes() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, true, true, UPDATE_STATUS_OK);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidNoInstall() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, false, true, UPDATE_STATUS_OK);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidNoPing() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, true, false, UPDATE_STATUS_OK);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidNoUpdatecheck() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, true, true, null);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidUpdatecheckNoUpdate() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, false, false, UPDATE_STATUS_NOUPDATE);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidUpdatecheckError() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, false, false, UPDATE_STATUS_ERROR);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidUpdatecheckUnknown() throws RequestFailureException {
        runSuccessTest(APP_STATUS_OK, false, false, UPDATE_STATUS_WTF);
    }

    @Test
    @Feature({"Omaha"})
    public void testValidAppStatusRestricted() throws RequestFailureException {
        runSuccessTest(APP_STATUS_RESTRICTED, false, false, null);
    }

    @Test
    @Feature({"Omaha"})
    public void testFailBogusResponse() {
        String xml = "Bogus";
        runFailureTest(xml, RequestFailureException.ERROR_MALFORMED_XML, false, false, false);
    }

    @Test
    @Feature({"Omaha"})
    public void testBadResponseProtocol() {
        String xml =
                createTestXML(
                        "2.0", "4088", "12345", APP_STATUS_OK, false, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_RESPONSE, false, false, false);
    }

    @Test
    @Feature({"Omaha"})
    public void testFailMissingDaystart() {
        String xml =
                createTestXML(
                        "3.0", null, null, APP_STATUS_OK, false, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_DAYSTART, false, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testFailMissingDaystartSeconds() {
        String xml =
                createTestXML(
                        "3.0", "4088", null, APP_STATUS_OK, false, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_DAYSTART, false, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testFailMissingDaystartDays() {
        String xml =
                createTestXML(
                        "3.0", null, "12345", APP_STATUS_OK, false, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_DAYSTART, false, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagMissingUpdatecheck() {
        String xml = createTestXML("3.0", "4088", "12345", APP_STATUS_OK, true, false, null, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_UPDATECHECK, true, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagUnexpectedUpdatecheck() {
        String xml =
                createTestXML(
                        "3.0", "4088", "12345", APP_STATUS_OK, true, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_UPDATECHECK, true, false, false);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagMissingPing() {
        String xml =
                createTestXML(
                        "3.0", "4088", "12345", APP_STATUS_OK, false, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_PING, false, true, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagUnexpectedPing() {
        String xml =
                createTestXML(
                        "3.0", "4088", "12345", APP_STATUS_OK, false, true, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_PING, false, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagMissingInstall() {
        String xml =
                createTestXML(
                        "3.0", "4088", "12345", APP_STATUS_OK, false, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_EVENT, true, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagUnexpectedInstall() {
        String xml =
                createTestXML(
                        "3.0", "4088", "12345", APP_STATUS_OK, true, false, UPDATE_STATUS_OK, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_EVENT, false, false, true);
    }

    @Test
    @Feature({"Omaha"})
    public void testAppTagStatusError() {
        String xml =
                createTestXML("3.0", "4088", "12345", APP_STATUS_ERROR, false, false, null, URL);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_APP, false, false, false);
    }

    @Test
    @Feature({"Omaha"})
    public void testUpdatecheckMissingUrl() {
        String xml =
                createTestXML(
                        "3.0",
                        "4088",
                        "12345",
                        APP_STATUS_OK,
                        false,
                        false,
                        UPDATE_STATUS_OK,
                        null);
        runFailureTest(xml, RequestFailureException.ERROR_PARSE_URLS, false, false, true);
    }
}