CalendarSyncAdapterTests.java revision c8e4352ea6cfa67f15140512e84af8ccede222d2
1/* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.exchange.adapter; 18 19import com.android.exchange.adapter.CalendarSyncAdapter.CalendarOperations; 20import com.android.exchange.adapter.CalendarSyncAdapter.EasCalendarSyncParser; 21import com.android.exchange.provider.MockProvider; 22 23import android.content.ContentProviderOperation; 24import android.content.ContentValues; 25import android.content.Context; 26import android.content.OperationApplicationException; 27import android.content.res.Resources; 28import android.database.Cursor; 29import android.os.RemoteException; 30import android.provider.Calendar.Attendees; 31import android.provider.Calendar.Events; 32import android.test.IsolatedContext; 33import android.test.RenamingDelegatingContext; 34import android.test.mock.MockContentResolver; 35import android.test.mock.MockContext; 36 37import java.io.ByteArrayInputStream; 38import java.io.File; 39import java.io.IOException; 40import java.util.GregorianCalendar; 41import java.util.List; 42import java.util.TimeZone; 43 44/** 45 * You can run this entire test case with: 46 * runtest -c com.android.exchange.adapter.CalendarSyncAdapterTests exchange 47 */ 48 49public class CalendarSyncAdapterTests extends SyncAdapterTestCase<CalendarSyncAdapter> { 50 private static final String[] ATTENDEE_PROJECTION = new String[] {Attendees.ATTENDEE_EMAIL, 51 Attendees.ATTENDEE_NAME, Attendees.ATTENDEE_STATUS}; 52 private static final int ATTENDEE_EMAIL = 0; 53 private static final int ATTENDEE_NAME = 1; 54 private static final int ATTENDEE_STATUS = 2; 55 56 private static final String SINGLE_ATTENDEE_EMAIL = "attendee@host.com"; 57 private static final String SINGLE_ATTENDEE_NAME = "Bill Attendee"; 58 59 private Context mMockContext; 60 private MockContentResolver mMockResolver; 61 62 // This is the US/Pacific time zone as a base64-encoded TIME_ZONE_INFORMATION structure, as 63 // it would appear coming from an Exchange server 64 private static final String TEST_TIME_ZONE = "4AEAAFAAYQBjAGkAZgBpAGMAIABTAHQAYQBuAGQAYQByA" + 65 "GQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAFAAY" + 66 "QBjAGkAZgBpAGMAIABEAGEAeQBsAGkAZwBoAHQAIABUAGkAbQBlAAAAAAAAAAAAAAAAAAAAAAAAA" + 67 "AAAAAAAAAMAAAACAAIAAAAAAAAAxP///w=="; 68 69 private class MockContext2 extends MockContext { 70 71 @Override 72 public Resources getResources() { 73 return getContext().getResources(); 74 } 75 76 @Override 77 public File getDir(String name, int mode) { 78 // name the directory so the directory will be separated from 79 // one created through the regular Context 80 return getContext().getDir("mockcontext2_" + name, mode); 81 } 82 83 @Override 84 public Context getApplicationContext() { 85 return this; 86 } 87 } 88 89 @Override 90 public void setUp() throws Exception { 91 super.setUp(); 92 93 mMockResolver = new MockContentResolver(); 94 final String filenamePrefix = "test."; 95 RenamingDelegatingContext targetContextWrapper = new 96 RenamingDelegatingContext( 97 new MockContext2(), // The context that most methods are delegated to 98 getContext(), // The context that file methods are delegated to 99 filenamePrefix); 100 mMockContext = new IsolatedContext(mMockResolver, targetContextWrapper); 101 mMockResolver.addProvider(MockProvider.AUTHORITY, new MockProvider(mMockContext)); 102 } 103 104 public CalendarSyncAdapterTests() { 105 super(); 106 } 107 108 public void testSetTimeRelatedValues_NonRecurring() throws IOException { 109 CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class); 110 EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter); 111 ContentValues cv = new ContentValues(); 112 // Basic, one-time meeting lasting an hour 113 GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30); 114 Long startTime = startCalendar.getTimeInMillis(); 115 GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30); 116 Long endTime = endCalendar.getTimeInMillis(); 117 118 p.setTimeRelatedValues(cv, startTime, endTime, 0); 119 assertNull(cv.getAsInteger(Events.DURATION)); 120 assertEquals(startTime, cv.getAsLong(Events.DTSTART)); 121 assertEquals(endTime, cv.getAsLong(Events.DTEND)); 122 assertEquals(endTime, cv.getAsLong(Events.LAST_DATE)); 123 assertNull(cv.getAsString(Events.EVENT_TIMEZONE)); 124 } 125 126 public void testSetTimeRelatedValues_Recurring() throws IOException { 127 CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class); 128 EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter); 129 ContentValues cv = new ContentValues(); 130 // Recurring meeting lasting an hour 131 GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30); 132 Long startTime = startCalendar.getTimeInMillis(); 133 GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 10, 9, 30); 134 Long endTime = endCalendar.getTimeInMillis(); 135 cv.put(Events.RRULE, "FREQ=DAILY"); 136 p.setTimeRelatedValues(cv, startTime, endTime, 0); 137 assertEquals("P60M", cv.getAsString(Events.DURATION)); 138 assertEquals(startTime, cv.getAsLong(Events.DTSTART)); 139 assertNull(cv.getAsLong(Events.DTEND)); 140 assertNull(cv.getAsLong(Events.LAST_DATE)); 141 assertNull(cv.getAsString(Events.EVENT_TIMEZONE)); 142 } 143 144 public void testSetTimeRelatedValues_AllDay() throws IOException { 145 CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class); 146 EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter); 147 ContentValues cv = new ContentValues(); 148 GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 10, 8, 30); 149 Long startTime = startCalendar.getTimeInMillis(); 150 GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 11, 8, 30); 151 Long endTime = endCalendar.getTimeInMillis(); 152 cv.put(Events.RRULE, "FREQ=WEEKLY;BYDAY=MO"); 153 p.setTimeRelatedValues(cv, startTime, endTime, 1); 154 155 // The start time should have hour/min/sec zero'd out 156 startCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 157 startCalendar.set(2010, 5, 10, 0, 0, 0); 158 startCalendar.set(GregorianCalendar.MILLISECOND, 0); 159 startTime = startCalendar.getTimeInMillis(); 160 assertEquals(startTime, cv.getAsLong(Events.DTSTART)); 161 162 // The duration should be in days 163 assertEquals("P1D", cv.getAsString(Events.DURATION)); 164 assertNull(cv.getAsLong(Events.DTEND)); 165 assertNull(cv.getAsLong(Events.LAST_DATE)); 166 // There must be a timezone 167 assertNotNull(cv.getAsString(Events.EVENT_TIMEZONE)); 168 } 169 170 public void testSetTimeRelatedValues_Recurring_AllDay_Exception () throws IOException { 171 CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class); 172 EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter); 173 ContentValues cv = new ContentValues(); 174 175 // Recurrence exception for all-day event; the exception is NOT all-day 176 GregorianCalendar startCalendar = new GregorianCalendar(2010, 5, 17, 8, 30); 177 Long startTime = startCalendar.getTimeInMillis(); 178 GregorianCalendar endCalendar = new GregorianCalendar(2010, 5, 17, 9, 30); 179 Long endTime = endCalendar.getTimeInMillis(); 180 cv.put(Events.ORIGINAL_ALL_DAY, 1); 181 GregorianCalendar instanceCalendar = new GregorianCalendar(2010, 5, 17, 8, 30); 182 cv.put(Events.ORIGINAL_INSTANCE_TIME, instanceCalendar.getTimeInMillis()); 183 p.setTimeRelatedValues(cv, startTime, endTime, 0); 184 185 // The original instance time should have hour/min/sec zero'd out 186 GregorianCalendar testCalendar = new GregorianCalendar(TimeZone.getTimeZone("UTC")); 187 testCalendar.set(2010, 5, 17, 0, 0, 0); 188 testCalendar.set(GregorianCalendar.MILLISECOND, 0); 189 Long testTime = testCalendar.getTimeInMillis(); 190 assertEquals(testTime, cv.getAsLong(Events.ORIGINAL_INSTANCE_TIME)); 191 192 // The exception isn't all-day, so we should have DTEND and LAST_DATE and no EVENT_TIMEZONE 193 assertNull(cv.getAsString(Events.DURATION)); 194 assertEquals(endTime, cv.getAsLong(Events.DTEND)); 195 assertEquals(endTime, cv.getAsLong(Events.LAST_DATE)); 196 assertNull(cv.getAsString(Events.EVENT_TIMEZONE)); 197 } 198 199 public void testIsValidEventValues() throws IOException { 200 CalendarSyncAdapter adapter = getTestSyncAdapter(CalendarSyncAdapter.class); 201 EasCalendarSyncParser p = adapter.new EasCalendarSyncParser(getTestInputStream(), adapter); 202 203 long validTime = System.currentTimeMillis(); 204 String validData = "foo-bar-bletch"; 205 String validDuration = "P30M"; 206 String validRrule = "FREQ=DAILY"; 207 208 ContentValues cv = new ContentValues(); 209 210 cv.put(Events.DTSTART, validTime); 211 // Needs _SYNC_DATA and DTEND/DURATION 212 assertFalse(p.isValidEventValues(cv)); 213 cv.put(Events._SYNC_DATA, validData); 214 // Needs DTEND/DURATION since not an exception 215 assertFalse(p.isValidEventValues(cv)); 216 cv.put(Events.DURATION, validDuration); 217 // Valid (DTSTART, _SYNC_DATA, DURATION) 218 assertTrue(p.isValidEventValues(cv)); 219 cv.remove(Events.DURATION); 220 cv.put(Events.ORIGINAL_INSTANCE_TIME, validTime); 221 // Needs DTEND since it's an exception 222 assertFalse(p.isValidEventValues(cv)); 223 cv.put(Events.DTEND, validTime); 224 // Valid (DTSTART, DTEND, ORIGINAL_INSTANCE_TIME) 225 cv.remove(Events.ORIGINAL_INSTANCE_TIME); 226 // Valid (DTSTART, _SYNC_DATA, DTEND) 227 assertTrue(p.isValidEventValues(cv)); 228 cv.remove(Events.DTSTART); 229 // Needs DTSTART 230 assertFalse(p.isValidEventValues(cv)); 231 cv.put(Events.DTSTART, validTime); 232 cv.put(Events.RRULE, validRrule); 233 // With RRULE, needs DURATION 234 assertFalse(p.isValidEventValues(cv)); 235 cv.put(Events.DURATION, "P30M"); 236 // Valid (DTSTART, RRULE, DURATION) 237 assertTrue(p.isValidEventValues(cv)); 238 cv.put(Events.ALL_DAY, "1"); 239 // Needs DURATION in the form P<n>D 240 assertFalse(p.isValidEventValues(cv)); 241 // Valid (DTSTART, RRULE, ALL_DAY, DURATION(P<n>D) 242 cv.put(Events.DURATION, "P1D"); 243 assertTrue(p.isValidEventValues(cv)); 244 } 245 246 private void addAttendeesToSerializer(Serializer s, int num) throws IOException { 247 for (int i = 0; i < num; i++) { 248 s.start(Tags.CALENDAR_ATTENDEE); 249 s.data(Tags.CALENDAR_ATTENDEE_EMAIL, "frederick" + num + 250 ".flintstone@this.that.verylongservername.com"); 251 s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); 252 s.data(Tags.CALENDAR_ATTENDEE_NAME, "Frederick" + num + " Flintstone, III"); 253 s.end(); 254 } 255 } 256 257 private void addAttendeeToSerializer(Serializer s, String email, String name) 258 throws IOException { 259 s.start(Tags.CALENDAR_ATTENDEE); 260 s.data(Tags.CALENDAR_ATTENDEE_EMAIL, email); 261 s.data(Tags.CALENDAR_ATTENDEE_TYPE, "1"); 262 s.data(Tags.CALENDAR_ATTENDEE_NAME, name); 263 s.end(); 264 } 265 266 private int countInsertOperationsForTable(CalendarOperations ops, String tableName) { 267 int cnt = 0; 268 for (ContentProviderOperation op: ops) { 269 List<String> segments = op.getUri().getPathSegments(); 270 if (segments.get(0).equalsIgnoreCase(tableName) && 271 op.getType() == ContentProviderOperation.TYPE_INSERT) { 272 cnt++; 273 } 274 } 275 return cnt; 276 } 277 278 class TestEvent extends Serializer { 279 CalendarSyncAdapter mAdapter; 280 EasCalendarSyncParser mParser; 281 Serializer mSerializer; 282 283 TestEvent() throws IOException { 284 super(false); 285 mAdapter = getTestSyncAdapter(CalendarSyncAdapter.class); 286 mParser = mAdapter.new EasCalendarSyncParser(getTestInputStream(), mAdapter); 287 } 288 289 void setUserEmailAddress(String addr) { 290 mAdapter.mAccount.mEmailAddress = addr; 291 mAdapter.mEmailAddress = addr; 292 } 293 294 EasCalendarSyncParser getParser() throws IOException { 295 // Set up our parser's input and eat the initial tag 296 mParser.resetInput(new ByteArrayInputStream(toByteArray())); 297 mParser.nextTag(0); 298 return mParser; 299 } 300 301 // setupPreAttendees and setupPostAttendees initialize calendar data in the order in which 302 // they would appear in an actual EAS session. Between these two calls, we initialize 303 // attendee data, which varies between the following tests 304 TestEvent setupPreAttendees() throws IOException { 305 start(Tags.SYNC_APPLICATION_DATA); 306 data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE); 307 data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z"); 308 data(Tags.CALENDAR_START_TIME, "20100518T220000Z"); 309 data(Tags.CALENDAR_SUBJECT, "Documentation"); 310 data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85"); 311 data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas"); 312 data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com"); 313 return this; 314 } 315 316 TestEvent setupPostAttendees()throws IOException { 317 data(Tags.CALENDAR_LOCATION, "CR SF 601T2/North Shore Presentation Self Service (16)"); 318 data(Tags.CALENDAR_END_TIME, "20100518T223000Z"); 319 start(Tags.BASE_BODY); 320 data(Tags.BASE_BODY_PREFERENCE, "1"); 321 data(Tags.BASE_ESTIMATED_DATA_SIZE, "69105"); // The number is ignored by the parser 322 data(Tags.BASE_DATA, 323 "This is the event description; we should probably make it longer"); 324 end(); // BASE_BODY 325 start(Tags.CALENDAR_RECURRENCE); 326 data(Tags.CALENDAR_RECURRENCE_TYPE, "1"); // weekly 327 data(Tags.CALENDAR_RECURRENCE_INTERVAL, "1"); 328 data(Tags.CALENDAR_RECURRENCE_OCCURRENCES, "10"); 329 data(Tags.CALENDAR_RECURRENCE_DAYOFWEEK, "12"); // tue, wed 330 data(Tags.CALENDAR_RECURRENCE_UNTIL, "2005-04-14T00:00:00.000Z"); 331 end(); // CALENDAR_RECURRENCE 332 data(Tags.CALENDAR_SENSITIVITY, "0"); 333 data(Tags.CALENDAR_BUSY_STATUS, "2"); 334 data(Tags.CALENDAR_ALL_DAY_EVENT, "0"); 335 data(Tags.CALENDAR_MEETING_STATUS, "3"); 336 data(Tags.BASE_NATIVE_BODY_TYPE, "3"); 337 end().done(); // SYNC_APPLICATION_DATA 338 return this; 339 } 340 } 341 342 public void testAddEvent() throws IOException { 343 TestEvent event = new TestEvent(); 344 event.setupPreAttendees(); 345 event.start(Tags.CALENDAR_ATTENDEES); 346 addAttendeesToSerializer(event, 10); 347 event.end(); // CALENDAR_ATTENDEES 348 event.setupPostAttendees(); 349 350 EasCalendarSyncParser p = event.getParser(); 351 p.addEvent(p.mOps, "1:1", false); 352 // There should be 1 event 353 assertEquals(1, countInsertOperationsForTable(p.mOps, "events")); 354 // Two attendees (organizer and 10 attendees) 355 assertEquals(11, countInsertOperationsForTable(p.mOps, "attendees")); 356 // dtstamp, meeting status, attendees, attendees redacted, and upsync prohibited 357 assertEquals(5, countInsertOperationsForTable(p.mOps, "extendedproperties")); 358 } 359 360 public void testAddEventIllegal() throws IOException { 361 // We don't send a start time; the event is illegal and nothing should be added 362 TestEvent event = new TestEvent(); 363 event.start(Tags.SYNC_APPLICATION_DATA); 364 event.data(Tags.CALENDAR_TIME_ZONE, TEST_TIME_ZONE); 365 event.data(Tags.CALENDAR_DTSTAMP, "20100518T213156Z"); 366 event.data(Tags.CALENDAR_SUBJECT, "Documentation"); 367 event.data(Tags.CALENDAR_UID, "4417556B-27DE-4ECE-B679-A63EFE1F9E85"); 368 event.data(Tags.CALENDAR_ORGANIZER_NAME, "Fred Squatibuquitas"); 369 event.data(Tags.CALENDAR_ORGANIZER_EMAIL, "fred.squatibuquitas@prettylongdomainname.com"); 370 event.start(Tags.CALENDAR_ATTENDEES); 371 addAttendeesToSerializer(event, 10); 372 event.end(); // CALENDAR_ATTENDEES 373 event.setupPostAttendees(); 374 375 EasCalendarSyncParser p = event.getParser(); 376 p.addEvent(p.mOps, "1:1", false); 377 assertEquals(0, countInsertOperationsForTable(p.mOps, "events")); 378 assertEquals(0, countInsertOperationsForTable(p.mOps, "attendees")); 379 assertEquals(0, countInsertOperationsForTable(p.mOps, "extendedproperties")); 380 } 381 382 public void testAddEventRedactedAttendees() throws IOException { 383 TestEvent event = new TestEvent(); 384 event.setupPreAttendees(); 385 event.start(Tags.CALENDAR_ATTENDEES); 386 addAttendeesToSerializer(event, 100); 387 event.end(); // CALENDAR_ATTENDEES 388 event.setupPostAttendees(); 389 390 EasCalendarSyncParser p = event.getParser(); 391 p.addEvent(p.mOps, "1:1", false); 392 // There should be 1 event 393 assertEquals(1, countInsertOperationsForTable(p.mOps, "events")); 394 // One attendees (organizer; all others are redacted) 395 assertEquals(1, countInsertOperationsForTable(p.mOps, "attendees")); 396 // dtstamp, meeting status, and attendees redacted 397 assertEquals(3, countInsertOperationsForTable(p.mOps, "extendedproperties")); 398 } 399 400 /** 401 * Setup for the following three tests, which check attendee status of an added event 402 * @param userEmail the email address of the user 403 * @param update whether or not the event is an update (rather than new) 404 * @return a Cursor to the Attendee records added to our MockProvider 405 * @throws IOException 406 * @throws RemoteException 407 * @throws OperationApplicationException 408 */ 409 private Cursor setupAddEventOneAttendee(String userEmail, boolean update) 410 throws IOException, RemoteException, OperationApplicationException { 411 TestEvent event = new TestEvent(); 412 event.setupPreAttendees(); 413 event.start(Tags.CALENDAR_ATTENDEES); 414 addAttendeeToSerializer(event, SINGLE_ATTENDEE_EMAIL, SINGLE_ATTENDEE_NAME); 415 event.setUserEmailAddress(userEmail); 416 event.end(); // CALENDAR_ATTENDEES 417 event.setupPostAttendees(); 418 419 EasCalendarSyncParser p = event.getParser(); 420 p.addEvent(p.mOps, "1:1", update); 421 // Send the CPO's to the mock provider 422 mMockResolver.applyBatch(MockProvider.AUTHORITY, p.mOps); 423 return mMockResolver.query(MockProvider.uri(Attendees.CONTENT_URI), ATTENDEE_PROJECTION, 424 null, null, null); 425 } 426 427 public void testAddEventOneAttendee() throws IOException, RemoteException, 428 OperationApplicationException { 429 Cursor c = setupAddEventOneAttendee("foo@bar.com", false); 430 assertEquals(2, c.getCount()); 431 // The organizer should be "accepted", the unknown attendee "none" 432 while (c.moveToNext()) { 433 if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) { 434 assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS)); 435 } else { 436 assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS)); 437 } 438 } 439 } 440 441 public void testAddEventSelfAttendee() throws IOException, RemoteException, 442 OperationApplicationException { 443 Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, false); 444 // The organizer should be "accepted", and our user/attendee should be "done" even though 445 // the busy status = 2 (because we can't tell from a status of 2 on new events) 446 while (c.moveToNext()) { 447 if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) { 448 assertEquals(Attendees.ATTENDEE_STATUS_NONE, c.getInt(ATTENDEE_STATUS)); 449 } else { 450 assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS)); 451 } 452 } 453 } 454 455 public void testAddEventSelfAttendeeUpdate() throws IOException, RemoteException, 456 OperationApplicationException { 457 Cursor c = setupAddEventOneAttendee(SINGLE_ATTENDEE_EMAIL, true); 458 // The organizer should be "accepted", and our user/attendee should be "accepted" (because 459 // busy status = 2 and this is an update 460 while (c.moveToNext()) { 461 if (SINGLE_ATTENDEE_EMAIL.equals(c.getString(ATTENDEE_EMAIL))) { 462 assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS)); 463 } else { 464 assertEquals(Attendees.ATTENDEE_STATUS_ACCEPTED, c.getInt(ATTENDEE_STATUS)); 465 } 466 } 467 } 468} 469