/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.test;
import java.io.IOException;
import java.io.PrintStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.junit.Assert;
import org.junit.Test;
import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
import org.mozilla.android.sync.test.helpers.MockServer;
import org.mozilla.android.sync.test.helpers.WaitHelper;
import org.mozilla.gecko.background.announcements.Announcement;
import org.mozilla.gecko.background.announcements.AnnouncementsConstants;
import org.mozilla.gecko.background.announcements.AnnouncementsFetchDelegate;
import org.mozilla.gecko.background.announcements.AnnouncementsFetcher;
import org.mozilla.gecko.sync.net.BaseResource;
import org.simpleframework.http.Path;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import ch.boye.httpclientandroidlib.impl.cookie.DateParseException;
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
public class TestAnnouncementFetch {
private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
private static final String TEST_SERVER = "http://127.0.0.1:" + TEST_PORT;
private static final String TEST_USER_AGENT = "TEST USER AGENT";
private static final String BASE_PATH = "/announce/";
private static final String BASE_URI = TEST_SERVER + BASE_PATH + AnnouncementsConstants.ANNOUNCE_PATH_SUFFIX;
private HTTPServerTestHelper data = new HTTPServerTestHelper();
private static void debug(String s) {
System.out.println(s);
}
public static final class MockFetchDelegate implements AnnouncementsFetchDelegate {
private final long now;
public List<Announcement> fetchedAnnouncements;
private String lastDate = null;
public MockFetchDelegate(long now) {
this.now = now;
}
private void done() {
WaitHelper.getTestWaiter().performNotify();
}
private void done(Exception e) {
e.printStackTrace();
Assert.fail("Error. " + e);
done();
}
@Override
public void onNewAnnouncements(List<Announcement> announcements, long fetched, String date) {
this.fetchedAnnouncements = announcements;
this.lastDate = date;
Assert.assertTrue(fetched >= now);
try {
// Date is seconds-granularity, so bump it by 1000ms for comparison.
Assert.assertTrue(DateUtils.parseDate(date).getTime() + 1000 >= fetched);
} catch (DateParseException e) {
WaitHelper.getTestWaiter().performNotify(e);
}
done();
}
@Override
public void onNoNewAnnouncements(long fetched, String date) {
Assert.fail("No new announcements. Fetched = " + fetched);
done();
}
@Override
public void onRemoteError(Exception e) {
done(e);
}
@Override
public void onLocalError(Exception e) {
done(e);
}
@Override
public void onRemoteFailure(int status) {
Assert.fail("Failure. " + status);
done();
}
@Override
public Locale getLocale() {
return new Locale("en", "gb");
}
@Override
public long getLastFetch() {
return 0L;
}
@Override
public String getLastDate() {
return this.lastDate;
}
@Override
public String getUserAgent() {
return TEST_USER_AGENT;
}
@Override
public String getServiceURL() {
return BASE_URI;
}
@Override
public void onBackoff(int retryAfterInSeconds) {
// Do nothing.
}
}
public class TestAnnouncement extends Announcement {
private int idleLower = 0;
private int idleUpper = Integer.MAX_VALUE;
private long availableFrom = 0;
private long availableTo = Long.MAX_VALUE;
private Locale targetLocale = null;
private String versionRegex = null;
private String channelRegex = null;
private String platformRegex = null;
/**
* Set the range of idle days that should match this snippet.
* @param lower If less than zero, rounds to zero.
* @param upper If zero, any value will match.
*/
public void setIdleBounds(int lower, int upper) {
this.idleLower = Math.min(0, lower);
this.idleUpper = (upper == 0) ? Integer.MAX_VALUE : upper;
}
public void setAvailableBounds(long lower, long upper) {
this.availableFrom = Math.min(0L, lower);
this.availableTo = (upper == 0L) ? Long.MAX_VALUE : upper;
}
public void setTargetLocale(Locale locale) {
this.targetLocale = locale;
}
public void setVersionRegex(String regex) {
this.versionRegex = regex;
}
public void setChannelRegex(String regex) {
this.channelRegex = regex;
}
public void setPlatformRegex(String regex) {
this.platformRegex = regex;
}
public TestAnnouncement(int id, String title, String text, URI uri) {
super(id, title, text, uri);
}
protected boolean localesMatch(List<Locale> locales) {
if (null == this.targetLocale) {
return true;
}
for (Locale locale : locales) {
// Thanks, tools, for messing around with case.
if (locale.toString().equalsIgnoreCase(this.targetLocale.toString().toLowerCase())) {
return true;
}
}
return false;
}
public boolean matches(String channel, String version, String platform, List<Locale> locales, int idle) {
long now = System.currentTimeMillis();
if (now < this.availableFrom ||
now > this.availableTo) {
debug("Not available.");
return false;
}
if (null != this.channelRegex) {
if (!channel.matches(this.channelRegex)) {
debug("No channel match.");
return false;
}
}
if (null != this.platformRegex) {
if (!platform.matches(this.platformRegex)) {
debug("No platform match.");
return false;
}
}
if (null != this.versionRegex) {
if (!version.matches(this.versionRegex)) {
debug("No version match.");
return false;
}
}
if (!this.localesMatch(locales)) {
debug("No locale match.");
return false;
}
if (idle < this.idleLower) {
debug("No idle match (too low).");
return false;
}
if (idle > this.idleUpper) {
debug("No idle match (too high).");
return false;
}
return true;
}
}
/**
* Respond to an announce API request by returning announcement JSON or
* response codes per
*
* https://wiki.mozilla.org/Services/Roadmaps/Campaign-Manager#Client_API
*
*/
public class AnnouncementFetchMockServer extends MockServer {
// announce/1/android/channel/version/platform
private static final int EXPECTED_PATH_LENGTH = 6;
public String lastReceivedIfModifiedSince;
private final ArrayList<TestAnnouncement> announcements = new ArrayList<TestAnnouncement>();
@SuppressWarnings("unchecked")
public void handle(Request request, Response response) {
try {
final String ims = request.getValue("if-modified-since");
lastReceivedIfModifiedSince = ims;
final String ua = request.getValue("user-agent");
debug("User-Agent: " + ua);
Assert.assertEquals(TEST_USER_AGENT, ua);
final List<Locale> locales = request.getLocales();
debug("Locales: " + locales + ", " + locales.get(0));
final String method = request.getMethod();
final Path path = request.getPath();
debug("Path: " + path);
String connectionHeader = request.getValue("connection");
Assert.assertEquals("close", connectionHeader);
if (!method.equalsIgnoreCase("get")) {
this.handleBasicHeaders(request, response, 405, "text/plain");
}
final String[] segments = path.getSegments();
if (segments.length != EXPECTED_PATH_LENGTH) {
this.handleBasicHeaders(request, response, 400, "text/plain");
}
// Don't bother with additional validation. This is test code!
String protocol = segments[1];
String product = segments[2];
String channel = segments[3];
String version = segments[4];
String platform = segments[5];
int idle = request.getQuery().getInteger("idle");
// These will cause the test to bomb out if they fail.
Assert.assertEquals("android", product);
Assert.assertEquals("1", protocol);
JSONArray matchingAnnouncements = this.getAnnouncements(channel, version, platform, locales, idle);
JSONObject body = new JSONObject();
body.put("announcements", matchingAnnouncements);
final PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json");
bodyStream.print(body.toJSONString());
bodyStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
@SuppressWarnings("unchecked")
private JSONArray getAnnouncements(String channel, String version,
String platform, List<Locale> locales,
int idle) {
JSONArray out = new JSONArray();
for (TestAnnouncement snippet : announcements) {
if (snippet.matches(channel, version, platform, locales, idle)) {
out.add(snippet.asJSON());
}
}
return out;
}
public void addAnnouncement(TestAnnouncement an) {
this.announcements.add(an);
}
}
private static final String TEST_ANNOUNCEMENT_ONE_TITLE = "Test announce one";
private TestAnnouncement prepareTestAnnouncementOne() {
URI uri;
try {
uri = new URI("http://example.com/");
} catch (URISyntaxException e) {
// It's fine.
return null;
}
String text = "Test snippet body is longer than the title.";
String title = TEST_ANNOUNCEMENT_ONE_TITLE;
int id = 1234;
TestAnnouncement announcement = new TestAnnouncement(id, title, text, uri);
announcement.setChannelRegex("^beta$");
announcement.setIdleBounds(2, 5);
announcement.setPlatformRegex("^arm.*$");
announcement.setTargetLocale(new Locale("en", "gb"));
announcement.setVersionRegex("^17\\..*$");
debug("Adding snippet:\n" + announcement.asJSON());
return announcement;
}
private MockFetchDelegate makeDelegate() {
final long now = System.currentTimeMillis();
return new MockFetchDelegate(now);
}
protected static MockFetchDelegate fetchBlocking(final URI uri, final MockFetchDelegate delegate) {
WaitHelper.getTestWaiter().performWait(new Runnable() {
@Override
public void run() {
AnnouncementsFetcher.fetchAnnouncements(uri, delegate);
}
});
return delegate;
}
@Test
public void testAnnouncementFetch() throws URISyntaxException {
BaseResource.rewriteLocalhost = false;
AnnouncementFetchMockServer mockServer = new AnnouncementFetchMockServer();
// Add a mock announcement.
mockServer.addAnnouncement(prepareTestAnnouncementOne());
try {
data.startHTTPServer(mockServer);
debug("Server started.");
// Make a request that matches.
final URI uri = AnnouncementsFetcher.getSnippetURI(BASE_URI, "beta", "17.0a1", "armeabi-v7a", 4);
final MockFetchDelegate delegate = fetchBlocking(uri, makeDelegate());
Assert.assertEquals(1, delegate.fetchedAnnouncements.size());
Assert.assertEquals(TEST_ANNOUNCEMENT_ONE_TITLE, delegate.fetchedAnnouncements.get(0).getTitle());
} finally {
data.stopHTTPServer();
}
}
@Test
public void testAnnouncementFetchResendsDate() throws URISyntaxException, DateParseException {
AnnouncementFetchMockServer mockServer = new AnnouncementFetchMockServer();
try {
data.startHTTPServer(mockServer);
debug("Server started.");
final URI uri = AnnouncementsFetcher.getSnippetURI(BASE_URI, "beta", "19", "armeabi", 0);
final MockFetchDelegate delegate = makeDelegate();
fetchBlocking(uri, delegate);
String firstFetch = delegate.getLastDate();
debug("First fetch got Date: " + firstFetch);
Assert.assertTrue(DateUtils.parseDate(firstFetch).getTime() + 1000 >= delegate.now);
fetchBlocking(uri, delegate);
debug("Second fetch sent If-Modified-Since: " + mockServer.lastReceivedIfModifiedSince);
Assert.assertEquals(firstFetch, mockServer.lastReceivedIfModifiedSince);
} finally {
data.stopHTTPServer();
}
}
}