/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
package org.mozilla.android.sync.net.test;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.IOException;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.json.simple.parser.ParseException;
import org.junit.After;
import org.junit.Test;
import org.mozilla.android.sync.test.helpers.CommandHelpers;
import org.mozilla.android.sync.test.helpers.HTTPServerTestHelper;
import org.mozilla.android.sync.test.helpers.MockClientsDataDelegate;
import org.mozilla.android.sync.test.helpers.MockClientsDatabaseAccessor;
import org.mozilla.android.sync.test.helpers.MockGlobalSession;
import org.mozilla.android.sync.test.helpers.MockGlobalSessionCallback;
import org.mozilla.android.sync.test.helpers.MockServer;
import org.mozilla.android.sync.test.helpers.MockSyncClientsEngineStage;
import org.mozilla.android.sync.test.helpers.WaitHelper;
import org.mozilla.gecko.sync.CollectionKeys;
import org.mozilla.gecko.sync.CommandProcessor.Command;
import org.mozilla.gecko.sync.CryptoRecord;
import org.mozilla.gecko.sync.ExtendedJSONObject;
import org.mozilla.gecko.sync.GlobalSession;
import org.mozilla.gecko.sync.Logger;
import org.mozilla.gecko.sync.NonObjectJSONException;
import org.mozilla.gecko.sync.SyncConfigurationException;
import org.mozilla.gecko.sync.Utils;
import org.mozilla.gecko.sync.crypto.CryptoException;
import org.mozilla.gecko.sync.crypto.KeyBundle;
import org.mozilla.gecko.sync.delegates.ClientsDataDelegate;
import org.mozilla.gecko.sync.delegates.GlobalSessionCallback;
import org.mozilla.gecko.sync.net.BaseResource;
import org.mozilla.gecko.sync.net.SyncStorageResponse;
import org.mozilla.gecko.sync.repositories.NullCursorException;
import org.mozilla.gecko.sync.repositories.android.ClientsDatabaseAccessor;
import org.mozilla.gecko.sync.repositories.domain.ClientRecord;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import ch.boye.httpclientandroidlib.HttpStatus;
public class TestClientsEngineStage extends MockSyncClientsEngineStage {
public final static String LOG_TAG = "TestClientsEngSta";
public TestClientsEngineStage() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
super(initializeSession());
}
// Static so we can set it during the constructor. This is so evil.
private static MockGlobalSessionCallback callback;
private static GlobalSession initializeSession() throws SyncConfigurationException, IllegalArgumentException, NonObjectJSONException, IOException, ParseException, CryptoException, URISyntaxException {
callback = new MockGlobalSessionCallback();
GlobalSession session = new MockClientsGlobalSession(TEST_SERVER, USERNAME, PASSWORD, new KeyBundle(USERNAME, SYNC_KEY), callback);
session.config.setClusterURL(new URI(TEST_SERVER));
session.config.setCollectionKeys(CollectionKeys.generateCollectionKeys());
return session;
}
private static final int TEST_PORT = HTTPServerTestHelper.getTestPort();
private static final String TEST_SERVER = "http://localhost:" + TEST_PORT;
private static final String USERNAME = "john";
private static final String PASSWORD = "password";
private static final String SYNC_KEY = "abcdeabcdeabcdeabcdeabcdea";
private HTTPServerTestHelper data = new HTTPServerTestHelper();
private int numRecordsFromGetRequest = 0;
private ArrayList<ClientRecord> expectedClients = new ArrayList<ClientRecord>();
private ArrayList<ClientRecord> downloadedClients = new ArrayList<ClientRecord>();
// For test purposes.
private ClientRecord lastComputedLocalClientRecord;
private ClientRecord uploadedRecord;
private String uploadBodyTimestamp;
private long uploadHeaderTimestamp;
private MockServer currentUploadMockServer;
private MockServer currentDownloadMockServer;
private boolean stubUpload = false;
protected static WaitHelper testWaiter() {
return WaitHelper.getTestWaiter();
}
@Override
protected ClientRecord newLocalClientRecord(ClientsDataDelegate delegate) {
lastComputedLocalClientRecord = super.newLocalClientRecord(delegate);
return lastComputedLocalClientRecord;
}
@After
public void teardown() {
stubUpload = false;
getMockDataAccessor().resetVars();
}
@Override
public synchronized ClientsDatabaseAccessor getClientsDatabaseAccessor() {
if (db == null) {
db = new MockClientsDatabaseAccessor();
}
return db;
}
// For test use.
private MockClientsDatabaseAccessor getMockDataAccessor() {
return (MockClientsDatabaseAccessor) getClientsDatabaseAccessor();
}
private synchronized boolean mockDataAccessorIsClosed() {
if (db == null) {
return true;
}
return ((MockClientsDatabaseAccessor) db).closed;
}
@Override
protected ClientDownloadDelegate makeClientDownloadDelegate() {
return clientDownloadDelegate;
}
@Override
protected void downloadClientRecords() {
BaseResource.rewriteLocalhost = false;
data.startHTTPServer(currentDownloadMockServer);
super.downloadClientRecords();
}
@Override
protected void uploadClientRecord(CryptoRecord record) {
BaseResource.rewriteLocalhost = false;
if (stubUpload) {
session.advance();
return;
}
data.startHTTPServer(currentUploadMockServer);
super.uploadClientRecord(record);
}
@Override
protected void uploadClientRecords(JSONArray records) {
BaseResource.rewriteLocalhost = false;
if (stubUpload) {
return;
}
data.startHTTPServer(currentUploadMockServer);
super.uploadClientRecords(records);
}
public static class MockClientsGlobalSession extends MockGlobalSession {
private ClientsDataDelegate clientsDataDelegate = new MockClientsDataDelegate();
public MockClientsGlobalSession(String clusterURL,
String username,
String password,
KeyBundle syncKeyBundle,
GlobalSessionCallback callback)
throws SyncConfigurationException,
IllegalArgumentException,
IOException,
ParseException,
NonObjectJSONException {
super(clusterURL, username, password, syncKeyBundle, callback);
}
@Override
public ClientsDataDelegate getClientsDelegate() {
return clientsDataDelegate;
}
}
public class TestSuccessClientDownloadDelegate extends TestClientDownloadDelegate {
public TestSuccessClientDownloadDelegate(HTTPServerTestHelper data) {
super(data);
}
@Override
public void handleRequestFailure(SyncStorageResponse response) {
super.handleRequestFailure(response);
assertTrue(getMockDataAccessor().closed);
fail("Should not error.");
}
@Override
public void handleRequestError(Exception ex) {
super.handleRequestError(ex);
assertTrue(getMockDataAccessor().closed);
fail("Should not fail.");
}
@Override
public void handleWBO(CryptoRecord record) {
ClientRecord r;
try {
r = (ClientRecord) factory.createRecord(record.decrypt());
downloadedClients.add(r);
numRecordsFromGetRequest++;
} catch (Exception e) {
fail("handleWBO failed.");
}
}
}
public class TestHandleWBODownloadDelegate extends TestClientDownloadDelegate {
public TestHandleWBODownloadDelegate(HTTPServerTestHelper data) {
super(data);
}
@Override
public void handleRequestFailure(SyncStorageResponse response) {
super.handleRequestFailure(response);
assertTrue(getMockDataAccessor().closed);
fail("Should not error.");
}
@Override
public void handleRequestError(Exception ex) {
super.handleRequestError(ex);
assertTrue(getMockDataAccessor().closed);
ex.printStackTrace();
fail("Should not fail.");
}
}
public class MockSuccessClientUploadDelegate extends MockClientUploadDelegate {
public MockSuccessClientUploadDelegate(HTTPServerTestHelper data) {
super(data);
}
@Override
public void handleRequestSuccess(SyncStorageResponse response) {
uploadHeaderTimestamp = response.normalizedWeaveTimestamp();
super.handleRequestSuccess(response);
}
@Override
public void handleRequestFailure(SyncStorageResponse response) {
super.handleRequestFailure(response);
fail("Should not fail.");
}
@Override
public void handleRequestError(Exception ex) {
super.handleRequestError(ex);
ex.printStackTrace();
fail("Should not error.");
}
}
public class MockFailureClientUploadDelegate extends MockClientUploadDelegate {
public MockFailureClientUploadDelegate(HTTPServerTestHelper data) {
super(data);
}
@Override
public void handleRequestSuccess(SyncStorageResponse response) {
super.handleRequestSuccess(response);
fail("Should not succeed.");
}
@Override
public void handleRequestError(Exception ex) {
super.handleRequestError(ex);
fail("Should not fail.");
}
}
public class UploadMockServer extends MockServer {
@SuppressWarnings("unchecked")
private String postBodyForRecord(ClientRecord cr) {
final long now = cr.lastModified;
final BigDecimal modified = Utils.millisecondsToDecimalSeconds(now);
Logger.debug(LOG_TAG, "Now is " + now + " (" + modified + ")");
final JSONArray idArray = new JSONArray();
idArray.add(cr.guid);
final JSONObject result = new JSONObject();
result.put("modified", modified);
result.put("success", idArray);
result.put("failed", new JSONObject());
uploadBodyTimestamp = modified.toString();
return result.toJSONString();
}
private String putBodyForRecord(ClientRecord cr) {
final String modified = Utils.millisecondsToDecimalSecondsString(cr.lastModified);
uploadBodyTimestamp = modified;
return modified;
}
protected void handleUploadPUT(Request request, Response response) throws Exception {
Logger.debug(LOG_TAG, "Handling PUT: " + request.getPath());
// Save uploadedRecord to test against.
CryptoRecord cryptoRecord = CryptoRecord.fromJSONRecord(request.getContent());
cryptoRecord.keyBundle = session.keyBundleForCollection(COLLECTION_NAME);
uploadedRecord = (ClientRecord) factory.createRecord(cryptoRecord.decrypt());
// Note: collection is not saved in CryptoRecord.toJSONObject() upon upload.
// So its value is null and is set here so ClientRecord.equals() may be used.
uploadedRecord.collection = lastComputedLocalClientRecord.collection;
// Create response body containing current timestamp.
long now = System.currentTimeMillis();
PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json", now);
uploadedRecord.lastModified = now;
bodyStream.println(putBodyForRecord(uploadedRecord));
bodyStream.close();
}
protected void handleUploadPOST(Request request, Response response) throws Exception {
Logger.debug(LOG_TAG, "Handling POST: " + request.getPath());
String content = request.getContent();
Logger.debug(LOG_TAG, "Content is " + content);
JSONArray array = (JSONArray) new JSONParser().parse(content);
Logger.debug(LOG_TAG, "Content is " + array);
KeyBundle keyBundle = session.keyBundleForCollection(COLLECTION_NAME);
if (array.size() != 1) {
Logger.debug(LOG_TAG, "Expecting only one record! Fail!");
PrintStream bodyStream = this.handleBasicHeaders(request, response, 400, "text/plain");
bodyStream.println("Expecting only one record! Fail!");
bodyStream.close();
return;
}
CryptoRecord r = CryptoRecord.fromJSONRecord(new ExtendedJSONObject((JSONObject) array.get(0)));
r.keyBundle = keyBundle;
ClientRecord cr = (ClientRecord) factory.createRecord(r.decrypt());
cr.collection = lastComputedLocalClientRecord.collection;
uploadedRecord = cr;
Logger.debug(LOG_TAG, "Record is " + cr);
long now = System.currentTimeMillis();
PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/json", now);
cr.lastModified = now;
final String responseBody = postBodyForRecord(cr);
Logger.debug(LOG_TAG, "Response is " + responseBody);
bodyStream.println(responseBody);
bodyStream.close();
}
@Override
public void handle(Request request, Response response) {
try {
String method = request.getMethod();
Logger.debug(LOG_TAG, "Handling " + method);
if (method.equalsIgnoreCase("post")) {
handleUploadPOST(request, response);
} else if (method.equalsIgnoreCase("put")) {
handleUploadPUT(request, response);
} else {
PrintStream bodyStream = this.handleBasicHeaders(request, response, 404, "text/plain");
bodyStream.close();
}
} catch (Exception e) {
fail("Error handling uploaded client record in UploadMockServer.");
}
}
}
public class DownloadMockServer extends MockServer {
@Override
public void handle(Request request, Response response) {
try {
PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
for (int i = 0; i < 5; i++) {
ClientRecord record = new ClientRecord();
expectedClients.add(record);
CryptoRecord cryptoRecord = cryptoFromClient(record);
bodyStream.print(cryptoRecord.toJSONString() + "\n");
}
bodyStream.close();
} catch (IOException e) {
fail("Error handling downloaded client records in DownloadMockServer.");
}
}
}
public class DownloadLocalRecordMockServer extends MockServer {
@SuppressWarnings("unchecked")
@Override
public void handle(Request request, Response response) {
try {
PrintStream bodyStream = this.handleBasicHeaders(request, response, 200, "application/newlines");
ClientRecord record = new ClientRecord(session.getClientsDelegate().getAccountGUID());
// Timestamp on server is 10 seconds after local timestamp
// (would trigger 412 if upload was attempted).
CryptoRecord cryptoRecord = cryptoFromClient(record);
JSONObject object = cryptoRecord.toJSONObject();
final long modified = (setRecentClientRecordTimestamp() + 10000) / 1000;
Logger.debug(LOG_TAG, "Setting modified to " + modified);
object.put("modified", modified);
bodyStream.print(object.toJSONString() + "\n");
bodyStream.close();
} catch (IOException e) {
fail("Error handling downloaded client records in DownloadLocalRecordMockServer.");
}
}
}
private CryptoRecord cryptoFromClient(ClientRecord record) {
CryptoRecord cryptoRecord = record.getEnvelope();
cryptoRecord.keyBundle = clientDownloadDelegate.keyBundle();
try {
cryptoRecord.encrypt();
} catch (Exception e) {
fail("Cannot encrypt client record.");
}
return cryptoRecord;
}
private long setRecentClientRecordTimestamp() {
long timestamp = System.currentTimeMillis() - (CLIENTS_TTL_REFRESH - 1000);
session.config.persistServerClientRecordTimestamp(timestamp);
return timestamp;
}
private void performFailingUpload() {
// performNotify() occurs in MockGlobalSessionCallback.
testWaiter().performWait(new Runnable() {
@Override
public void run() {
clientUploadDelegate = new MockFailureClientUploadDelegate(data);
checkAndUpload();
}
});
}
@Test
public void testShouldUploadNoCommandsToProcess() throws NullCursorException {
// shouldUpload() returns true.
assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
assertFalse(commandsProcessedShouldUpload);
assertTrue(shouldUpload());
// Set the timestamp to be a little earlier than refresh time,
// so shouldUpload() returns false.
setRecentClientRecordTimestamp();
assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
assertFalse(commandsProcessedShouldUpload);
assertFalse(shouldUpload());
}
@SuppressWarnings("unchecked")
@Test
public void testShouldUploadProcessCommands() throws NullCursorException {
// shouldUpload() returns false since array is size 0 and
// it has not been long enough yet to require an upload.
processCommands(new JSONArray());
setRecentClientRecordTimestamp();
assertFalse(commandsProcessedShouldUpload);
assertFalse(shouldUpload());
// shouldUpload() returns true since array is size 1 even though
// it has not been long enough yet to require an upload.
JSONArray commands = new JSONArray();
commands.add(new JSONObject());
processCommands(commands);
setRecentClientRecordTimestamp();
assertEquals(1, commands.size());
assertTrue(commandsProcessedShouldUpload);
assertTrue(shouldUpload());
}
@Test
public void testWipeAndStoreShouldNotWipe() {
assertFalse(shouldWipe);
wipeAndStore(new ClientRecord());
assertFalse(shouldWipe);
assertFalse(getMockDataAccessor().clientsTableWiped);
assertTrue(getMockDataAccessor().storedRecord);
}
@Test
public void testWipeAndStoreShouldWipe() {
assertFalse(shouldWipe);
shouldWipe = true;
wipeAndStore(new ClientRecord());
assertFalse(shouldWipe);
assertTrue(getMockDataAccessor().clientsTableWiped);
assertTrue(getMockDataAccessor().storedRecord);
}
@Test
public void testDownloadClientRecord() {
// Make sure no upload occurs after a download so we can
// test download in isolation.
stubUpload = true;
currentDownloadMockServer = new DownloadMockServer();
// performNotify() occurs in MockGlobalSessionCallback.
testWaiter().performWait(new Runnable() {
@Override
public void run() {
clientDownloadDelegate = new TestSuccessClientDownloadDelegate(data);
downloadClientRecords();
}
});
assertEquals(expectedClients.size(), numRecordsFromGetRequest);
for (int i = 0; i < downloadedClients.size(); i++) {
assertTrue(expectedClients.get(i).guid.equals(downloadedClients.get(i).guid));
}
assertTrue(mockDataAccessorIsClosed());
}
@Test
public void testCheckAndUploadClientRecord() {
uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
assertFalse(commandsProcessedShouldUpload);
assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
currentUploadMockServer = new UploadMockServer();
// performNotify() occurs in MockGlobalSessionCallback.
testWaiter().performWait(new Runnable() {
@Override
public void run() {
clientUploadDelegate = new MockSuccessClientUploadDelegate(data);
checkAndUpload();
}
});
// Test ClientUploadDelegate.handleRequestSuccess().
Logger.debug(LOG_TAG, "Last computed local client record: " + lastComputedLocalClientRecord.guid);
Logger.debug(LOG_TAG, "Uploaded client record: " + uploadedRecord.guid);
assertTrue(lastComputedLocalClientRecord.equalPayloads(uploadedRecord));
assertEquals(0, uploadAttemptsCount.get());
assertTrue(callback.calledSuccess);
assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
// Body and header are the same.
assertEquals(Utils.decimalSecondsToMilliseconds(uploadBodyTimestamp),
session.config.getPersistedServerClientsTimestamp());
assertEquals(uploadedRecord.lastModified,
session.config.getPersistedServerClientRecordTimestamp());
assertEquals(uploadHeaderTimestamp, session.config.getPersistedServerClientsTimestamp());
}
@Test
public void testDownloadHasOurRecord() {
// Make sure no upload occurs after a download so we can
// test download in isolation.
stubUpload = true;
// We've uploaded our local record recently.
long initialTimestamp = setRecentClientRecordTimestamp();
currentDownloadMockServer = new DownloadLocalRecordMockServer();
// performNotify() occurs in MockGlobalSessionCallback.
testWaiter().performWait(new Runnable() {
@Override
public void run() {
clientDownloadDelegate = new TestHandleWBODownloadDelegate(data);
downloadClientRecords();
}
});
// Timestamp got updated (but not reset) since we downloaded our record
assertFalse(0 == session.config.getPersistedServerClientRecordTimestamp());
assertTrue(initialTimestamp < session.config.getPersistedServerClientRecordTimestamp());
assertTrue(mockDataAccessorIsClosed());
}
@Test
public void testResetTimestampOnDownload() {
// Make sure no upload occurs after a download so we can
// test download in isolation.
stubUpload = true;
currentDownloadMockServer = new DownloadMockServer();
// performNotify() occurs in MockGlobalSessionCallback.
testWaiter().performWait(new Runnable() {
@Override
public void run() {
clientDownloadDelegate = new TestHandleWBODownloadDelegate(data);
downloadClientRecords();
}
});
// Timestamp got reset since our record wasn't downloaded.
assertEquals(0, session.config.getPersistedServerClientRecordTimestamp());
assertTrue(mockDataAccessorIsClosed());
}
/**
* The following 8 tests are for ClientUploadDelegate.handleRequestFailure().
* for the varying values of uploadAttemptsCount, commandsProcessedShouldUpload,
* and the type of server error.
*
* The first 4 are for 412 Precondition Failures.
* The second 4 represent the functionality given any other type of variable.
*/
@Test
public void testHandle412UploadFailureLowCount() {
assertFalse(commandsProcessedShouldUpload);
currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
assertEquals(0, uploadAttemptsCount.get());
performFailingUpload();
assertEquals(0, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandle412UploadFailureHighCount() {
assertFalse(commandsProcessedShouldUpload);
currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
performFailingUpload();
assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandle412UploadFailureLowCountWithCommand() {
commandsProcessedShouldUpload = true;
currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
assertEquals(0, uploadAttemptsCount.get());
performFailingUpload();
assertEquals(0, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandle412UploadFailureHighCountWithCommand() {
commandsProcessedShouldUpload = true;
currentUploadMockServer = new MockServer(HttpStatus.SC_PRECONDITION_FAILED, null);
uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
performFailingUpload();
assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandleMiscUploadFailureLowCount() {
currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
assertFalse(commandsProcessedShouldUpload);
assertEquals(0, uploadAttemptsCount.get());
performFailingUpload();
assertEquals(0, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandleMiscUploadFailureHighCount() {
currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
assertFalse(commandsProcessedShouldUpload);
uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
performFailingUpload();
assertEquals(MAX_UPLOAD_FAILURE_COUNT, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandleMiscUploadFailureHighCountWithCommands() {
currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
commandsProcessedShouldUpload = true;
uploadAttemptsCount.set(MAX_UPLOAD_FAILURE_COUNT);
performFailingUpload();
assertEquals(MAX_UPLOAD_FAILURE_COUNT + 1, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
@Test
public void testHandleMiscUploadFailureMaxAttempts() {
currentUploadMockServer = new MockServer(HttpStatus.SC_BAD_REQUEST, null);
commandsProcessedShouldUpload = true;
assertEquals(0, uploadAttemptsCount.get());
performFailingUpload();
assertEquals(MAX_UPLOAD_FAILURE_COUNT + 1, uploadAttemptsCount.get());
assertTrue(callback.calledError);
}
class TestAddCommandsMockClientsDatabaseAccessor extends MockClientsDatabaseAccessor {
@Override
public List<Command> fetchCommandsForClient(String accountGUID) throws NullCursorException {
List<Command> commands = new ArrayList<Command>();
commands.add(CommandHelpers.getCommand1());
commands.add(CommandHelpers.getCommand2());
commands.add(CommandHelpers.getCommand3());
commands.add(CommandHelpers.getCommand4());
return commands;
}
}
@Test
public void testAddCommands() throws NullCursorException {
db = new TestAddCommandsMockClientsDatabaseAccessor();
this.addCommands(new ClientRecord());
assertEquals(1, toUpload.size());
assertEquals(4, toUpload.get(0).commands.size());
}
}