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:
Marc Blank 2010-07-02 18:00:48 -07:00
parent 6e47262274
commit f115d31ae8
5 changed files with 614 additions and 86 deletions

View File

@ -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

View File

@ -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));
}
}
}
}

View File

@ -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

View File

@ -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();
}
}

View File

@ -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);
}
}