New calendar sync adapter tests
* Created MockProvider that can be used for testing the results of ContentProviderOperation's for Calendar/Contacts (we can't use these within our mock contexts because we can't instantiate the provider classes within the Email package) * Wrote some unit tests for MockProvider * Use MockProvider to test addEvent, in particular how a user's attendee status is stored, depending on whether the event is new or updated Change-Id: I97f02d125eb7347726261e12ce70aadc539be1d4
This commit is contained in:
parent
6e47262274
commit
f115d31ae8
|
@ -154,7 +154,7 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
|
|||
private long mCalendarId = -1;
|
||||
private String mCalendarIdString;
|
||||
private String[] mCalendarIdArgument;
|
||||
private String mEmailAddress;
|
||||
/*package*/ String mEmailAddress;
|
||||
|
||||
private ArrayList<Long> mDeletedIdList = new ArrayList<Long>();
|
||||
private ArrayList<Long> mUploadedIdList = new ArrayList<Long>();
|
||||
|
@ -410,11 +410,11 @@ public class CalendarSyncAdapter extends AbstractSyncAdapter {
|
|||
Cursor c = getServerIdCursor(serverId);
|
||||
long id = -1;
|
||||
try {
|
||||
if (c.moveToFirst()) {
|
||||
if (c != null && c.moveToFirst()) {
|
||||
id = c.getLong(0);
|
||||
}
|
||||
} finally {
|
||||
c.close();
|
||||
if (c != null) c.close();
|
||||
}
|
||||
if (id > 0) {
|
||||
// DTSTAMP can come first, and we simply need to track it
|
||||
|
|
|
@ -18,9 +18,14 @@ package com.android.exchange.adapter;
|
|||
|
||||
import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations;
|
||||
import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser;
|
||||
import com.android.exchange.provider.MockProvider;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentValues;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.os.RemoteException;
|
||||
import android.provider.Calendar.Attendees;
|
||||
import android.provider.Calendar.Events;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
|
@ -35,6 +40,21 @@ import java.util.TimeZone;
|
|||
*/
|
||||
|
||||
public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> {
|
||||
private static final String[] ATTENDEE_PROJECTION = new String[] {Attendees.ATTENDEE_EMAIL,
|
||||
Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_STATUS};
|
||||
private static final int ATTENDEE_EMAIL = 0;
|
||||
private static final int ATTENDEE_NAME = 1;
|
||||
private static final int ATTENDEE_STATUS = 2;
|
||||
|
||||
private static final String SINGLE_ATTENDEE_EMAIL = "attendee@host.com";
|
||||
private static final String SINGLE_ATTENDEE_NAME = "Bill Attendee";
|
||||
|
||||
// This is the US/Pacific time zone as a base64-encoded TIME_ZONE_INFORMATION structure, as
|
||||
// it would appear coming from an Exchange server
|
||||
private static final String TEST_TIME_ZONE = "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
|
||||
"GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
|
||||
"QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==";
|
||||
|
||||
public CalendarSyncAdapterTests() {
|
||||
super();
|
||||
|
@ -189,6 +209,15 @@ public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAd
|
|||
}
|
||||
}
|
||||
|
||||
private void addAttendeeToSerializer(Serializer s, String email, String name)
|
||||
throws IOException {
|
||||
s.start(Tags.CALENDAR_ATTENDEE);
|
||||
s.data(Tags.CALENDAR_ATTENDEE_EMAIL, email);
|
||||
s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1");
|
||||
s.data(Tags.CALENDAR_ATTENDEE_NAME, name);
|
||||
s.end();
|
||||
}
|
||||
|
||||
private int countInsertOperationsForTable(CalendarOperations ops, String tableName) {
|
||||
int cnt = 0;
|
||||
for (ContentProviderOperation op: ops) {
|
||||
|
@ -201,51 +230,79 @@ public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAd
|
|||
return cnt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that there is no way to access the ContentValues inside of a ContentProviderOperation,
|
||||
* which limits the extent to which we can test the result of parsing events. We can count
|
||||
* the number of objects to be created, but we can't examine them.
|
||||
*/
|
||||
// TODO Try to convince fredq to allow access to the ContentValues of a CPO
|
||||
class TestEvent extends Serializer {
|
||||
CalendarSyncAdapter mAdapter;
|
||||
EasCalendarSyncParser mParser;
|
||||
Serializer mSerializer;
|
||||
|
||||
TestEvent() throws IOException {
|
||||
super(false);
|
||||
mAdapter = getTestSyncAdapter(CalendarSyncAdapter.class);
|
||||
mParser = mAdapter.new EasCalendarSyncParser(getTestInputStream(), mAdapter);
|
||||
}
|
||||
|
||||
void setUserEmailAddress(String addr) {
|
||||
mAdapter.mAccount.mEmailAddress = addr;
|
||||
mAdapter.mEmailAddress = addr;
|
||||
}
|
||||
|
||||
EasCalendarSyncParser getParser() throws IOException {
|
||||
// Set up our parser's input and eat the initial tag
|
||||
mParser.resetInput(new ByteArrayInputStream(toByteArray()));
|
||||
mParser.nextTag(0);
|
||||
return mParser;
|
||||
}
|
||||
|
||||
// setupPreAttendees and setupPostAttendees initialize calendar data in the order in which
|
||||
// they would appear in an actual EAS session. Between these two calls, we initialize
|
||||
// attendee data, which varies between the following tests
|
||||
TestEvent setupPreAttendees() throws IOException {
|
||||
start(Tags.SYNC_APPLICATION_DATA);
|
||||
data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
|
||||
data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
|
||||
data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
|
||||
data(Tags.CALENDAR_SUBJECT, "Documentation");
|
||||
data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
|
||||
data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
|
||||
data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
|
||||
return this;
|
||||
}
|
||||
|
||||
TestEvent setupPostAttendees()throws IOException {
|
||||
data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
|
||||
data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
|
||||
start(Tags.BASE_BODY);
|
||||
data(Tags.BASE_BODY_PREFERENCE, "1");
|
||||
data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
|
||||
data(Tags.BASE_DATA,
|
||||
"This is the event description; we should probably make it longer");
|
||||
end(); // BASE_BODY
|
||||
start(Tags.CALENDAR_RECURRENCE);
|
||||
data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); // weekly
|
||||
data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1");
|
||||
data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, "10");
|
||||
data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "12"); // tue, wed
|
||||
data(Tags.CALENDAR_RECURRENCE_UNTIL, "2005-04-14T00:00:00.000Z");
|
||||
end(); // CALENDAR_RECURRENCE
|
||||
data(Tags.CALENDAR_SENSITIVITY, "0");
|
||||
data(Tags.CALENDAR_BUSY_STATUS, "2");
|
||||
data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
|
||||
data(Tags.CALENDAR_MEETING_STATUS, "3");
|
||||
data(Tags.BASE_NATIVE_BODY_TYPE, "3");
|
||||
end().done(); // SYNC_APPLICATION_DATA
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
public void testAddEvent() throws IOException {
|
||||
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
|
||||
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
|
||||
|
||||
// Set up an input stream with new event data
|
||||
Serializer s = new Serializer(false);
|
||||
s.start(Tags.SYNC_APPLICATION_DATA);
|
||||
s.data(Tags.CALENDAR_TIME_ZONE, "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
|
||||
"GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
|
||||
"QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==");
|
||||
s.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
|
||||
s.data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
|
||||
s.data(Tags.CALENDAR_SUBJECT, "Documentation");
|
||||
s.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
|
||||
s.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
|
||||
s.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
|
||||
s.start(Tags.CALENDAR_ATTENDEES);
|
||||
addAttendeesToSerializer(s, 10);
|
||||
s.end(); // CALENDAR_ATTENDEES
|
||||
s.data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
|
||||
s.data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
|
||||
s.start(Tags.BASE_BODY);
|
||||
s.data(Tags.BASE_BODY_PREFERENCE, "1");
|
||||
s.data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
|
||||
s.data(Tags.BASE_DATA, "This is the event description; we should probably make it longer");
|
||||
s.end(); // BASE_BODY
|
||||
s.data(Tags.CALENDAR_SENSITIVITY, "0");
|
||||
s.data(Tags.CALENDAR_BUSY_STATUS, "2");
|
||||
s.data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
|
||||
s.data(Tags.CALENDAR_MEETING_STATUS, "3");
|
||||
s.data(Tags.BASE_NATIVE_BODY_TYPE, "3");
|
||||
s.end().done(); // SYNC_APPLICATION_DATA
|
||||
|
||||
// Set up our parser's input and eat the initial tag
|
||||
byte[] bytes = s.toByteArray();
|
||||
p.resetInput(new ByteArrayInputStream(bytes));
|
||||
p.nextTag(0);
|
||||
TestEvent event = new TestEvent();
|
||||
event.setupPreAttendees();
|
||||
event.start(Tags.CALENDAR_ATTENDEES);
|
||||
addAttendeesToSerializer(event, 10);
|
||||
event.end(); // CALENDAR_ATTENDEES
|
||||
event.setupPostAttendees();
|
||||
|
||||
EasCalendarSyncParser p = event.getParser();
|
||||
p.addEvent(p.mOps, "1:1", false);
|
||||
// There should be 1 event
|
||||
assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
|
||||
|
@ -255,45 +312,37 @@ public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAd
|
|||
assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties"));
|
||||
}
|
||||
|
||||
public void testAddEventIllegal() throws IOException {
|
||||
// We don't send a start time; the event is illegal and nothing should be added
|
||||
TestEvent event = new TestEvent();
|
||||
event.start(Tags.SYNC_APPLICATION_DATA);
|
||||
event.data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE);
|
||||
event.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
|
||||
event.data(Tags.CALENDAR_SUBJECT, "Documentation");
|
||||
event.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
|
||||
event.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
|
||||
event.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
|
||||
event.start(Tags.CALENDAR_ATTENDEES);
|
||||
addAttendeesToSerializer(event, 10);
|
||||
event.end(); // CALENDAR_ATTENDEES
|
||||
event.setupPostAttendees();
|
||||
|
||||
EasCalendarSyncParser p = event.getParser();
|
||||
p.addEvent(p.mOps, "1:1", false);
|
||||
assertEquals(0, countInsertOperationsForTable(p.mOps, "events"));
|
||||
assertEquals(0, countInsertOperationsForTable(p.mOps, "attendees"));
|
||||
assertEquals(0, countInsertOperationsForTable(p.mOps, "extendedproperties"));
|
||||
}
|
||||
|
||||
public void testAddEventRedactedAttendees() throws IOException {
|
||||
CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class);
|
||||
EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter);
|
||||
|
||||
// Set up an input stream with new event data
|
||||
Serializer s = new Serializer(false);
|
||||
s.start(Tags.SYNC_APPLICATION_DATA);
|
||||
s.data(Tags.CALENDAR_TIME_ZONE, "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" +
|
||||
"GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" +
|
||||
"QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" +
|
||||
"AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==");
|
||||
s.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z");
|
||||
s.data(Tags.CALENDAR_START_TIME, "20100518T220000Z");
|
||||
s.data(Tags.CALENDAR_SUBJECT, "Documentation");
|
||||
s.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85");
|
||||
s.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas");
|
||||
s.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com");
|
||||
s.start(Tags.CALENDAR_ATTENDEES);
|
||||
addAttendeesToSerializer(s, 100);
|
||||
s.end(); // CALENDAR_ATTENDEES
|
||||
s.data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)");
|
||||
s.data(Tags.CALENDAR_END_TIME, "20100518T223000Z");
|
||||
s.start(Tags.BASE_BODY);
|
||||
s.data(Tags.BASE_BODY_PREFERENCE, "1");
|
||||
s.data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser
|
||||
s.data(Tags.BASE_DATA, "This is the event description; we should probably make it longer");
|
||||
s.end(); // BASE_BODY
|
||||
s.data(Tags.CALENDAR_SENSITIVITY, "0");
|
||||
s.data(Tags.CALENDAR_BUSY_STATUS, "2");
|
||||
s.data(Tags.CALENDAR_ALL_DAY_EVENT, "0");
|
||||
s.data(Tags.CALENDAR_MEETING_STATUS, "3");
|
||||
s.data(Tags.BASE_NATIVE_BODY_TYPE, "3");
|
||||
s.end().done(); // SYNC_APPLICATION_DATA
|
||||
|
||||
// Set up our parser's input and eat the initial tag
|
||||
byte[] bytes = s.toByteArray();
|
||||
p.resetInput(new ByteArrayInputStream(bytes));
|
||||
p.nextTag(0);
|
||||
TestEvent event = new TestEvent();
|
||||
event.setupPreAttendees();
|
||||
event.start(Tags.CALENDAR_ATTENDEES);
|
||||
addAttendeesToSerializer(event, 100);
|
||||
event.end(); // CALENDAR_ATTENDEES
|
||||
event.setupPostAttendees();
|
||||
|
||||
EasCalendarSyncParser p = event.getParser();
|
||||
p.addEvent(p.mOps, "1:1", false);
|
||||
// There should be 1 event
|
||||
assertEquals(1, countInsertOperationsForTable(p.mOps, "events"));
|
||||
|
@ -302,4 +351,73 @@ public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAd
|
|||
// dtstamp, meeting status, and attendees redacted
|
||||
assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup for the following three tests, which check attendee status of an added event
|
||||
* @param userEmail the email address of the user
|
||||
* @param update whether or not the event is an update (rather than new)
|
||||
* @return a Cursor to the Attendee records added to our MockProvider
|
||||
* @throws IOException
|
||||
* @throws RemoteException
|
||||
* @throws OperationApplicationException
|
||||
*/
|
||||
private Cursor setupAddEventOneAttendee(String userEmail, boolean update)
|
||||
throws IOException, RemoteException, OperationApplicationException {
|
||||
TestEvent event = new TestEvent();
|
||||
event.setupPreAttendees();
|
||||
event.start(Tags.CALENDAR_ATTENDEES);
|
||||
addAttendeeToSerializer(event, SINGLE_ATTENDEE_EMAIL, SINGLE_ATTENDEE_NAME);
|
||||
event.setUserEmailAddress(userEmail);
|
||||
event.end(); // CALENDAR_ATTENDEES
|
||||
event.setupPostAttendees();
|
||||
|
||||
EasCalendarSyncParser p = event.getParser();
|
||||
p.addEvent(p.mOps, "1:1", update);
|
||||
// Send the CPO's to the mock provider
|
||||
mMockResolver.applyBatch(MockProvider.AUTHORITY, p.mOps);
|
||||
return mMockResolver.query(MockProvider.uri(Attendees.CONTENT_URI), ATTENDEE_PROJECTION,
|
||||
null, null, null);
|
||||
}
|
||||
|
||||
public void testAddEventOneAttendee() throws IOException, RemoteException,
|
||||
OperationApplicationException {
|
||||
Cursor c = setupAddEventOneAttendee("foo@bar.com", false);
|
||||
assertEquals(2, c.getCount());
|
||||
// The organizer should be "accepted", the unknown attendee "none"
|
||||
while (c.moveToNext()) {
|
||||
if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
|
||||
assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
|
||||
} else {
|
||||
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testAddEventSelfAttendee() throws IOException, RemoteException,
|
||||
OperationApplicationException {
|
||||
Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, false);
|
||||
// The organizer should be "accepted", and our user/attendee should be "done" even though
|
||||
// the busy status = 2 (because we can't tell from a status of 2 on new events)
|
||||
while (c.moveToNext()) {
|
||||
if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
|
||||
assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS));
|
||||
} else {
|
||||
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void testAddEventSelfAttendeeUpdate() throws IOException, RemoteException,
|
||||
OperationApplicationException {
|
||||
Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, true);
|
||||
// The organizer should be "accepted", and our user/attendee should be "accepted" (because
|
||||
// busy status = 2 and this is an update
|
||||
while (c.moveToNext()) {
|
||||
if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) {
|
||||
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
|
||||
} else {
|
||||
assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,10 +21,11 @@ import com.android.email.provider.EmailContent.Account;
|
|||
import com.android.email.provider.EmailContent.Mailbox;
|
||||
import com.android.exchange.EasSyncService;
|
||||
import com.android.exchange.adapter.EmailSyncAdapter.EasEmailSyncParser;
|
||||
import com.android.exchange.provider.MockProvider;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.test.ProviderTestCase2;
|
||||
import android.test.mock.MockContentResolver;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
|
@ -35,7 +36,7 @@ public class SyncAdapterTestCase<T extends AbstractSyncAdapter>
|
|||
extends ProviderTestCase2<EmailProvider> {
|
||||
EmailProvider mProvider;
|
||||
Context mMockContext;
|
||||
ContentResolver mMockResolver;
|
||||
MockContentResolver mMockResolver;
|
||||
Mailbox mMailbox;
|
||||
Account mAccount;
|
||||
EmailSyncAdapter mSyncAdapter;
|
||||
|
@ -49,7 +50,8 @@ public class SyncAdapterTestCase<T extends AbstractSyncAdapter>
|
|||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMockContext = getMockContext();
|
||||
mMockResolver = mMockContext.getContentResolver();
|
||||
mMockResolver = (MockContentResolver)mMockContext.getContentResolver();
|
||||
mMockResolver.addProvider(MockProvider.AUTHORITY, new MockProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* 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.android.exchange.provider;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.UriMatcher;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
import java.util.Map.Entry;
|
||||
|
||||
/**
|
||||
* MockProvider is a ContentProvider that can be used to simulate the storage and retrieval of
|
||||
* records from any ContentProvider, even if that ContentProvider does not exist in the caller's
|
||||
* package. It is specifically designed to enable testing of sync adapters that create
|
||||
* ContentProviderOperations (CPOs) that are then executed using ContentResolver.applyBatch()
|
||||
*
|
||||
* Why is this useful? Because we can't instantiate CalendarProvider or ContactsProvider from our
|
||||
* package, as required by MockContentResolver.addProvider()
|
||||
*
|
||||
* Usage:
|
||||
* ContentResolver.applyBatch(MockProvider.AUTHORITY, batch) will cause the CPOs to be executed,
|
||||
* returning an array of ContentProviderResult; in the case of inserts, the result will include
|
||||
* a Uri that can be used via query(). Note that the CPOs in the batch can contain references
|
||||
* to any authority.
|
||||
*
|
||||
* query() does not allow non-null selection, selectionArgs, or sortOrder arguments; the
|
||||
* presence of these will result in an UnsupportedOperationException
|
||||
*
|
||||
* insert() acts as expected, returning a Uri that can be directly used in a query
|
||||
*
|
||||
* delete() and update() do not allow non-null selection or selectionArgs arguments; the
|
||||
* presence of these will result in an UnsupportedOperationException
|
||||
*
|
||||
* NOTE: When using any operation other than applyBatch, the Uri to be used must be created
|
||||
* with MockProvider.uri(yourUri). This guarantees that the operation is sent to MockProvider
|
||||
*
|
||||
* NOTE: MockProvider only simulates direct storage/retrieval of rows; it does not (and can not)
|
||||
* simulate other actions (e.g. creation of ancillary data) that the actual provider might
|
||||
* perform
|
||||
*
|
||||
* NOTE: See MockProviderTests for usage examples
|
||||
**/
|
||||
public class MockProvider extends ContentProvider {
|
||||
public static final String AUTHORITY = "com.android.exchange.mock.provider";
|
||||
/*package*/ static final UriMatcher sURIMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
/*package*/ static final int TABLE = 100;
|
||||
/*package*/ static final int RECORD = 101;
|
||||
|
||||
public static final String ID_COLUMN = "_id";
|
||||
|
||||
// We'll store our values here
|
||||
private HashMap<String, ContentValues> mMockStore = new HashMap<String, ContentValues>();
|
||||
// And we'll generate new id's from here
|
||||
long mMockId = 1;
|
||||
|
||||
/**
|
||||
* Create a Uri for MockProvider from a given Uri
|
||||
* @param uri the Uri from which the MockProvider Uri will be created
|
||||
* @return a Uri that can be used with MockProvider
|
||||
*/
|
||||
public static Uri uri(Uri uri) {
|
||||
return new Uri.Builder().scheme("content").authority(AUTHORITY)
|
||||
.path(uri.getPath().substring(1)).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int delete(Uri uri, String selection, String[] selectionArgs) {
|
||||
if (selection != null || selectionArgs != null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
String path = uri.getPath();
|
||||
if (mMockStore.containsKey(path)) {
|
||||
mMockStore.remove(path);
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(Uri uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri insert(Uri uri, ContentValues values) {
|
||||
// Remove the leading slash
|
||||
String table = uri.getPath().substring(1);
|
||||
long id = mMockId++;
|
||||
Uri newUri = new Uri.Builder().scheme("content").authority(AUTHORITY).path(table)
|
||||
.appendPath(Long.toString(id)).build();
|
||||
// Remember to store the _id
|
||||
values.put(ID_COLUMN, id);
|
||||
mMockStore.put(newUri.getPath(), values);
|
||||
int match = sURIMatcher.match(uri);
|
||||
if (match == UriMatcher.NO_MATCH) {
|
||||
sURIMatcher.addURI(AUTHORITY, table, TABLE);
|
||||
sURIMatcher.addURI(AUTHORITY, table + "/#", RECORD);
|
||||
}
|
||||
return newUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
|
||||
String sortOrder) {
|
||||
if (selection != null || selectionArgs != null || sortOrder != null || projection == null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
final int match = sURIMatcher.match(uri(uri));
|
||||
ArrayList<ContentValues> valuesList = new ArrayList<ContentValues>();
|
||||
switch(match) {
|
||||
case TABLE:
|
||||
Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
|
||||
String prefix = uri.getPath() + "/";
|
||||
for (Entry<String, ContentValues> entry: entrySet) {
|
||||
if (entry.getKey().startsWith(prefix)) {
|
||||
valuesList.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RECORD:
|
||||
ContentValues values = mMockStore.get(uri.getPath());
|
||||
if (values != null) {
|
||||
valuesList.add(values);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI " + uri);
|
||||
}
|
||||
MatrixCursor cursor = new MatrixCursor(projection, 1);
|
||||
for (ContentValues cv: valuesList) {
|
||||
Object[] rowValues = new Object[projection.length];
|
||||
int i = 0;
|
||||
for (String column: projection) {
|
||||
rowValues[i++] = cv.get(column);
|
||||
}
|
||||
cursor.addRow(rowValues);
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(Uri uri, ContentValues newValues, String selection, String[] selectionArgs) {
|
||||
if (selection != null || selectionArgs != null) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
final int match = sURIMatcher.match(uri(uri));
|
||||
ArrayList<ContentValues> updateValuesList = new ArrayList<ContentValues>();
|
||||
String path = uri.getPath();
|
||||
switch(match) {
|
||||
case TABLE:
|
||||
Set<Entry<String, ContentValues>> entrySet = mMockStore.entrySet();
|
||||
String prefix = path + "/";
|
||||
for (Entry<String, ContentValues> entry: entrySet) {
|
||||
if (entry.getKey().startsWith(prefix)) {
|
||||
updateValuesList.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
break;
|
||||
case RECORD:
|
||||
ContentValues cv = mMockStore.get(path);
|
||||
if (cv != null) {
|
||||
updateValuesList.add(cv);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown URI " + uri);
|
||||
}
|
||||
Set<Entry<String, Object>> newValuesSet = newValues.valueSet();
|
||||
for (Entry<String, Object> entry: newValuesSet) {
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
for (ContentValues targetValues: updateValuesList) {
|
||||
if (value instanceof Integer) {
|
||||
targetValues.put(key, (Integer)value);
|
||||
} else if (value instanceof Long) {
|
||||
targetValues.put(key, (Long)value);
|
||||
} else if (value instanceof String) {
|
||||
targetValues.put(key, (String)value);
|
||||
} else if (value instanceof Boolean) {
|
||||
targetValues.put(key, (Boolean)value);
|
||||
} else {
|
||||
throw new IllegalArgumentException();
|
||||
}
|
||||
}
|
||||
}
|
||||
for (ContentValues targetValues: updateValuesList) {
|
||||
mMockStore.put(path + "/" + targetValues.getAsLong(ID_COLUMN), targetValues);
|
||||
}
|
||||
return updateValuesList.size();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* Copyright (C) 2010 The Android Open Source Project
|
||||
*
|
||||
* 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.android.exchange.provider;
|
||||
|
||||
import android.content.ContentProviderOperation;
|
||||
import android.content.ContentProviderResult;
|
||||
import android.content.ContentUris;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.OperationApplicationException;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.RemoteException;
|
||||
import android.test.ProviderTestCase2;
|
||||
import android.test.mock.MockContentResolver;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* You can run this entire test case with:
|
||||
* runtest -c com.android.exchange.provider.MockProviderTests email
|
||||
*/
|
||||
public class MockProviderTests extends ProviderTestCase2<MockProvider> {
|
||||
Context mMockContext;
|
||||
MockContentResolver mMockResolver;
|
||||
|
||||
private static final String CANHAZ_AUTHORITY = "com.android.canhaz";
|
||||
private static final String PONY_TABLE = "pony";
|
||||
private static final String PONY_COLUMN_NAME = "name";
|
||||
private static final String PONY_COLUMN_TYPE = "type";
|
||||
private static final String PONY_COLUMN_LEGS= "legs";
|
||||
private static final String PONY_COLUMN_CAN_RIDE = "canRide";
|
||||
private static final String[] PONY_PROJECTION = {MockProvider.ID_COLUMN, PONY_COLUMN_NAME,
|
||||
PONY_COLUMN_TYPE, PONY_COLUMN_LEGS, PONY_COLUMN_CAN_RIDE};
|
||||
private static final int PONY_ID = 0;
|
||||
private static final int PONY_NAME = 1;
|
||||
private static final int PONY_TYPE = 2;
|
||||
private static final int PONY_LEGS = 3;
|
||||
private static final int PONY_CAN_RIDE = 4;
|
||||
|
||||
public MockProviderTests() {
|
||||
super(MockProvider.class, MockProvider.AUTHORITY);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
mMockContext = getMockContext();
|
||||
mMockResolver = (MockContentResolver)mMockContext.getContentResolver();
|
||||
mMockResolver.addProvider(CANHAZ_AUTHORITY, new MockProvider());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tearDown() throws Exception {
|
||||
super.tearDown();
|
||||
}
|
||||
|
||||
private ContentValues ponyValues(String name, String type, int legs, boolean canRide) {
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(PONY_COLUMN_NAME, name);
|
||||
cv.put(PONY_COLUMN_TYPE, type);
|
||||
cv.put(PONY_COLUMN_LEGS, legs);
|
||||
cv.put(PONY_COLUMN_CAN_RIDE, canRide ? 1 : 0);
|
||||
return cv;
|
||||
}
|
||||
|
||||
private ContentProviderResult[] setupPonies() throws RemoteException,
|
||||
OperationApplicationException {
|
||||
// The Uri is content://com.android.canhaz/pony
|
||||
Uri uri = new Uri.Builder().scheme("content").authority(CANHAZ_AUTHORITY)
|
||||
.path(PONY_TABLE).build();
|
||||
// Our array of CPO's to be used with applyBatch
|
||||
ArrayList<ContentProviderOperation> ops = new ArrayList<ContentProviderOperation>();
|
||||
|
||||
// Insert two ponies
|
||||
ContentValues pony1 = ponyValues("Flicka", "wayward", 4, true);
|
||||
ops.add(ContentProviderOperation.newInsert(uri).withValues(pony1).build());
|
||||
ContentValues pony2 = ponyValues("Elise", "dastardly", 3, false);
|
||||
ops.add(ContentProviderOperation.newInsert(uri).withValues(pony2).build());
|
||||
// Apply the batch with one insert operation
|
||||
return mMockResolver.applyBatch(MockProvider.AUTHORITY, ops);
|
||||
}
|
||||
|
||||
private Uri getPonyUri() {
|
||||
return new Uri.Builder().scheme("content").authority(CANHAZ_AUTHORITY)
|
||||
.path(PONY_TABLE).build();
|
||||
}
|
||||
|
||||
public void testInsertQueryandDelete() throws RemoteException, OperationApplicationException {
|
||||
// The Uri is content://com.android.canhaz/pony
|
||||
ContentProviderResult[] results = setupPonies();
|
||||
Uri uri = getPonyUri();
|
||||
|
||||
// Check the results
|
||||
assertNotNull(results);
|
||||
assertEquals(2, results.length);
|
||||
// Make sure that we've created matcher entries for pony and pony/#
|
||||
assertEquals(MockProvider.TABLE, MockProvider.sURIMatcher.match(MockProvider.uri(uri)));
|
||||
assertEquals(MockProvider.RECORD,
|
||||
MockProvider.sURIMatcher.match(MockProvider.uri(results[0].uri)));
|
||||
Cursor c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
|
||||
assertNotNull(c);
|
||||
assertEquals(2, c.getCount());
|
||||
long eliseId = -1;
|
||||
long flickaId = -1;
|
||||
while (c.moveToNext()) {
|
||||
String name = c.getString(PONY_NAME);
|
||||
if ("Flicka".equals(name)) {
|
||||
assertEquals("Flicka", c.getString(PONY_NAME));
|
||||
assertEquals("wayward", c.getString(PONY_TYPE));
|
||||
assertEquals(4, c.getInt(PONY_LEGS));
|
||||
assertEquals(1, c.getInt(PONY_CAN_RIDE));
|
||||
flickaId = c.getLong(PONY_ID);
|
||||
} else if ("Elise".equals(name)) {
|
||||
assertEquals("dastardly", c.getString(PONY_TYPE));
|
||||
assertEquals(3, c.getInt(PONY_LEGS));
|
||||
assertEquals(0, c.getInt(PONY_CAN_RIDE));
|
||||
eliseId = c.getLong(PONY_ID);
|
||||
} else {
|
||||
fail("Wrong record: " + name);
|
||||
}
|
||||
}
|
||||
|
||||
// eliseId and flickaId should have been set
|
||||
assertNotSame(-1, eliseId);
|
||||
assertNotSame(-1, flickaId);
|
||||
// Delete the elise record
|
||||
assertEquals(1, mMockResolver.delete(ContentUris.withAppendedId(MockProvider.uri(uri),
|
||||
eliseId), null, null));
|
||||
c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
|
||||
assertNotNull(c);
|
||||
// There should be one left (Flicka)
|
||||
assertEquals(1, c.getCount());
|
||||
assertTrue(c.moveToNext());
|
||||
assertEquals("Flicka", c.getString(PONY_NAME));
|
||||
}
|
||||
|
||||
public void testUpdate() throws RemoteException, OperationApplicationException {
|
||||
// The Uri is content://com.android.canhaz/pony
|
||||
Uri uri = getPonyUri();
|
||||
setupPonies();
|
||||
Cursor c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
|
||||
assertNotNull(c);
|
||||
assertEquals(2, c.getCount());
|
||||
// Give all the ponies 5 legs
|
||||
ContentValues cv = new ContentValues();
|
||||
cv.put(PONY_COLUMN_LEGS, 5);
|
||||
assertEquals(2, mMockResolver.update(MockProvider.uri(uri), cv, null, null));
|
||||
c = mMockResolver.query(MockProvider.uri(uri), PONY_PROJECTION, null, null, null);
|
||||
assertNotNull(c);
|
||||
// We should still have two records, and each should have 5 legs, but otherwise be the same
|
||||
assertEquals(2, c.getCount());
|
||||
long eliseId = -1;
|
||||
long flickaId = -1;
|
||||
while (c.moveToNext()) {
|
||||
String name = c.getString(PONY_NAME);
|
||||
if ("Flicka".equals(name)) {
|
||||
assertEquals("Flicka", c.getString(PONY_NAME));
|
||||
assertEquals("wayward", c.getString(PONY_TYPE));
|
||||
assertEquals(5, c.getInt(PONY_LEGS));
|
||||
assertEquals(1, c.getInt(PONY_CAN_RIDE));
|
||||
flickaId = c.getLong(PONY_ID);
|
||||
} else if ("Elise".equals(name)) {
|
||||
assertEquals("dastardly", c.getString(PONY_TYPE));
|
||||
assertEquals(5, c.getInt(PONY_LEGS));
|
||||
assertEquals(0, c.getInt(PONY_CAN_RIDE));
|
||||
eliseId = c.getLong(PONY_ID);
|
||||
} else {
|
||||
fail("Wrong record: " + name);
|
||||
}
|
||||
}
|
||||
// eliseId and flickaId should have been set
|
||||
assertNotSame(-1, eliseId);
|
||||
assertNotSame(-1, flickaId);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue