package org.ohmage.service.test; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.test.ServiceTestCase; import android.test.mock.MockContext; import com.commonsware.cwac.wakeful.WakefulIntentService; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.ohmage.OhmageApi; import org.ohmage.db.DbContract.Campaigns; import org.ohmage.db.DbContract.Responses; import org.ohmage.db.Models.Campaign; import org.ohmage.db.Models.Response; import org.ohmage.db.test.MockContentProviderContext; import org.ohmage.db.test.ResponseCursor; import org.ohmage.service.SurveyGeotagService; import org.ohmage.service.UploadService; import java.util.ArrayList; /** * Tests the {@link UploadService} * We might want to change how these tests are performed, they are fairly tightly * coupled with the implementation of the service. It might be better if it was less coupled * to implementation and more to functionality * @author cketcham * */ public class UploadServiceTest extends ServiceTestCase<UploadService> { private OhmageApi mOhmageApi = new OhmageApi() { @Override public UploadResponse surveyUpload(String serverUrl, String username, String hashedPassword, String client, String campaignUrn, String campaignCreationTimestamp, String responseJson, ArrayList<MediaPart> photos) { return new UploadResponse(Result.SUCCESS, null); } }; public UploadServiceTest() { super(UploadService.class); } /** * Test uploading response with an image * @throws InterruptedException */ public void testResponseUploadWithImage() throws InterruptedException { fail("todo"); } /** * Test that the response is formatted correctly for the server * @throws InterruptedException */ public void testResponseIsFormattedForServer() throws InterruptedException { Intent i =new Intent(); i.setData(Responses.CONTENT_URI); i.putExtra("upload_surveys", true); final Response response = new Response(); response._id = 3; response.campaignUrn = ResponseCursor.MOCK_CAMPAIGN_URN; response.timezone = "America/Los_Angeles"; response.time = Long.parseLong("1321489758496"); response.surveyLaunchContext = "{\"launch_time\":\"2011-11-16 16:29:00\",\"active_triggers\":[]}"; response.surveyId = "AdvertisingMedia"; response.locationStatus = SurveyGeotagService.LOCATION_UNAVAILABLE; response.response = "[]"; UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), response); setContext(context); startService(i, new OhmageApi() { @Override public UploadResponse surveyUpload(String serverUrl, String username, String hashedPassword, String client, String campaignUrn, String campaignCreationTimestamp, String responseJson, ArrayList<MediaPart> photos) { assertEquals(response.campaignUrn, campaignUrn); try { JSONArray json = new JSONArray(responseJson); assertEquals(1, json.length()); JSONObject object = json.getJSONObject(0); //TODO: timestamp and prompt responses assertEquals(response.timezone, object.getString("timezone")); assertEquals(response.time, object.getLong("time")); assertEquals(response.surveyLaunchContext, object.getString("survey_launch_context")); assertEquals(response.surveyId, object.getString("survey_id")); assertEquals(response.locationStatus, object.getString("location_status")); assertFalse(object.has("location")); JSONArray responses = new JSONArray(object.getString("responses")); } catch (JSONException e) { fail(); } return new UploadResponse(Result.SUCCESS, null); } }); } /** * Tests that the upload service fails gracefully with invalid uri * @throws InterruptedException */ public void testUploadInvalidUri() throws InterruptedException { Intent i =new Intent(); i.setData(Uri.parse("content://blah")); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int first = 0; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(first++ == 0) { assertEquals(Responses.buildResponseUri(0), uri); } return super.query(uri, projection, selection, selectionArgs, sortOrder); } }; setContext(context); startService(i); } /** * Tests that the upload service fails gracefully with junk uri * @throws InterruptedException */ public void testUploadBadUri() throws InterruptedException { Intent i =new Intent(); i.setData(Uri.parse("blah")); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int first = 0; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(first++ == 0) { assertEquals(Responses.buildResponseUri(0), uri); } return super.query(uri, projection, selection, selectionArgs, sortOrder); } }; setContext(context); startService(i); } /** * Tests that the upload service fails gracefully when given a campaign uri * @throws InterruptedException */ public void testUploadCampaign() throws InterruptedException { Intent i =new Intent(); i.setData(Responses.buildResponseUri(0)); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int first = 0; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(first++ == 0) { assertEquals(Responses.buildResponseUri(0), uri); } return super.query(uri, projection, selection, selectionArgs, sortOrder); } }; setContext(context); startService(i); } /** * Tests that the {@link UploadService} will try to upload a single * response given to it by the intent * @throws InterruptedException */ public void testUploadSingleResponse() throws InterruptedException { Intent i =new Intent(); i.setData(Responses.buildResponseUri(0)); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int first = 0; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(first++ == 0) { assertEquals(Responses.buildResponseUri(0), uri); } return super.query(uri, projection, selection, selectionArgs, sortOrder); } }; setContext(context); startService(i); } /** * Tests setup before the upload happens * <ui> * <li>Downloaded responses are not included</li> * <li>Uploaded responses are not included</li> * <li>Responses which are waiting for loction are not included</li> * </ui> * @throws InterruptedException */ public void testSetupResponsesForUpload() throws InterruptedException { Intent i =new Intent(); i.setData(Responses.CONTENT_URI); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int first = 0; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(first++ == 0) { assertEquals(Responses.CONTENT_URI, uri); assertTrue(selection.contains("response_status!=" + Response.STATUS_DOWNLOADED)); assertTrue(selection.contains("response_status!=" + Response.STATUS_UPLOADED)); assertTrue(selection.contains("response_status!=" + Response.STATUS_WAITING_FOR_LOCATION)); } return super.query(uri, projection, selection, selectionArgs, sortOrder); } }; setContext(context); startService(i); } /** * Tests that all the correct items are set to queued when the service starts * @throws InterruptedException */ public void testResponsesQueued() throws InterruptedException { Intent i =new Intent(); i.setData(Responses.CONTENT_URI); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int first = 0; @Override public int update(Uri uri, ContentValues values, String where, String[] selectionArgs) { if(first++ == 0) { assertEquals(Responses.CONTENT_URI, uri); //TODO: make sure this will only get responses which should be uploaded assertTrue(where.contains("response_status!=" + Response.STATUS_DOWNLOADED)); assertTrue(where.contains("response_status!=" + Response.STATUS_UPLOADED)); assertTrue(where.contains("response_status!=" + Response.STATUS_WAITING_FOR_LOCATION)); assertTrue(values.getAsLong(Responses.RESPONSE_STATUS) == Response.STATUS_QUEUED); } return super.update(uri, values, where, selectionArgs); } }; setContext(context); startService(i); } /** * As each response starts to upload it should be set to uploading * @throws InterruptedException */ public void testEachUploadingState() throws InterruptedException { Intent i =new Intent(); i.setData(Responses.CONTENT_URI); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int update = 0; @Override public int update(Uri uri, ContentValues values, String where, String[] selectionArgs) { if(update > 0 && update < 6) { assertResponse(0, uri, where); if(update%2==1) { // Set the first one to uploading assertTrue(values.getAsLong(Responses.RESPONSE_STATUS) == Response.STATUS_UPLOADING); } else { // then set it to uploaded assertTrue(values.getAsLong(Responses.RESPONSE_STATUS) == Response.STATUS_UPLOADED); } } update++; return super.update(uri, values, where, selectionArgs); } }; setContext(context); startService(i); } public void testCampaignDoesNotExistError() throws InterruptedException { errorTestHelper("0700", Response.STATUS_ERROR_CAMPAIGN_NO_EXIST, Campaign.STATUS_NO_EXIST); } public void testInvalidUserRoleError() throws InterruptedException { errorTestHelper("0707", Response.STATUS_ERROR_INVALID_USER_ROLE, Campaign.STATUS_INVALID_USER_ROLE); } public void testCampaignStopped() throws InterruptedException { errorTestHelper("0703", Response.STATUS_ERROR_CAMPAIGN_STOPPED, Campaign.STATUS_STOPPED); } public void testCampaignOutOfDate() throws InterruptedException { errorTestHelper("0710", Response.STATUS_ERROR_CAMPAIGN_OUT_OF_DATE, Campaign.STATUS_OUT_OF_DATE); } /** * Help test error codes cause the correct updates to the response and campaigns * @param code * @param responseStatusId * @param campaignStatusId * @throws InterruptedException */ private void errorTestHelper(final String code, final int responseStatusId, final int campaignStatusId) throws InterruptedException { Intent i =new Intent(); i.setData(Responses.CONTENT_URI); i.putExtra("upload_surveys", true); UploadServiceResponsesContext context = new UploadServiceResponsesContext(mContext, i.getData(), 5) { int update = 0; @Override public int update(Uri uri, ContentValues values, String where, String[] selectionArgs) { if(update == 2) { assertResponse(0, uri, where); assertTrue(values.getAsLong(Responses.RESPONSE_STATUS) == responseStatusId); } update++; return 1; } }; setContext(context); startService(i, new OhmageApi() { @Override public UploadResponse surveyUpload(String serverUrl, String username, String hashedPassword, String client, String campaignUrn, String campaignCreationTimestamp, String responseJson, ArrayList<MediaPart> photos) { return new UploadResponse(Result.FAILURE, new String[] { code }); } }); } @Override protected void setupService() { super.setupService(); getService().setOhmageApi(mOhmageApi); } @Override protected void startService(Intent intent) { super.startService(intent); // The WakefulIntentService requires that it gets a wake lock before it is started // The MockContext prevents it from actually starting the service since we already // took care of that WakefulIntentService.sendWakefulWork(new MockContext() { @Override public Context getApplicationContext() { return getService().getApplicationContext(); } @Override public ComponentName startService(Intent intent) { return null; } }, new Intent()); } /** * Sets the api the service uses to help with testing * @param intent * @param api */ protected void startService(Intent intent, OhmageApi api) { mOhmageApi = api; startService(intent); } /** * Context used for testing uploading survey responses. * This context makes it easy to block until the upload operation has completed * @author cketcham * */ private static class UploadServiceResponsesContext extends MockContentProviderContext { private final Uri mUri; private final Response[] mResponses; public UploadServiceResponsesContext(Context context, Uri uri, int count) { super(context); mUri = uri; mResponses = new Response[count]; } public UploadServiceResponsesContext(Context context, Uri uri, Response... responses) { super(context); mUri = uri; mResponses = responses; } @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { if(uri.compareTo(mUri) == 0) return new ResponseCursor(projection, mResponses); return super.query(uri, projection, selection, selectionArgs, sortOrder); } protected Uri getUri() { return mUri; } } /** * Asserts that this db operation applies to the response with the given id. * Might not be valid for certain cases of where. * @param id * @param uri * @param where */ protected void assertResponse(int id, Uri uri, String where) { assertTrue(Responses.buildResponseUri(id).equals(uri) || (Responses.CONTENT_URI.equals(uri) && where.contains(Responses._ID + "=" + id))); } /** * Asserts that the db operation applies to the given campaign urn. * Might not be valid for certain cases of where. * @param urn * @param uri * @param where */ protected void assertCampaign(String urn, Uri uri, String where) { assertTrue(Campaigns.buildCampaignUri(urn).equals(uri) || (Campaigns.CONTENT_URI.equals(uri) && where.contains(Campaigns.CAMPAIGN_URN + "='" + urn + "'"))); } }