Bug 1243855 - Add Java client for interacting with autopush endpoint service. r?rnewman,sebastian
A few notes: the test is live, so I've marked it @Ignore, so that it
doesn't run during |mach gradle test|. There's some value in mocking
the service endpoint, but this is how I verify that the server works,
so it has more value right now as a live test than a mocked test. In
the future, that probably won't be true.
There are issues running the test locally because Robolectric doesn't
provide all the cipher suites we use in GlobalConstants: in
particular, the GCM suites aren't supported. This may improve as
Robolectric matures, or we may add a work-around in the code (like at
http://androidxref.com/4.4.4_r1/xref/libcore/support/src/test/java/libcore/java/security/StandardNames.java#68),
or we may add a test-specific flag. For now, I'm not going to address
it directly.
Finally, I put the code in mobile/android/services, simply because the
less that goes into base, the better our build times will be.
--- a/mobile/android/base/android-services.mozbuild
+++ b/mobile/android/base/android-services.mozbuild
@@ -854,16 +854,20 @@ sync_java_files = [TOPSRCDIR + '/mobile/
'fxa/sync/FxAccountProfileService.java',
'fxa/sync/FxAccountSchedulePolicy.java',
'fxa/sync/FxAccountSyncAdapter.java',
'fxa/sync/FxAccountSyncDelegate.java',
'fxa/sync/FxAccountSyncService.java',
'fxa/sync/FxAccountSyncStatusHelper.java',
'fxa/sync/SchedulePolicy.java',
'fxa/SyncStatusListener.java',
+ 'push/autopush/AutopushClient.java',
+ 'push/autopush/AutopushClientException.java',
+ 'push/RegisterUserAgentResponse.java',
+ 'push/SubscribeChannelResponse.java',
'sync/AlreadySyncingException.java',
'sync/BackoffHandler.java',
'sync/BadRequiredFieldJSONException.java',
'sync/CollectionKeys.java',
'sync/CommandProcessor.java',
'sync/CommandRunner.java',
'sync/CredentialException.java',
'sync/crypto/CryptoException.java',
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/RegisterUserAgentResponse.java
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.push;
+
+/**
+ * Thin container for a register User-Agent response.
+ */
+public class RegisterUserAgentResponse {
+ public final String uaid;
+ public final String secret;
+
+ public RegisterUserAgentResponse(String uaid, String secret) {
+ this.uaid = uaid;
+ this.secret = secret;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/SubscribeChannelResponse.java
@@ -0,0 +1,19 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 4; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.push;
+
+/**
+ * Thin container for a subscribe channel response.
+ */
+public class SubscribeChannelResponse {
+ public final String channelID;
+ public final String endpoint;
+
+ public SubscribeChannelResponse(String channelID, String endpoint) {
+ this.channelID = channelID;
+ this.endpoint = endpoint;
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClient.java
@@ -0,0 +1,402 @@
+/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.push.autopush;
+
+import ch.boye.httpclientandroidlib.HttpEntity;
+import ch.boye.httpclientandroidlib.HttpHeaders;
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.client.ClientProtocolException;
+import ch.boye.httpclientandroidlib.client.methods.HttpRequestBase;
+import ch.boye.httpclientandroidlib.impl.client.DefaultHttpClient;
+import org.mozilla.gecko.fxa.FxAccountConstants;
+import org.mozilla.gecko.push.RegisterUserAgentResponse;
+import org.mozilla.gecko.push.SubscribeChannelResponse;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.AuthHeaderProvider;
+import org.mozilla.gecko.sync.net.BaseResource;
+import org.mozilla.gecko.sync.net.BaseResourceDelegate;
+import org.mozilla.gecko.sync.net.BearerAuthHeaderProvider;
+import org.mozilla.gecko.sync.net.Resource;
+import org.mozilla.gecko.sync.net.SyncResponse;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.security.GeneralSecurityException;
+import java.util.Locale;
+import java.util.concurrent.Executor;
+
+
+/**
+ * Interact with the autopush endpoint HTTP API.
+ * <p/>
+ * The API is a Mozilla-proprietary interface, and not even specified to Mozilla's usual ad-hoc standards.
+ * This client is written against a work-in-progress, un-deployed upstream commit.
+ */
+public class AutopushClient {
+ protected static final String LOG_TAG = AutopushClient.class.getSimpleName();
+
+ protected static final String ACCEPT_HEADER = "application/json;charset=utf-8";
+ protected static final String TYPE = "gcm";
+
+ protected static final String JSON_KEY_UAID = "uaid";
+ protected static final String JSON_KEY_SECRET = "secret";
+ protected static final String JSON_KEY_CHANNEL_ID = "channelID";
+ protected static final String JSON_KEY_ENDPOINT = "endpoint";
+
+ protected static final String[] REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_UAID, JSON_KEY_SECRET, JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
+ protected static final String[] REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS = new String[] { JSON_KEY_CHANNEL_ID, JSON_KEY_ENDPOINT };
+
+ public static final String JSON_KEY_CODE = "code";
+ public static final String JSON_KEY_ERRNO = "errno";
+ public static final String JSON_KEY_ERROR = "error";
+ public static final String JSON_KEY_MESSAGE = "message";
+
+ /**
+ * The server's URI.
+ * <p>
+ * We assume throughout that this ends with a trailing slash (and guarantee as
+ * much in the constructor).
+ */
+ public final String serverURI;
+
+ protected final Executor executor;
+
+ public AutopushClient(String serverURI, Executor executor) {
+ if (serverURI == null) {
+ throw new IllegalArgumentException("Must provide a server URI.");
+ }
+ if (executor == null) {
+ throw new IllegalArgumentException("Must provide a non-null executor.");
+ }
+ this.serverURI = serverURI.endsWith("/") ? serverURI : serverURI + "/";
+ if (!this.serverURI.endsWith("/")) {
+ throw new IllegalArgumentException("Constructed serverURI must end with a trailing slash: " + this.serverURI);
+ }
+ this.executor = executor;
+ }
+
+ /**
+ * A legal autopush server URL includes a sender ID embedded into it. Extract it.
+ *
+ * @return a non-null non-empty sender ID.
+ * @throws AutopushClientException on failure.
+ */
+ public String getSenderIDFromServerURI() throws AutopushClientException {
+ // Turn "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407/" into "829133274407".
+ final String[] parts = serverURI.split("/", -1); // The -1 keeps the trailing empty part.
+ if (parts.length < 3) {
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ if (!parts[parts.length - 1].isEmpty()) {
+ // We guarantee a trailing slash, so we should always have an empty part at the tail.
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ if (!"gcm".equals(parts[parts.length - 3])) {
+ // We should always have /gcm/senderID/.
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ final String senderID = parts[parts.length - 2];
+ if (senderID.isEmpty()) {
+ // Something is horribly wrong -- we have /gcm//. Abort.
+ throw new AutopushClientException("Could not get sender ID from autopush server URI: " + serverURI);
+ }
+ return senderID;
+ }
+
+ /**
+ * Process a typed value extracted from a successful response (in an
+ * endpoint-dependent way).
+ */
+ public interface RequestDelegate<T> {
+ void handleError(Exception e);
+ void handleFailure(AutopushClientException e);
+ void handleSuccess(T result);
+ }
+
+ /**
+ * Intepret a response from the autopush server.
+ * <p>
+ * Throw an appropriate exception on errors; otherwise, return the response's
+ * status code.
+ *
+ * @return response's HTTP status code.
+ * @throws AutopushClientException
+ */
+ public static int validateResponse(HttpResponse response) throws AutopushClientException {
+ final int status = response.getStatusLine().getStatusCode();
+ if (200 <= status && status <= 299) {
+ return status;
+ }
+ int code;
+ int errno;
+ String error;
+ String message;
+ String info;
+ ExtendedJSONObject body;
+ try {
+ body = new SyncStorageResponse(response).jsonObjectBody();
+ // TODO: The service doesn't do the right thing yet :(
+ // body.throwIfFieldsMissingOrMisTyped(requiredErrorStringFields, String.class);
+ // body.throwIfFieldsMissingOrMisTyped(requiredErrorLongFields, Long.class);
+ code = body.getLong(JSON_KEY_CODE).intValue();
+ errno = body.getLong(JSON_KEY_ERRNO).intValue();
+ error = body.getString(JSON_KEY_ERROR);
+ message = body.getString(JSON_KEY_MESSAGE);
+ } catch (Exception e) {
+ throw new AutopushClientException.AutopushClientMalformedResponseException(response);
+ }
+ throw new AutopushClientException.AutopushClientRemoteException(response, code, errno, error, message, body);
+ }
+
+ protected <T> void invokeHandleError(final RequestDelegate<T> delegate, final Exception e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleError(e);
+ }
+ });
+ }
+
+ protected <T> void post(BaseResource resource, final ExtendedJSONObject requestBody, final RequestDelegate<T> delegate) {
+ try {
+ if (requestBody == null) {
+ resource.post((HttpEntity) null);
+ } else {
+ resource.post(requestBody);
+ }
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+ }
+
+ /**
+ * Translate resource callbacks into request callbacks invoked on the provided
+ * executor.
+ * <p>
+ * Override <code>handleSuccess</code> to parse the body of the resource
+ * request and call the request callback. <code>handleSuccess</code> is
+ * invoked via the executor, so you don't need to delegate further.
+ */
+ protected abstract class ResourceDelegate<T> extends BaseResourceDelegate {
+ protected abstract void handleSuccess(final int status, HttpResponse response, final ExtendedJSONObject body);
+
+ protected final String secret;
+ protected final RequestDelegate<T> delegate;
+
+ /**
+ * Create a delegate for an un-authenticated resource.
+ */
+ public ResourceDelegate(final Resource resource, final String secret, final RequestDelegate<T> delegate) {
+ super(resource);
+ this.delegate = delegate;
+ this.secret = secret;
+ }
+
+ @Override
+ public AuthHeaderProvider getAuthHeaderProvider() {
+ if (secret != null) {
+ return new BearerAuthHeaderProvider(secret);
+ }
+ return null;
+ }
+
+ @Override
+ public String getUserAgent() {
+ return FxAccountConstants.USER_AGENT;
+ }
+
+ @Override
+ public void handleHttpResponse(HttpResponse response) {
+ try {
+ final int status = validateResponse(response);
+ invokeHandleSuccess(status, response);
+ } catch (AutopushClientException e) {
+ invokeHandleFailure(e);
+ }
+ }
+
+ protected void invokeHandleFailure(final AutopushClientException e) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ delegate.handleFailure(e);
+ }
+ });
+ }
+
+ protected void invokeHandleSuccess(final int status, final HttpResponse response) {
+ executor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ ExtendedJSONObject body = new SyncResponse(response).jsonObjectBody();
+ ResourceDelegate.this.handleSuccess(status, response, body);
+ } catch (Exception e) {
+ delegate.handleError(e);
+ }
+ }
+ });
+ }
+
+ @Override
+ public void handleHttpProtocolException(final ClientProtocolException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleHttpIOException(IOException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void handleTransportException(GeneralSecurityException e) {
+ invokeHandleError(delegate, e);
+ }
+
+ @Override
+ public void addHeaders(HttpRequestBase request, DefaultHttpClient client) {
+ super.addHeaders(request, client);
+
+ // The basics.
+ final Locale locale = Locale.getDefault();
+ request.addHeader(HttpHeaders.ACCEPT_LANGUAGE, Utils.getLanguageTag(locale));
+ request.addHeader(HttpHeaders.ACCEPT, ACCEPT_HEADER);
+ }
+ }
+
+ public void registerUserAgent(final String token, RequestDelegate<RegisterUserAgentResponse> delegate) {
+ BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration"));
+ } catch (URISyntaxException e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<RegisterUserAgentResponse>(resource, null, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ body.throwIfFieldsMissingOrMisTyped(REGISTER_USER_AGENT_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
+ final String uaid = body.getString(JSON_KEY_UAID);
+ final String secret = body.getString(JSON_KEY_SECRET);
+ delegate.handleSuccess(new RegisterUserAgentResponse(uaid, secret));
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ body.put("type", TYPE);
+ body.put("token", token);
+
+ resource.post(body);
+ }
+
+ public void reregisterUserAgent(final String uaid, final String secret, final String token, RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ delegate.handleSuccess(null);
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ body.put("type", TYPE);
+ body.put("token", token);
+
+ resource.put(body);
+ }
+
+
+ public void subscribeChannel(final String uaid, final String secret, RequestDelegate<SubscribeChannelResponse> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription"));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<SubscribeChannelResponse>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ try {
+ body.throwIfFieldsMissingOrMisTyped(REGISTER_CHANNEL_RESPONSE_REQUIRED_STRING_FIELDS, String.class);
+ final String channelID = body.getString(JSON_KEY_CHANNEL_ID);
+ final String endpoint = body.getString(JSON_KEY_ENDPOINT);
+ delegate.handleSuccess(new SubscribeChannelResponse(channelID, endpoint));
+ return;
+ } catch (Exception e) {
+ delegate.handleError(e);
+ return;
+ }
+ }
+ };
+
+ final ExtendedJSONObject body = new ExtendedJSONObject();
+ resource.post(body);
+ }
+
+ public void unsubscribeChannel(final String uaid, final String secret, final String channelID, RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid + "/subscription/" + channelID));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ delegate.handleSuccess(null);
+ }
+ };
+
+ resource.delete();
+ }
+
+ public void unregisterUserAgent(final String uaid, final String secret, RequestDelegate<Void> delegate) {
+ final BaseResource resource;
+ try {
+ resource = new BaseResource(new URI(serverURI + "registration/" + uaid));
+ } catch (Exception e) {
+ invokeHandleError(delegate, e);
+ return;
+ }
+
+ resource.delegate = new ResourceDelegate<Void>(resource, secret, delegate) {
+ @Override
+ public void handleSuccess(int status, HttpResponse response, ExtendedJSONObject body) {
+ delegate.handleSuccess(null);
+ }
+ };
+
+ resource.delete();
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/services/src/main/java/org/mozilla/gecko/push/autopush/AutopushClientException.java
@@ -0,0 +1,68 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+package org.mozilla.gecko.push.autopush;
+
+import ch.boye.httpclientandroidlib.HttpResponse;
+import ch.boye.httpclientandroidlib.HttpStatus;
+import org.mozilla.gecko.sync.ExtendedJSONObject;
+import org.mozilla.gecko.sync.HTTPFailureException;
+import org.mozilla.gecko.sync.net.SyncStorageResponse;
+
+public class AutopushClientException extends Exception {
+ private static final long serialVersionUID = 7953459541558266500L;
+
+ public AutopushClientException(String detailMessage) {
+ super(detailMessage);
+ }
+
+ public AutopushClientException(Exception e) {
+ super(e);
+ }
+
+ public static class AutopushClientRemoteException extends AutopushClientException {
+ private static final long serialVersionUID = 2209313149952001000L;
+
+ public final HttpResponse response;
+ public final long httpStatusCode;
+ public final long apiErrorNumber;
+ public final String error;
+ public final String message;
+ public final ExtendedJSONObject body;
+
+ public AutopushClientRemoteException(HttpResponse response, long httpStatusCode, long apiErrorNumber, String error, String message, ExtendedJSONObject body) {
+ super(new HTTPFailureException(new SyncStorageResponse(response)));
+ if (body == null) {
+ throw new IllegalArgumentException("body must not be null");
+ }
+ this.response = response;
+ this.httpStatusCode = httpStatusCode;
+ this.apiErrorNumber = apiErrorNumber;
+ this.error = error;
+ this.message = message;
+ this.body = body;
+ }
+
+ @Override
+ public String toString() {
+ return "<AutopushClientRemoteException " + this.httpStatusCode + " [" + this.apiErrorNumber + "]: " + this.message + ">";
+ }
+
+ public boolean isInvalidAuthentication() {
+ return httpStatusCode == HttpStatus.SC_UNAUTHORIZED;
+ }
+
+ public boolean isNotFound() {
+ return httpStatusCode == HttpStatus.SC_NOT_FOUND;
+ }
+ }
+
+ public static class AutopushClientMalformedResponseException extends AutopushClientRemoteException {
+ private static final long serialVersionUID = 2209313149952001909L;
+
+ public AutopushClientMalformedResponseException(HttpResponse response) {
+ super(response, 0, 999, "Response malformed", "Response malformed", new ExtendedJSONObject());
+ }
+ }
+}
--- a/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/background/testhelpers/WaitHelper.java
@@ -2,16 +2,17 @@
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.testhelpers;
import org.mozilla.gecko.background.common.log.Logger;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executor;
import java.util.concurrent.TimeUnit;
/**
* Implements waiting for asynchronous test events.
*
* Call WaitHelper.getTestWaiter() to get the unique instance.
*
* Call performWait(runnable) to execute runnable synchronously.
@@ -163,9 +164,19 @@ public class WaitHelper {
public static void resetTestWaiter() {
singleWaiter = new WaitHelper();
}
public boolean isIdle() {
return queue.isEmpty();
}
+
+ public static Executor newSynchronousExecutor() {
+ return new Executor() {
+
+ @Override
+ public void execute(Runnable runnable) {
+ runnable.run();
+ }
+ };
+ }
}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestAutopushClient.java
@@ -0,0 +1,22 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.push.autopush.test;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.push.autopush.AutopushClient;
+
+@RunWith(TestRunner.class)
+public class TestAutopushClient {
+ final String serverURL = "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407";
+
+ @Test
+ public void testGetSenderID() throws Exception {
+ final AutopushClient client = new AutopushClient(serverURL, WaitHelper.newSynchronousExecutor());
+ Assert.assertEquals("829133274407", client.getSenderIDFromServerURI());
+ }
+}
new file mode 100644
--- /dev/null
+++ b/mobile/android/tests/background/junit4/src/org/mozilla/gecko/push/autopush/test/TestLiveAutopushClient.java
@@ -0,0 +1,141 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+package org.mozilla.gecko.push.autopush.test;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mozilla.gecko.background.fxa.FxAccountUtils;
+import org.mozilla.gecko.background.testhelpers.TestRunner;
+import org.mozilla.gecko.background.testhelpers.WaitHelper;
+import org.mozilla.gecko.push.RegisterUserAgentResponse;
+import org.mozilla.gecko.push.SubscribeChannelResponse;
+import org.mozilla.gecko.push.autopush.AutopushClient;
+import org.mozilla.gecko.push.autopush.AutopushClient.RequestDelegate;
+import org.mozilla.gecko.push.autopush.AutopushClientException;
+import org.mozilla.gecko.sync.Utils;
+import org.mozilla.gecko.sync.net.BaseResource;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.startsWith;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+
+/**
+ * This test straddles an awkward line: it uses Mockito, but doesn't actually mock the service
+ * endpoint. That's why it's a <b>live</b> test: most of its value is checking that the client
+ * implementation and the upstream server implementation are corresponding correctly.
+ */
+@RunWith(TestRunner.class)
+@Ignore("Live test that requires network connection -- remove this line to run this test.")
+public class TestLiveAutopushClient {
+ final String serverURL = "https://updates-autopush-dev.stage.mozaws.net/v1/gcm/829133274407";
+
+ protected AutopushClient client;
+
+ @Before
+ public void setUp() throws Exception {
+ BaseResource.rewriteLocalhost = false;
+ client = new AutopushClient(serverURL, WaitHelper.newSynchronousExecutor());
+ }
+
+ protected <T> T assertSuccess(RequestDelegate<T> delegate, Class<T> klass) {
+ verify(delegate, never()).handleError(any(Exception.class));
+ verify(delegate, never()).handleFailure(any(AutopushClientException.class));
+
+ final ArgumentCaptor<T> register = ArgumentCaptor.forClass(klass);
+ verify(delegate).handleSuccess(register.capture());
+
+ return register.getValue();
+ }
+
+ protected <T> AutopushClientException assertFailure(RequestDelegate<T> delegate, Class<T> klass) {
+ verify(delegate, never()).handleError(any(Exception.class));
+ verify(delegate, never()).handleSuccess(any(klass));
+
+ final ArgumentCaptor<AutopushClientException> failure = ArgumentCaptor.forClass(AutopushClientException.class);
+ verify(delegate).handleFailure(failure.capture());
+
+ return failure.getValue();
+ }
+
+ @Test
+ public void testUserAgent() throws Exception {
+ final RequestDelegate<RegisterUserAgentResponse> registerDelegate = mock(RequestDelegate.class);
+ client.registerUserAgent(Utils.generateGuid(), registerDelegate);
+
+ final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class);
+ Assert.assertNotNull(registerResponse);
+ Assert.assertNotNull(registerResponse.uaid);
+ Assert.assertNotNull(registerResponse.secret);
+
+ // Reregistering with a new GUID should succeed.
+ final RequestDelegate<Void> reregisterDelegate = mock(RequestDelegate.class);
+ client.reregisterUserAgent(registerResponse.uaid, registerResponse.secret, Utils.generateGuid(), reregisterDelegate);
+
+ Assert.assertNull(assertSuccess(reregisterDelegate, Void.class));
+
+ // Unregistering should succeed.
+ final RequestDelegate<Void> unregisterDelegate = mock(RequestDelegate.class);
+ client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, unregisterDelegate);
+
+ Assert.assertNull(assertSuccess(unregisterDelegate, Void.class));
+
+ // Trying to unregister a second time should give a 404.
+ final RequestDelegate<Void> reunregisterDelegate = mock(RequestDelegate.class);
+ client.unregisterUserAgent(registerResponse.uaid, registerResponse.secret, reunregisterDelegate);
+
+ final AutopushClientException failureException = assertFailure(reunregisterDelegate, Void.class);
+ Assert.assertThat(failureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
+ Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) failureException).isNotFound());
+ }
+
+ @Test
+ public void testChannel() throws Exception {
+ final RequestDelegate<RegisterUserAgentResponse> registerDelegate = mock(RequestDelegate.class);
+ client.registerUserAgent(Utils.generateGuid(), registerDelegate);
+
+ final RegisterUserAgentResponse registerResponse = assertSuccess(registerDelegate, RegisterUserAgentResponse.class);
+ Assert.assertNotNull(registerResponse);
+ Assert.assertNotNull(registerResponse.uaid);
+ Assert.assertNotNull(registerResponse.secret);
+
+ // We should be able to subscribe to a channel.
+ final RequestDelegate<SubscribeChannelResponse> subscribeDelegate = mock(RequestDelegate.class);
+ client.subscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeDelegate);
+
+ final SubscribeChannelResponse subscribeResponse = assertSuccess(subscribeDelegate, SubscribeChannelResponse.class);
+ Assert.assertNotNull(subscribeResponse);
+ Assert.assertNotNull(subscribeResponse.channelID);
+ Assert.assertNotNull(subscribeResponse.endpoint);
+ Assert.assertThat(subscribeResponse.endpoint, startsWith(FxAccountUtils.getAudienceForURL(serverURL)));
+
+ // And we should be able to unsubscribe.
+ final RequestDelegate<Void> unsubscribeDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, unsubscribeDelegate);
+
+ Assert.assertNull(assertSuccess(unsubscribeDelegate, Void.class));
+
+ // Trying to unsubscribe a second time should give a 404.
+ final RequestDelegate<Void> reunsubscribeDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid, registerResponse.secret, subscribeResponse.channelID, reunsubscribeDelegate);
+
+ final AutopushClientException reunsubscribeFailureException = assertFailure(reunsubscribeDelegate, Void.class);
+ Assert.assertThat(reunsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
+ Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) reunsubscribeFailureException).isNotFound());
+
+ // Trying to unsubscribe from a non-existent channel should give a 404. Right now it gives a 401!
+ final RequestDelegate<Void> badUnsubscribeDelegate = mock(RequestDelegate.class);
+ client.unsubscribeChannel(registerResponse.uaid + "BAD", registerResponse.secret, subscribeResponse.channelID, badUnsubscribeDelegate);
+
+ final AutopushClientException badUnsubscribeFailureException = assertFailure(badUnsubscribeDelegate, Void.class);
+ Assert.assertThat(badUnsubscribeFailureException, instanceOf(AutopushClientException.AutopushClientRemoteException.class));
+ Assert.assertTrue(((AutopushClientException.AutopushClientRemoteException) badUnsubscribeFailureException).isInvalidAuthentication());
+ }
+}