/*
* Copyright 2012 Google Inc.
*
* 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 com.google.android.gcm.server;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.anyInt;
import static org.mockito.Matchers.anyListOf;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.runners.MockitoJUnitRunner;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@RunWith(MockitoJUnitRunner.class)
public class SenderTest {
private final String regId = "15;16";
private final String collapseKey = "collapseKey";
private final boolean delayWhileIdle = true;
private final int retries = 42;
private final int ttl = 108;
private final String authKey = "4815162342";
private final JSONParser jsonParser = new JSONParser();
private final Message message =
new Message.Builder()
.collapseKey(collapseKey)
.delayWhileIdle(delayWhileIdle)
.timeToLive(ttl)
.addData("k1", "v1")
.addData("k2", "v2")
.addData("k3", "v3")
.build();
// creates a Mockito Spy so we can stub internal methods
@Spy private Sender sender = new Sender(authKey);
@Mock private HttpURLConnection mockedConn;
private final ByteArrayOutputStream outputStream =
new ByteArrayOutputStream();
private Result result;
@Before
public void setFixtures() {
result = new Result.Builder().build();
}
@Test(expected = IllegalArgumentException.class)
public void testConstructor_null() {
new Sender(null);
}
@Test
public void testSend_noRetryOk() throws Exception {
doNotSleep();
doReturn(result).when(sender).sendNoRetry(message, regId);
sender.send(message, regId, 0);
}
@Test(expected = IOException.class)
public void testSend_noRetryFail() throws Exception {
doNotSleep();
doReturn(null).when(sender).sendNoRetry(message, regId);
sender.send(message, regId, 0);
}
@Test(expected = IOException.class)
public void testSend_noRetryException() throws Exception {
doThrow(new IOException()).when(sender).sendNoRetry(message, regId);
sender.send(message, regId, 0);
}
@Test
public void testSend_retryOk() throws Exception {
doNothing().when(sender).sleep(anyInt());
doReturn(null) // fails 1st time
.doReturn(null) // fails 2nd time
.doReturn(result) // succeeds 3rd time
.when(sender).sendNoRetry(message, regId);
sender.send(message, regId, 2);
verify(sender, times(3)).sendNoRetry(message, regId);
}
@Test(expected = IOException.class)
public void testSend_retryFails() throws Exception {
doNothing().when(sender).sleep(anyInt());
doReturn(null) // fails 1st time
.doReturn(null) // fails 2nd time
.doReturn(null) // fails 3rd time
.when(sender).sendNoRetry(message, regId);
sender.send(message, regId, 2);
verify(sender, times(3)).sendNoRetry(message, regId);
}
@Test
public void testSend_retryExponentialBackoff() throws Exception {
ArgumentCaptor<Long> capturedSleep = ArgumentCaptor.forClass(Long.class);
int total = retries + 1; // fist attempt + retries
doNothing().when(sender).sleep(anyInt());
doReturn(null).when(sender).sendNoRetry(message, regId);
try {
sender.send(message, regId, retries);
fail("Should have thrown IOEXception");
} catch (IOException e) {
String message = e.getMessage();
assertTrue("invalid message:" + message, message.contains("" + total));
}
verify(sender, times(total)).sendNoRetry(message, regId);
verify(sender, times(retries)).sleep(capturedSleep.capture());
long backoffRange = Sender.BACKOFF_INITIAL_DELAY;
for (long value : capturedSleep.getAllValues()) {
assertTrue(value >= backoffRange / 2);
assertTrue(value <= backoffRange * 3 / 2);
if (2 * backoffRange < Sender.MAX_BACKOFF_DELAY) {
backoffRange *= 2;
}
}
}
@Test
public void testSendNoRetry_ok() throws Exception {
setResponseExpectations(200, "id=4815162342");
Result result = sender.sendNoRetry(message, regId);
assertNotNull(result);
assertEquals("4815162342", result.getMessageId());
assertNull(result.getCanonicalRegistrationId());
assertNull(result.getErrorCodeName());
assertRequestBody();
verify(mockedConn).disconnect();
}
@Test
public void testSendNoRetry_ok_canonical() throws Exception {
setResponseExpectations(200, "id=4815162342\nregistration_id=108");
Result result = sender.sendNoRetry(message, regId);
assertNotNull(result);
assertEquals("4815162342", result.getMessageId());
assertEquals("108", result.getCanonicalRegistrationId());
assertNull(result.getErrorCodeName());
assertRequestBody();
verify(mockedConn).disconnect();
}
@Test
public void testSendNoRetry_unauthorized() throws Exception {
setResponseExpectations(401, "");
try {
sender.sendNoRetry(message, regId);
fail("Should have thrown InvalidRequestException");
} catch (InvalidRequestException e) {
assertEquals(401, e.getHttpStatusCode());
}
assertRequestBody();
}
@Test
public void testSendNoRetry_error() throws Exception {
setResponseExpectations(200, "Error=D'OH!");
Result result = sender.sendNoRetry(message, regId);
assertNull(result.getMessageId());
assertNull(result.getCanonicalRegistrationId());
assertEquals("D'OH!", result.getErrorCodeName());
assertRequestBody();
verify(mockedConn).disconnect();
}
@Test
public void testSendNoRetry_serviceUnavailable() throws Exception {
setResponseExpectations(503, "");
Result result = sender.sendNoRetry(message, regId);
assertNull(result);
assertRequestBody();
}
@Test(expected = IOException.class)
public void testSendNoRetry_emptyBody() throws Exception {
setResponseExpectations(200, "");
sender.sendNoRetry(message, regId);
assertRequestBody();
verify(mockedConn).disconnect();
}
@Test(expected = IOException.class)
public void testSendNoRetry_noToken() throws Exception {
setResponseExpectations(200, "no token");
sender.sendNoRetry(message, regId);
assertRequestBody();
verify(mockedConn).disconnect();
}
@Test(expected = IOException.class)
public void testSendNoRetry_invalidToken() throws Exception {
setResponseExpectations(200, "bad=token");
sender.sendNoRetry(message, regId);
verify(mockedConn).disconnect();
}
@Test(expected = IOException.class)
public void testSendNoRetry_emptyToken() throws Exception {
setResponseExpectations(200, "token=");
sender.sendNoRetry(message, regId);
verify(mockedConn).disconnect();
}
@Test
public void testSendNoRetry_invalidHttpStatusCode() throws Exception {
setResponseExpectations(108, "id=4815162342");
try {
sender.sendNoRetry(message, regId);
} catch (InvalidRequestException e) {
assertEquals(108, e.getHttpStatusCode());
}
}
@Test(expected = IllegalArgumentException.class)
public void testSendNoRetry_noRegistrationId() throws Exception {
sender.sendNoRetry(new Message.Builder().build(), (String) null);
}
@Test()
public void testSend_json_allAttemptsFail() throws Exception {
doNothing().when(sender).sleep(anyInt());
// mock sendNoRetry
Result unaivalableResult =
new Result.Builder().errorCode("Unavailable").build();
// for the intermediate request, only the multicast id matters
MulticastResult mockedResult = new MulticastResult.Builder(0, 0, 0, 42)
.addResult(unaivalableResult).build();
List<String> regIds = Arrays.asList("108");
doReturn(mockedResult).when(sender).sendNoRetry(message, regIds);
MulticastResult actualResult = sender.send(message, regIds, 2);
assertNotNull(actualResult);
assertEquals(1, actualResult.getTotal());
assertEquals(0, actualResult.getSuccess());
assertEquals(1, actualResult.getFailure());
assertEquals(0, actualResult.getCanonicalIds());
assertEquals(42, actualResult.getMulticastId());
assertEquals(1, actualResult.getResults().size());
assertResult(actualResult.getResults().get(0), null, "Unavailable", null);
verify(sender, times(3)).sendNoRetry(message, regIds);
}
@Test()
public void testSend_json_secondAttemptOk() throws Exception {
doNothing().when(sender).sleep(anyInt());
// mock sendNoRetry
Result unaivalableResult =
new Result.Builder().errorCode("Unavailable").build();
Result okResult =
new Result.Builder().messageId("42").build();
// for the intermediate request, only the multicast id matters
MulticastResult mockedResult1 = new MulticastResult.Builder(0, 0, 0, 100)
.addResult(unaivalableResult).build();
MulticastResult mockedResult2 = new MulticastResult.Builder(0, 0, 0, 200)
.addResult(okResult).build();
List<String> regIds = Arrays.asList("108");
doReturn(mockedResult1) // fist time it fails
.doReturn(mockedResult2) // second time it succeeds
.when(sender).sendNoRetry(message, regIds);
MulticastResult actualResult = sender.send(message, regIds, 10);
assertNotNull(actualResult);
assertEquals(1, actualResult.getTotal());
assertEquals(1, actualResult.getSuccess());
assertEquals(0, actualResult.getFailure());
assertEquals(0, actualResult.getCanonicalIds());
assertEquals(100, actualResult.getMulticastId());
assertEquals(1, actualResult.getResults().size());
assertResult(actualResult.getResults().get(0), "42", null, null);
List<Long> retryMulticastIds = actualResult.getRetryMulticastIds();
assertEquals(1, retryMulticastIds.size());
assertEquals(200, retryMulticastIds.get(0).longValue());
verify(sender, times(2)).sendNoRetry(message, regIds);
}
@Test()
public void testSend_json_ok() throws Exception {
doNothing().when(sender).sleep(anyInt());
/*
* The following scenario is mocked below:
*
* input: 4, 8, 15, 16, 23, 42
*
* 1st call (multicast_id:100): 4,16:ok 8,15,23:unavailable, 42:error,
* 2nd call (multicast_id:200): 8,15: unavailable, 23:ok
* 3rd call (multicast_id:300): 8:error, 15:unavailable
* 4th call (multicast_id:400): 15:unavailable
*
* output: total:6, success:3, error: 3, canonicals: 0, multicast_id: 100
* results: ok, error, unavailable, ok, ok, error
*/
Result unaivalableResult =
new Result.Builder().errorCode("Unavailable").build();
Result errorResult =
new Result.Builder().errorCode("D'OH!").build();
Result okResultMsg4 =
new Result.Builder().messageId("msg4").build();
Result okResultMsg16 =
new Result.Builder().messageId("msg16").build();
Result okResultMsg23 =
new Result.Builder().messageId("msg23").build();
MulticastResult result1stCall = new MulticastResult.Builder(0, 0, 0, 100)
.addResult(okResultMsg4)
.addResult(unaivalableResult)
.addResult(unaivalableResult)
.addResult(okResultMsg16)
.addResult(unaivalableResult)
.addResult(errorResult)
.build();
doReturn(result1stCall).when(sender).sendNoRetry(message,
Arrays.asList("4", "8", "15", "16", "23", "42"));
MulticastResult result2ndCall = new MulticastResult.Builder(0, 0, 0, 200)
.addResult(unaivalableResult)
.addResult(unaivalableResult)
.addResult(okResultMsg23)
.build();
doReturn(result2ndCall).when(sender).sendNoRetry(message,
Arrays.asList("8", "15", "23"));
MulticastResult result3rdCall = new MulticastResult.Builder(0, 0, 0, 300)
.addResult(errorResult)
.addResult(unaivalableResult)
.build();
doReturn(result3rdCall).when(sender).sendNoRetry(message,
Arrays.asList("8", "15"));
MulticastResult result4thCall = new MulticastResult.Builder(0, 0, 0, 400)
.addResult(unaivalableResult)
.build();
doReturn(result4thCall).when(sender).sendNoRetry(message,
Arrays.asList("15"));
// call it
MulticastResult actualResult = sender.send(message,
Arrays.asList("4", "8", "15", "16", "23", "42"), 3);
// assert results
assertNotNull(actualResult);
assertEquals(6, actualResult.getTotal());
assertEquals(3, actualResult.getSuccess());
assertEquals(3, actualResult.getFailure());
assertEquals(0, actualResult.getCanonicalIds());
assertEquals(100, actualResult.getMulticastId());
List<Result> actualResults = actualResult.getResults();
assertEquals(6, actualResults.size());
assertResult(actualResults.get(0), "msg4", null, null); // 4
assertResult(actualResults.get(1), null, "D'OH!", null); // 8
assertResult(actualResults.get(2), null, "Unavailable", null); // 15
assertResult(actualResults.get(3), "msg16", null, null); // 16
assertResult(actualResults.get(4), "msg23", null, null); // 23
assertResult(actualResults.get(5), null, "D'OH!", null); // 42
List<Long> retryMulticastIds = actualResult.getRetryMulticastIds();
assertEquals(3, retryMulticastIds.size());
assertEquals(200, retryMulticastIds.get(0).longValue());
assertEquals(300, retryMulticastIds.get(1).longValue());
assertEquals(400, retryMulticastIds.get(2).longValue());
verify(sender, times(4)).sendNoRetry(eq(message), anyListOf(String.class));
}
@Test(expected = IllegalArgumentException.class)
public void testSendNoRetry_json_nullRegIds() throws Exception {
sender.sendNoRetry(message, (List<String>) null);
}
@Test(expected = IllegalArgumentException.class)
public void testSendNoRetry_json_emptyRegIds() throws Exception {
sender.sendNoRetry(message, Collections.<String>emptyList());
}
@Test
public void testSendNoRetry_json_badRequest() throws Exception {
setResponseExpectations(42, "bad json");
try {
sender.sendNoRetry(message, Arrays.asList("108"));
} catch (InvalidRequestException e) {
assertEquals(42, e.getHttpStatusCode());
assertEquals("bad json", e.getDescription());
assertRequestJsonBody("108");
}
}
@Test()
public void testSendNoRetry_json_ok() throws Exception {
String json = replaceQuotes("\n"
+ "{"
+ " 'multicast_id': 108,"
+ " 'success': 2,"
+ " 'failure': 1,"
+ " 'canonical_ids': 1,"
+ " 'results': ["
+ " {'message_id': '16'}, "
+ " {'error': 'DOH!'}, "
+ " {'message_id': '23', 'registration_id': '42'}"
+ " ]"
+ "}");
setResponseExpectations(200, json);
MulticastResult multicastResult = sender.sendNoRetry(message,
Arrays.asList("4", "8", "15"));
assertNotNull(multicastResult);
assertEquals(3, multicastResult.getTotal());
assertEquals(2, multicastResult.getSuccess());
assertEquals(1, multicastResult.getFailure());
assertEquals(1, multicastResult.getCanonicalIds());
assertEquals(108, multicastResult.getMulticastId());
List<Result> results = multicastResult.getResults();
assertNotNull(results);
assertEquals(3, results.size());
assertResult(results.get(0), "16", null, null);
assertResult(results.get(1), null, "DOH!", null);
assertResult(results.get(2), "23", null, "42");
assertRequestJsonBody("4", "8", "15");
}
// replace ' by ", otherwise JSON strins would need to escape double-quotes
private String replaceQuotes(String json) {
return json.replaceAll("'", "\"");
}
private void assertResult(Result result, String messageId, String error,
String canonicalRegistrationId) {
assertEquals(messageId, result.getMessageId());
assertEquals(error, result.getErrorCodeName());
assertEquals(canonicalRegistrationId, result.getCanonicalRegistrationId());
}
private void assertRequestJsonBody(String...expectedRegIds) throws Exception {
ArgumentCaptor<String> capturedBody = ArgumentCaptor.forClass(String.class);
verify(sender).post(eq(Constants.GCM_SEND_ENDPOINT), eq("application/json"),
capturedBody.capture());
// parse body
String body = capturedBody.getValue();
JSONObject json = (JSONObject) jsonParser.parse(body);
assertEquals(ttl, ((Long) json.get("time_to_live")).intValue());
assertEquals(collapseKey, json.get("collapse_key"));
assertEquals(delayWhileIdle, json.get("delay_while_idle"));
@SuppressWarnings("unchecked")
Map<String, Object> payload = (Map<String, Object>) json.get("data");
assertNotNull("no payload", payload);
assertEquals("wrong payload size", 3, payload.size());
assertEquals("v1", payload.get("k1"));
assertEquals("v2", payload.get("k2"));
assertEquals("v3", payload.get("k3"));
JSONArray actualRegIds = (JSONArray) json.get("registration_ids");
assertEquals("Wrong number of regIds",
expectedRegIds.length, actualRegIds.size());
for (int i = 0; i < expectedRegIds.length; i++) {
String expectedRegId = expectedRegIds[i];
String actualRegId = (String) actualRegIds.get(i);
assertEquals("invalid regId at index " + i, expectedRegId, actualRegId);
}
}
@Test
public void testNewKeyValues() {
Map<String, String> x = Sender.newKeyValues("key", "value");
assertEquals(1, x.size());
assertEquals("value", x.get("key"));
}
@Test(expected = IllegalArgumentException.class)
public void testNewKeyValues_nullKey() {
Sender.newKeyValues(null, "value");
}
@Test(expected = IllegalArgumentException.class)
public void testNewKeyValues_nullValue() {
Sender.newKeyValues("key", null);
}
@Test
public void testNewBody() {
StringBuilder body = Sender.newBody("name", "value");
assertEquals("name=value", body.toString());
}
@Test(expected = IllegalArgumentException.class)
public void testNewBody_nullKey() {
Sender.newBody(null, "value");
}
@Test(expected = IllegalArgumentException.class)
public void testNewBody_nullValue() {
Sender.newBody("key", null);
}
@Test
public void testAddParameter() {
StringBuilder body = new StringBuilder("P=NP");
Sender.addParameter(body, "name", "value");
assertEquals("P=NP&name=value", body.toString());
}
@Test(expected = IllegalArgumentException.class)
public void testAddParameter_nullBody() {
Sender.addParameter(null, "key", "value");
}
@Test(expected = IllegalArgumentException.class)
public void testAddParameter_nullKey() {
StringBuilder body = new StringBuilder();
Sender.addParameter(body, null, "value");
}
@Test(expected = IllegalArgumentException.class)
public void testAddParameter_nullValue() {
StringBuilder body = new StringBuilder();
Sender.addParameter(body, "key", null);
}
@Test
public void testGetString_oneLine() throws Exception {
String expected = "108";
InputStream stream = new ByteArrayInputStream(expected.getBytes());
String actual = Sender.getString(stream);
assertEquals(expected, actual);
}
@Test
public void testGetString_stripsLastLine() throws Exception {
InputStream stream = new ByteArrayInputStream("108\n".getBytes());
String stripped = Sender.getString(stream);
assertEquals("108", stripped);
}
@Test
public void testGetString_multipleLines() throws Exception {
String expected = "4\n8\n15\n\n16\n23\n42";
InputStream stream = new ByteArrayInputStream(expected.getBytes());
String actual = Sender.getString(stream);
assertEquals(expected, actual);
}
@Test(expected = IllegalArgumentException.class)
public void testGetString_nullValue() throws Exception {
Sender.getString(null);
}
@Test(expected = IllegalArgumentException.class)
public void testPost_noUrl() throws Exception {
sender.post(null, "whatever");
}
@Test(expected = IllegalArgumentException.class)
public void testPost_noBody() throws Exception {
sender.post(Constants.GCM_SEND_ENDPOINT, null);
}
@Test
public void testPost() throws Exception {
String requestBody = "req";
String responseBody = "resp";
setResponseExpectations(200, responseBody);
HttpURLConnection response =
sender.post(Constants.GCM_SEND_ENDPOINT, requestBody);
assertEquals(requestBody, new String(outputStream.toByteArray()));
verify(mockedConn).setRequestMethod("POST");
verify(mockedConn).setFixedLengthStreamingMode(requestBody.length());
verify(mockedConn).setRequestProperty("Content-Type",
"application/x-www-form-urlencoded;charset=UTF-8");
verify(mockedConn).setRequestProperty("Authorization", "key=" + authKey);
assertEquals(200, response.getResponseCode());
}
@Test
public void testPost_customType() throws Exception {
String requestBody = "req";
String responseBody = "resp";
setResponseExpectations(200, responseBody);
HttpURLConnection response =
sender.post(Constants.GCM_SEND_ENDPOINT, "stuff", requestBody);
assertEquals(requestBody, new String(outputStream.toByteArray()));
verify(mockedConn).setRequestMethod("POST");
verify(mockedConn).setFixedLengthStreamingMode(requestBody.length());
verify(mockedConn).setRequestProperty("Content-Type", "stuff");
verify(mockedConn).setRequestProperty("Authorization", "key=" + authKey);
assertEquals(200, response.getResponseCode());
}
/**
* Sets the expectations of the HTTP connection.
*/
private void setResponseExpectations(int statusCode, String response)
throws IOException {
when(mockedConn.getResponseCode()).thenReturn(statusCode);
InputStream inputStream = new ByteArrayInputStream(response.getBytes());
if (statusCode == 200) {
when(mockedConn.getInputStream()).thenReturn(inputStream);
} else {
when(mockedConn.getErrorStream()).thenReturn(inputStream);
}
when(mockedConn.getOutputStream()).thenReturn(outputStream);
doReturn(mockedConn).when(sender)
.getConnection(Constants.GCM_SEND_ENDPOINT);
}
private void doNotSleep() {
doThrow(new AssertionError("Thou should not sleep!")).when(sender)
.sleep(anyInt());
}
private void assertRequestBody() throws Exception {
ArgumentCaptor<String> capturedBody = ArgumentCaptor.forClass(String.class);
verify(sender).post(eq(Constants.GCM_SEND_ENDPOINT),
capturedBody.capture());
// parse body
String body = capturedBody.getValue();
Map<String, String> params = new HashMap<String, String>();
for(String param : body.split("&")) {
String[] split = param.split("=");
params.put(split[0], split[1]);
}
// check parameters
assertEquals(7, params.size());
assertParameter(params, "registration_id", regId);
assertParameter(params, "collapse_key", collapseKey);
assertParameter(params, "delay_while_idle", delayWhileIdle ? "1" : "0");
assertParameter(params, "time_to_live", "" + ttl);
assertParameter(params, "data.k1", "v1");
assertParameter(params, "data.k2", "v2");
assertParameter(params, "data.k3", "v3");
}
static void assertParameter(Map<String, String> params, String name,
String expectedValue) {
assertEquals("invalid value for request parameter parameter " + name,
params.get(name), expectedValue);
}
}