Bug 1182193 - Part 1: Add Gradle-based Robolectric JUnit 4 tests r=nalexander

This commit is contained in:
vivek 2015-08-29 00:26:36 +03:00
parent 98facf267a
commit 8bcbe8a667
9 changed files with 767 additions and 7 deletions

View File

@ -28,7 +28,6 @@ android {
sourceSets {
main {
java {
exclude 'org/mozilla/gecko/tests/**'
exclude 'org/mozilla/gecko/resources/**'
if (!mozconfig.substs.MOZ_CRASHREPORTER) {
@ -57,6 +56,16 @@ android {
}
}
}
test {
java {
srcDir "src/background_junit4"
}
resources {
srcDir "resources/background_junit4"
}
}
}
}
@ -75,6 +84,9 @@ dependencies {
compile project(':preprocessed_code')
compile project(':preprocessed_resources')
compile project(':thirdparty')
testCompile 'junit:junit:4.12'
testCompile 'org.robolectric:robolectric:3.0'
}
apply plugin: 'idea'
@ -82,6 +94,5 @@ apply plugin: 'idea'
idea {
module {
excludeDirs += file('org/mozilla/gecko/resources')
excludeDirs += file('org/mozilla/gecko/tests')
}
}

View File

@ -30,10 +30,9 @@ buildscript {
}
dependencies {
// IntelliJ 14.0.2 wants 0.14.4; IntelliJ 14.0.3 and Android Studio want
// 1.0.0. There are major issues with the combination of 0.14.4, Gradle
// 2.2.1, and IntelliJ 14.0.2: see Bug 1120032.
classpath 'com.android.tools.build:gradle:1.0.0'
// Unit testing support was added in 1.1.0. IntelliJ 14.1.4 and Android
// Studio 1.2.1.1 appear to work fine with plugin version 1.3.0.
classpath 'com.android.tools.build:gradle:1.3.0'
classpath('com.stanfy.spoon:spoon-gradle-plugin:1.0.3-SNAPSHOT') {
// Without these, we get errors linting.
exclude module: 'guava'

View File

@ -136,7 +136,7 @@ class MachCommands(MachCommandBase):
# Test code.
srcdir('app/src/robocop_harness/org/mozilla/gecko', 'build/mobile/robocop')
srcdir('app/src/robocop/org/mozilla/gecko/tests', 'mobile/android/tests/browser/robocop')
srcdir('app/src/background/org/mozilla/gecko', 'mobile/android/tests/background/junit3/src')
srcdir('app/src/background/org/mozilla/gecko/background', 'mobile/android/tests/background/junit3/src')
srcdir('app/src/browser', 'mobile/android/tests/browser/junit3/src')
srcdir('app/src/javaaddons', 'mobile/android/tests/javaaddons/src')
# Test libraries.
@ -155,6 +155,9 @@ class MachCommands(MachCommandBase):
srcdir('base/src/main/res', 'mobile/android/base/resources')
srcdir('base/src/main/assets', 'mobile/android/app/assets')
srcdir('base/src/crashreporter/res', 'mobile/android/base/crashreporter/res')
# JUnit 4 test code.
srcdir('base/src/background_junit4', 'mobile/android/tests/background/junit4/src')
srcdir('base/resources/background_junit4', 'mobile/android/tests/background/junit4/resources')
manifest_path = os.path.join(self.topobjdir, 'mobile', 'android', 'gradle.manifest')
with FileAvoidWrite(manifest_path) as f:

View File

@ -0,0 +1,2 @@
sdk=21
constants=org.mozilla.gecko.BuildConfig

View File

@ -0,0 +1,171 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.background.testhelpers;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
import org.mozilla.gecko.background.common.log.Logger;
/**
* Implements waiting for asynchronous test events.
*
* Call WaitHelper.getTestWaiter() to get the unique instance.
*
* Call performWait(runnable) to execute runnable synchronously.
* runnable *must* call performNotify() on all exit paths to signal to
* the TestWaiter that the runnable has completed.
*
* @author rnewman
* @author nalexander
*/
public class WaitHelper {
public static final String LOG_TAG = "WaitHelper";
public static class Result {
public Throwable error;
public Result() {
error = null;
}
public Result(Throwable error) {
this.error = error;
}
}
public static abstract class WaitHelperError extends Error {
private static final long serialVersionUID = 7074690961681883619L;
}
/**
* Immutable.
*
* @author rnewman
*/
public static class TimeoutError extends WaitHelperError {
private static final long serialVersionUID = 8591672555848651736L;
public final int waitTimeInMillis;
public TimeoutError(int waitTimeInMillis) {
this.waitTimeInMillis = waitTimeInMillis;
}
}
public static class MultipleNotificationsError extends WaitHelperError {
private static final long serialVersionUID = -9072736521571635495L;
}
public static class InterruptedError extends WaitHelperError {
private static final long serialVersionUID = 8383948170038639308L;
}
public static class InnerError extends WaitHelperError {
private static final long serialVersionUID = 3008502618576773778L;
public Throwable innerError;
public InnerError(Throwable e) {
innerError = e;
if (e != null) {
// Eclipse prints the stack trace of the cause.
this.initCause(e);
}
}
}
public BlockingQueue<Result> queue = new ArrayBlockingQueue<Result>(1);
/**
* How long performWait should wait for, in milliseconds, with the
* convention that a negative value means "wait forever".
*/
public static int defaultWaitTimeoutInMillis = -1;
public void performWait(Runnable action) throws WaitHelperError {
this.performWait(defaultWaitTimeoutInMillis, action);
}
public void performWait(int waitTimeoutInMillis, Runnable action) throws WaitHelperError {
Logger.debug(LOG_TAG, "performWait called.");
Result result = null;
try {
if (action != null) {
try {
action.run();
Logger.debug(LOG_TAG, "Action done.");
} catch (Exception ex) {
Logger.debug(LOG_TAG, "Performing action threw: " + ex.getMessage());
throw new InnerError(ex);
}
}
if (waitTimeoutInMillis < 0) {
result = queue.take();
} else {
result = queue.poll(waitTimeoutInMillis, TimeUnit.MILLISECONDS);
}
Logger.debug(LOG_TAG, "Got result from queue: " + result);
} catch (InterruptedException e) {
// We were interrupted.
Logger.debug(LOG_TAG, "performNotify interrupted with InterruptedException " + e);
final InterruptedError interruptedError = new InterruptedError();
interruptedError.initCause(e);
throw interruptedError;
}
if (result == null) {
// We timed out.
throw new TimeoutError(waitTimeoutInMillis);
} else if (result.error != null) {
Logger.debug(LOG_TAG, "Notified with error: " + result.error.getMessage());
// Rethrow any assertion with which we were notified.
InnerError innerError = new InnerError(result.error);
throw innerError;
}
// Success!
}
public void performNotify(final Throwable e) {
if (e != null) {
Logger.debug(LOG_TAG, "performNotify called with Throwable: " + e.getMessage());
} else {
Logger.debug(LOG_TAG, "performNotify called.");
}
if (!queue.offer(new Result(e))) {
// This could happen if performNotify is called multiple times (which is an error).
throw new MultipleNotificationsError();
}
}
public void performNotify() {
this.performNotify(null);
}
public static Runnable onThreadRunnable(final Runnable r) {
return new Runnable() {
@Override
public void run() {
new Thread(r).start();
}
};
}
private static WaitHelper singleWaiter = new WaitHelper();
public static WaitHelper getTestWaiter() {
return singleWaiter;
}
public static void resetTestWaiter() {
singleWaiter = new WaitHelper();
}
public boolean isIdle() {
return queue.isEmpty();
}
}

View File

@ -0,0 +1,69 @@
/* 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.fxa;
import org.mozilla.gecko.background.fxa.SkewHandler;
import org.mozilla.gecko.sync.net.BaseResource;
import ch.boye.httpclientandroidlib.impl.cookie.DateUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricGradleTestRunner;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
@RunWith(RobolectricGradleTestRunner.class)
public class TestSkewHandler {
public TestSkewHandler() {
}
@Test
public void testSkewUpdating() throws Throwable {
SkewHandler h = new SkewHandler("foo.com");
assertEquals(0L, h.getSkewInSeconds());
assertEquals(0L, h.getSkewInMillis());
long server = 1390101197865L;
long local = server - 4500L;
h.updateSkewFromServerMillis(server, local);
assertEquals(4500L, h.getSkewInMillis());
assertEquals(4L, h.getSkewInSeconds());
local = server;
h.updateSkewFromServerMillis(server, local);
assertEquals(0L, h.getSkewInMillis());
assertEquals(0L, h.getSkewInSeconds());
local = server + 500L;
h.updateSkewFromServerMillis(server, local);
assertEquals(-500L, h.getSkewInMillis());
assertEquals(0L, h.getSkewInSeconds());
String date = "Sat, 18 Jan 2014 19:16:52 PST";
long dateInMillis = 1390101412000L; // Obviously this can differ somewhat due to precision.
long parsed = DateUtils.parseDate(date).getTime();
assertEquals(parsed, dateInMillis);
h.updateSkewFromHTTPDateString(date, dateInMillis);
assertEquals(0L, h.getSkewInMillis());
assertEquals(0L, h.getSkewInSeconds());
h.updateSkewFromHTTPDateString(date, dateInMillis + 1100L);
assertEquals(-1100L, h.getSkewInMillis());
assertEquals(Math.round(-1100L / 1000L), h.getSkewInSeconds());
}
@Test
public void testSkewSingleton() throws Exception {
SkewHandler h1 = SkewHandler.getSkewHandlerFromEndpointString("http://foo.com/bar");
SkewHandler h2 = SkewHandler.getSkewHandlerForHostname("foo.com");
SkewHandler h3 = SkewHandler.getSkewHandlerForResource(new BaseResource("http://foo.com/baz"));
assertTrue(h1 == h2);
assertTrue(h1 == h3);
SkewHandler.getSkewHandlerForHostname("foo.com").updateSkewFromServerMillis(1390101412000L, 1390001412000L);
final long actual = SkewHandler.getSkewHandlerForHostname("foo.com").getSkewInMillis();
assertEquals(100000000L, actual);
}
}

View File

@ -0,0 +1,204 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.fxa.login;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountClient10.RequestDelegate;
import org.mozilla.gecko.background.fxa.FxAccountClient10.StatusResponse;
import org.mozilla.gecko.background.fxa.FxAccountClient10.TwoKeys;
import org.mozilla.gecko.background.fxa.FxAccountClient20.LoginResponse;
import org.mozilla.gecko.background.fxa.FxAccountClientException.FxAccountClientRemoteException;
import org.mozilla.gecko.background.fxa.FxAccountRemoteError;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.fxa.PasswordStretcher;
import org.mozilla.gecko.browserid.MockMyIDTokenFactory;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import ch.boye.httpclientandroidlib.HttpStatus;
import ch.boye.httpclientandroidlib.ProtocolVersion;
import ch.boye.httpclientandroidlib.entity.StringEntity;
import ch.boye.httpclientandroidlib.message.BasicHttpResponse;
public class MockFxAccountClient implements FxAccountClient {
protected static MockMyIDTokenFactory mockMyIdTokenFactory = new MockMyIDTokenFactory();
public final String serverURI = "http://testServer.com";
public final Map<String, User> users = new HashMap<String, User>();
public final Map<String, String> sessionTokens = new HashMap<String, String>();
public final Map<String, String> keyFetchTokens = new HashMap<String, String>();
public static class User {
public final String email;
public final byte[] quickStretchedPW;
public final String uid;
public boolean verified;
public final byte[] kA;
public final byte[] wrapkB;
public User(String email, byte[] quickStretchedPW) {
this.email = email;
this.quickStretchedPW = quickStretchedPW;
this.uid = "uid/" + this.email;
this.verified = false;
this.kA = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
this.wrapkB = Utils.generateRandomBytes(FxAccountUtils.CRYPTO_KEY_LENGTH_BYTES);
}
}
protected LoginResponse addLogin(User user, byte[] sessionToken, byte[] keyFetchToken) {
// byte[] sessionToken = Utils.generateRandomBytes(8);
if (sessionToken != null) {
sessionTokens.put(Utils.byte2Hex(sessionToken), user.email);
}
// byte[] keyFetchToken = Utils.generateRandomBytes(8);
if (keyFetchToken != null) {
keyFetchTokens.put(Utils.byte2Hex(keyFetchToken), user.email);
}
return new LoginResponse(user.email, user.uid, user.verified, sessionToken, keyFetchToken);
}
public void addUser(String email, byte[] quickStretchedPW, boolean verified, byte[] sessionToken, byte[] keyFetchToken) {
User user = new User(email, quickStretchedPW);
users.put(email, user);
if (verified) {
verifyUser(email);
}
addLogin(user, sessionToken, keyFetchToken);
}
public void verifyUser(String email) {
users.get(email).verified = true;
}
public void clearAllUserTokens() throws UnsupportedEncodingException {
sessionTokens.clear();
keyFetchTokens.clear();
}
protected BasicHttpResponse makeHttpResponse(int statusCode, String body) {
BasicHttpResponse httpResponse = new BasicHttpResponse(new ProtocolVersion("HTTP", 1, 1), statusCode, body);
httpResponse.setEntity(new StringEntity(body, "UTF-8"));
return httpResponse;
}
protected <T> void handleFailure(RequestDelegate<T> requestDelegate, int code, int errno, String message) {
requestDelegate.handleFailure(new FxAccountClientRemoteException(makeHttpResponse(code, message),
code, errno, "Bad authorization", message, null, new ExtendedJSONObject()));
}
@Override
public void status(byte[] sessionToken, RequestDelegate<StatusResponse> requestDelegate) {
String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
User user = users.get(email);
if (email == null || user == null) {
handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
return;
}
requestDelegate.handleSuccess(new StatusResponse(email, user.verified));
}
@Override
public void loginAndGetKeys(byte[] emailUTF8, final PasswordStretcher passwordStretcher, final Map<String, String> queryParameters, RequestDelegate<LoginResponse> requestDelegate) {
User user;
try {
user = users.get(new String(emailUTF8, "UTF-8"));
} catch (UnsupportedEncodingException e) {
user = null;
}
if (user == null) {
handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_CREATE_AN_ACCOUNT_THAT_ALREADY_EXISTS, "invalid emailUTF8");
return;
}
byte[] quickStretchedPW;
try {
quickStretchedPW = passwordStretcher.getQuickStretchedPW(emailUTF8);
} catch (Exception e) {
handleFailure(requestDelegate, HttpStatus.SC_INTERNAL_SERVER_ERROR, 999, "error stretching password");
return;
}
if (user.quickStretchedPW == null || !Arrays.equals(user.quickStretchedPW, quickStretchedPW)) {
handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.INCORRECT_PASSWORD, "invalid quickStretchedPW");
return;
}
LoginResponse loginResponse = addLogin(user, Utils.generateRandomBytes(8), Utils.generateRandomBytes(8));
requestDelegate.handleSuccess(loginResponse);
}
@Override
public void keys(byte[] keyFetchToken, RequestDelegate<TwoKeys> requestDelegate) {
String email = keyFetchTokens.get(Utils.byte2Hex(keyFetchToken));
User user = users.get(email);
if (email == null || user == null) {
handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid keyFetchToken");
return;
}
if (!user.verified) {
handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
return;
}
requestDelegate.handleSuccess(new TwoKeys(user.kA, user.wrapkB));
}
@Override
public void sign(byte[] sessionToken, ExtendedJSONObject publicKey, long certificateDurationInMilliseconds, RequestDelegate<String> requestDelegate) {
String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
User user = users.get(email);
if (email == null || user == null) {
handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
return;
}
if (!user.verified) {
handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_OPERATE_ON_AN_UNVERIFIED_ACCOUNT, "user is unverified");
return;
}
try {
final long iat = System.currentTimeMillis();
final long dur = certificateDurationInMilliseconds;
final long exp = iat + dur;
String certificate = mockMyIdTokenFactory.createMockMyIDCertificate(RSACryptoImplementation.createPublicKey(publicKey), "test", iat, exp);
requestDelegate.handleSuccess(certificate);
} catch (Exception e) {
requestDelegate.handleError(e);
}
}
@Override
public void resendCode(byte[] sessionToken, RequestDelegate<Void> requestDelegate) {
String email = sessionTokens.get(Utils.byte2Hex(sessionToken));
User user = users.get(email);
if (email == null || user == null) {
handleFailure(requestDelegate, HttpStatus.SC_UNAUTHORIZED, FxAccountRemoteError.INVALID_AUTHENTICATION_TOKEN, "invalid sessionToken");
return;
}
requestDelegate.handleSuccess(null);
}
@Override
public void resendUnlockCode(byte[] emailUTF8, RequestDelegate<Void> requestDelegate) {
User user;
try {
user = users.get(new String(emailUTF8, "UTF-8"));
} catch (UnsupportedEncodingException e) {
user = null;
}
if (user == null) {
handleFailure(requestDelegate, HttpStatus.SC_BAD_REQUEST, FxAccountRemoteError.ATTEMPT_TO_ACCESS_AN_ACCOUNT_THAT_DOES_NOT_EXIST, "invalid emailUTF8");
return;
}
requestDelegate.handleSuccess(null);
}
@Override
public void createAccountAndGetKeys(byte[] emailUTF8, PasswordStretcher passwordStretcher, final Map<String, String> queryParameters, RequestDelegate<LoginResponse> delegate) {
delegate.handleError(new RuntimeException("Not yet implemented"));
}
}

View File

@ -0,0 +1,207 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.fxa.login;
import java.security.NoSuchAlgorithmException;
import java.util.LinkedList;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.BuildConfig;
import org.mozilla.gecko.background.fxa.FxAccountClient;
import org.mozilla.gecko.background.fxa.FxAccountUtils;
import org.mozilla.gecko.background.testhelpers.WaitHelper;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.RSACryptoImplementation;
import org.mozilla.gecko.fxa.login.FxAccountLoginStateMachine.LoginStateMachineDelegate;
import org.mozilla.gecko.fxa.login.FxAccountLoginTransition.Transition;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.Utils;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricGradleTestRunner.class)
public class TestFxAccountLoginStateMachine {
// private static final String TEST_AUDIENCE = "http://testAudience.com";
private static final String TEST_EMAIL = "test@test.com";
private static byte[] TEST_EMAIL_UTF8;
private static final String TEST_PASSWORD = "testtest";
private static byte[] TEST_PASSWORD_UTF8;
private static byte[] TEST_QUICK_STRETCHED_PW;
private static byte[] TEST_UNWRAPKB;
private static final byte[] TEST_SESSION_TOKEN = Utils.generateRandomBytes(32);
private static final byte[] TEST_KEY_FETCH_TOKEN = Utils.generateRandomBytes(32);
protected MockFxAccountClient client;
protected FxAccountLoginStateMachine sm;
@Before
public void setUp() throws Exception {
if (TEST_EMAIL_UTF8 == null) {
TEST_EMAIL_UTF8 = TEST_EMAIL.getBytes("UTF-8");
}
if (TEST_PASSWORD_UTF8 == null) {
TEST_PASSWORD_UTF8 = TEST_PASSWORD.getBytes("UTF-8");
}
if (TEST_QUICK_STRETCHED_PW == null) {
TEST_QUICK_STRETCHED_PW = FxAccountUtils.generateQuickStretchedPW(TEST_EMAIL_UTF8, TEST_PASSWORD_UTF8);
}
if (TEST_UNWRAPKB == null) {
TEST_UNWRAPKB = FxAccountUtils.generateUnwrapBKey(TEST_QUICK_STRETCHED_PW);
}
client = new MockFxAccountClient();
sm = new FxAccountLoginStateMachine();
}
protected static class Trace {
public final LinkedList<State> states;
public final LinkedList<Transition> transitions;
public Trace(LinkedList<State> states, LinkedList<Transition> transitions) {
this.states = states;
this.transitions = transitions;
}
public void assertEquals(String string) {
Assert.assertArrayEquals(string.split(", "), toString().split(", "));
}
@Override
public String toString() {
final LinkedList<State> states = new LinkedList<State>(this.states);
final LinkedList<Transition> transitions = new LinkedList<Transition>(this.transitions);
LinkedList<String> names = new LinkedList<String>();
State state;
while ((state = states.pollFirst()) != null) {
names.add(state.getStateLabel().name());
Transition transition = transitions.pollFirst();
if (transition != null) {
names.add(">" + transition.toString());
}
}
return names.toString();
}
public String stateString() {
LinkedList<String> names = new LinkedList<String>();
for (State state : states) {
names.add(state.getStateLabel().name());
}
return names.toString();
}
public String transitionString() {
LinkedList<String> names = new LinkedList<String>();
for (Transition transition : transitions) {
names.add(transition.toString());
}
return names.toString();
}
}
protected Trace trace(final State initialState, final StateLabel desiredState) {
final LinkedList<Transition> transitions = new LinkedList<Transition>();
final LinkedList<State> states = new LinkedList<State>();
states.add(initialState);
WaitHelper.getTestWaiter().performWait(new Runnable() {
@Override
public void run() {
sm.advance(initialState, desiredState, new LoginStateMachineDelegate() {
@Override
public void handleTransition(Transition transition, State state) {
transitions.add(transition);
states.add(state);
}
@Override
public void handleFinal(State state) {
WaitHelper.getTestWaiter().performNotify();
}
@Override
public FxAccountClient getClient() {
return client;
}
@Override
public long getCertificateDurationInMilliseconds() {
return 30 * 1000;
}
@Override
public long getAssertionDurationInMilliseconds() {
return 10 * 1000;
}
@Override
public BrowserIDKeyPair generateKeyPair() throws NoSuchAlgorithmException {
return RSACryptoImplementation.generateKeyPair(512);
}
});
}
});
return new Trace(states, transitions);
}
@Test
public void testEnagedUnverified() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, false, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
trace.assertEquals("[Engaged, >AccountNeedsVerification, Engaged]");
}
@Test
public void testEngagedTransitionToAccountVerified() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", false, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
trace.assertEquals("[Engaged, >AccountVerified, Cohabiting, >LogMessage('sign succeeded'), Married]");
}
@Test
public void testEngagedVerified() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >LogMessage('sign succeeded'), Married]");
}
@Test
public void testPartial() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
// What if we stop at Cohabiting?
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Cohabiting);
trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting]");
}
@Test
public void testBadSessionToken() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
client.sessionTokens.clear();
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >Log(<FxAccountClientRemoteException 401 [110]: invalid sessionToken>), Separated, >PasswordRequired, Separated]");
}
@Test
public void testBadKeyFetchToken() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
client.keyFetchTokens.clear();
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
trace.assertEquals("[Engaged, >Log(<FxAccountClientRemoteException 401 [110]: invalid keyFetchToken>), Separated, >PasswordRequired, Separated]");
}
@Test
public void testMarried() throws Exception {
client.addUser(TEST_EMAIL, TEST_QUICK_STRETCHED_PW, true, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN);
Trace trace = trace(new Engaged(TEST_EMAIL, "uid", true, TEST_UNWRAPKB, TEST_SESSION_TOKEN, TEST_KEY_FETCH_TOKEN), StateLabel.Married);
trace.assertEquals("[Engaged, >LogMessage('keys succeeded'), Cohabiting, >LogMessage('sign succeeded'), Married]");
// What if we're already in the desired state?
State married = trace.states.getLast();
Assert.assertEquals(StateLabel.Married, married.getStateLabel());
trace = trace(married, StateLabel.Married);
trace.assertEquals("[Married]");
}
}

View File

@ -0,0 +1,94 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.gecko.fxa.login;
import junit.framework.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mozilla.gecko.BuildConfig;
import org.mozilla.gecko.browserid.BrowserIDKeyPair;
import org.mozilla.gecko.browserid.DSACryptoImplementation;
import org.mozilla.gecko.fxa.login.State.StateLabel;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.Utils;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;
@RunWith(RobolectricGradleTestRunner.class)
public class TestStateFactory {
@Test
public void testGetStateV3() throws Exception {
MigratedFromSync11 migrated = new MigratedFromSync11("email", "uid", true, "password");
// For the current version, we expect to read back what we wrote.
ExtendedJSONObject o;
State state;
o = migrated.toJSONObject();
Assert.assertEquals(3, o.getLong("version").intValue());
state = StateFactory.fromJSONObject(migrated.stateLabel, o);
Assert.assertEquals(StateLabel.MigratedFromSync11, state.stateLabel);
Assert.assertEquals(o, state.toJSONObject());
// Null passwords are OK.
MigratedFromSync11 migratedNullPassword = new MigratedFromSync11("email", "uid", true, null);
o = migratedNullPassword.toJSONObject();
Assert.assertEquals(3, o.getLong("version").intValue());
state = StateFactory.fromJSONObject(migratedNullPassword.stateLabel, o);
Assert.assertEquals(StateLabel.MigratedFromSync11, state.stateLabel);
Assert.assertEquals(o, state.toJSONObject());
}
@Test
public void testGetStateV2() throws Exception {
byte[] sessionToken = Utils.generateRandomBytes(32);
byte[] kA = Utils.generateRandomBytes(32);
byte[] kB = Utils.generateRandomBytes(32);
BrowserIDKeyPair keyPair = DSACryptoImplementation.generateKeyPair(512);
Cohabiting cohabiting = new Cohabiting("email", "uid", sessionToken, kA, kB, keyPair);
String certificate = "certificate";
Married married = new Married("email", "uid", sessionToken, kA, kB, keyPair, certificate);
// For the current version, we expect to read back what we wrote.
ExtendedJSONObject o;
State state;
o = married.toJSONObject();
Assert.assertEquals(3, o.getLong("version").intValue());
state = StateFactory.fromJSONObject(married.stateLabel, o);
Assert.assertEquals(StateLabel.Married, state.stateLabel);
Assert.assertEquals(o, state.toJSONObject());
o = cohabiting.toJSONObject();
Assert.assertEquals(3, o.getLong("version").intValue());
state = StateFactory.fromJSONObject(cohabiting.stateLabel, o);
Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel);
Assert.assertEquals(o, state.toJSONObject());
}
@Test
public void testGetStateV1() throws Exception {
// We can't rely on generating correct V1 objects (since the generation code
// may change); so we hard code a few test examples here. These examples
// have RSA key pairs; when they're parsed, we return DSA key pairs.
ExtendedJSONObject o = new ExtendedJSONObject("{\"uid\":\"uid\",\"sessionToken\":\"4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011\",\"certificate\":\"certificate\",\"keyPair\":{\"publicKey\":{\"e\":\"65537\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"6807533330618101360064115400338014782301295929300445938471117364691566605775022173055292460962170873583673516346599808612503093914221141089102289381448225\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"}},\"email\":\"email\",\"verified\":true,\"kB\":\"0b048f285c19067f200da7bfbe734ed213cefcd8f543f0fdd4a8ccab48cbbc89\",\"kA\":\"59a9edf2d41de8b24e69df9133bc88e96913baa75421882f4c55d842d18fc8a1\",\"version\":1}");
// A Married state is regressed to a Cohabited state.
Cohabiting state = (Cohabiting) StateFactory.fromJSONObject(StateLabel.Married, o);
Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel);
Assert.assertEquals("uid", state.uid);
Assert.assertEquals("4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011", Utils.byte2Hex(state.sessionToken));
Assert.assertEquals("DS128", state.keyPair.getPrivate().getAlgorithm());
o = new ExtendedJSONObject("{\"uid\":\"uid\",\"sessionToken\":\"4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011\",\"keyPair\":{\"publicKey\":{\"e\":\"65537\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"},\"privateKey\":{\"d\":\"6807533330618101360064115400338014782301295929300445938471117364691566605775022173055292460962170873583673516346599808612503093914221141089102289381448225\",\"n\":\"7598360104379019497828904063491254083855849024432238665262988260947462372141971045236693389494635158997975098558915846889960089362159921622822266839560631\",\"algorithm\":\"RS\"}},\"email\":\"email\",\"verified\":true,\"kB\":\"0b048f285c19067f200da7bfbe734ed213cefcd8f543f0fdd4a8ccab48cbbc89\",\"kA\":\"59a9edf2d41de8b24e69df9133bc88e96913baa75421882f4c55d842d18fc8a1\",\"version\":1}");
state = (Cohabiting) StateFactory.fromJSONObject(StateLabel.Cohabiting, o);
Assert.assertEquals(StateLabel.Cohabiting, state.stateLabel);
Assert.assertEquals("uid", state.uid);
Assert.assertEquals("4e2830da6ce466ddb401fbca25b96a621209eea83851254800f84cc4069ef011", Utils.byte2Hex(state.sessionToken));
Assert.assertEquals("DS128", state.keyPair.getPrivate().getAlgorithm());
}
}