1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 package org.mozilla.gecko.background.db; 5 6 import java.util.ArrayList; 7 8 import org.json.simple.JSONArray; 9 import org.mozilla.gecko.background.sync.helpers.BookmarkHelpers; 10 import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate; 11 import org.mozilla.gecko.background.sync.helpers.ExpectFetchSinceDelegate; 12 import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate; 13 import org.mozilla.gecko.background.sync.helpers.ExpectInvalidTypeStoreDelegate; 14 import org.mozilla.gecko.db.BrowserContract; 15 import org.mozilla.gecko.sync.Utils; 16 import org.mozilla.gecko.sync.repositories.NullCursorException; 17 import org.mozilla.gecko.sync.repositories.Repository; 18 import org.mozilla.gecko.sync.repositories.RepositorySession; 19 import org.mozilla.gecko.sync.repositories.android.BookmarksDataAccessor; 20 import org.mozilla.gecko.sync.repositories.android.BookmarksRepository; 21 import org.mozilla.gecko.sync.repositories.android.BookmarksRepositorySession; 22 import org.mozilla.gecko.sync.repositories.android.DataAccessor; 23 import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers; 24 import org.mozilla.gecko.sync.repositories.android.RepoUtils; 25 import org.mozilla.gecko.sync.repositories.domain.BookmarkRecord; 26 import org.mozilla.gecko.sync.repositories.domain.Record; 27 28 import android.content.ContentValues; 29 import android.content.Context; 30 import android.database.Cursor; 31 32 public class TestAndroidBrowserBookmarksRepository extends ThreadedRepositoryTestCase { 33 34 @Override getRepository()35 protected Repository getRepository() { 36 37 /** 38 * Override this chain in order to avoid our test code having to create two 39 * sessions all the time. 40 */ 41 return new BookmarksRepository() { 42 @Override 43 public RepositorySession createSession(Context context) { 44 return new BookmarksRepositorySession(this, context) { 45 @Override 46 protected synchronized void trackGUID(String guid) { 47 System.out.println("Ignoring trackGUID call: this is a test!"); 48 } 49 }; 50 } 51 }; 52 } 53 54 @Override 55 protected DataAccessor getDataAccessor() { 56 return new BookmarksDataAccessor(getApplicationContext()); 57 } 58 59 /** 60 * Hook to return an ExpectFetchDelegate, possibly with special GUIDs ignored. 61 */ 62 @Override 63 public ExpectFetchDelegate preparedExpectFetchDelegate(Record[] expected) { 64 ExpectFetchDelegate delegate = new ExpectFetchDelegate(expected); 65 delegate.ignore.addAll(BookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); 66 return delegate; 67 } 68 69 /** 70 * Hook to return an ExpectFetchSinceDelegate, possibly with special GUIDs ignored. 71 */ 72 public ExpectFetchSinceDelegate preparedExpectFetchSinceDelegate(long timestamp, String[] expected) { 73 ExpectFetchSinceDelegate delegate = new ExpectFetchSinceDelegate(timestamp, expected); 74 delegate.ignore.addAll(BookmarksRepositorySession.SPECIAL_GUIDS_MAP.keySet()); 75 return delegate; 76 } 77 78 // NOTE NOTE NOTE 79 // Must store folder before records if we we are checking that the 80 // records returned are the same as those sent in. If the folder isn't stored 81 // first, the returned records won't be identical to those stored because we 82 // aren't able to find the parent name/guid when we do a fetch. If you don't want 83 // to store a folder first, store your record in "mobile" or one of the folders 84 // that always exists. 85 86 public void testFetchOneWithChildren() throws Exception { 87 BookmarkRecord folder = BookmarkHelpers.createFolder1(); 88 BookmarkRecord bookmark1 = BookmarkHelpers.createBookmark1(); 89 BookmarkRecord bookmark2 = BookmarkHelpers.createBookmark2(); 90 91 RepositorySession session = createAndBeginSession(); 92 93 Record[] records = new Record[] { folder, bookmark1, bookmark2 }; 94 performWait(storeManyRunnable(session, records)); 95 96 DataAccessor helper = getDataAccessor(); 97 helper.dumpDB(); 98 closeDataAccessor(helper); 99 100 String[] guids = new String[] { folder.guid }; 101 Record[] expected = new Record[] { folder }; 102 performWait(fetchRunnable(session, guids, expected)); 103 dispose(session); 104 } 105 106 @Override 107 public void testFetchAll() { 108 Record[] expected = new Record[3]; 109 expected[0] = BookmarkHelpers.createFolder1(); 110 expected[1] = BookmarkHelpers.createBookmark1(); 111 expected[2] = BookmarkHelpers.createBookmark2(); 112 basicFetchAllTest(expected); 113 } 114 115 @Override 116 public void testFetchSinceOneRecord() { 117 fetchSinceOneRecord(BookmarkHelpers.createBookmarkInMobileFolder1(), 118 BookmarkHelpers.createBookmarkInMobileFolder2()); 119 } 120 121 @Override 122 public void testFetchSinceReturnNoRecords() { 123 fetchSinceReturnNoRecords(BookmarkHelpers.createBookmark1()); 124 } 125 126 @Override 127 public void testFetchOneRecordByGuid() { 128 fetchOneRecordByGuid(BookmarkHelpers.createBookmarkInMobileFolder1(), 129 BookmarkHelpers.createBookmarkInMobileFolder2()); 130 } 131 132 @Override 133 public void testFetchMultipleRecordsByGuids() { 134 BookmarkRecord record0 = BookmarkHelpers.createFolder1(); 135 BookmarkRecord record1 = BookmarkHelpers.createBookmark1(); 136 BookmarkRecord record2 = BookmarkHelpers.createBookmark2(); 137 fetchMultipleRecordsByGuids(record0, record1, record2); 138 } 139 140 @Override 141 public void testFetchNoRecordByGuid() { 142 fetchNoRecordByGuid(BookmarkHelpers.createBookmark1()); 143 } 144 145 146 @Override 147 public void testWipe() { 148 doWipe(BookmarkHelpers.createBookmarkInMobileFolder1(), 149 BookmarkHelpers.createBookmarkInMobileFolder2()); 150 } 151 152 @Override 153 public void testStore() { 154 basicStoreTest(BookmarkHelpers.createBookmark1()); 155 } 156 157 158 public void testStoreFolder() throws Exception { 159 basicStoreTest(BookmarkHelpers.createFolder1()); 160 } 161 162 /** 163 * TODO: 2011-12-24, tests disabled because we no longer fail 164 * a store call if we get an unknown record type. 165 */ 166 /* 167 * Test storing each different type of Bookmark record. 168 * We expect any records with type other than "bookmark" 169 * or "folder" to fail. For now we throw these away. 170 */ 171 /* 172 public void testStoreMicrosummary() { 173 basicStoreFailTest(BookmarkHelpers.createMicrosummary()); 174 } 175 176 public void testStoreQuery() { 177 basicStoreFailTest(BookmarkHelpers.createQuery()); 178 } 179 180 public void testStoreLivemark() { 181 basicStoreFailTest(BookmarkHelpers.createLivemark()); 182 } 183 184 public void testStoreSeparator() { 185 basicStoreFailTest(BookmarkHelpers.createSeparator()); 186 } 187 */ 188 189 protected void basicStoreFailTest(Record record) throws Exception { 190 final RepositorySession session = createAndBeginSession(); 191 performWait(storeRunnable(session, record, new ExpectInvalidTypeStoreDelegate())); 192 dispose(session); 193 } 194 195 /* 196 * Re-parenting tests 197 */ 198 // Insert two records missing parent, then insert their parent. 199 // Make sure they end up with the correct parent on fetch. 200 public void testBasicReparenting() throws Exception { 201 Record[] expected = new Record[] { 202 BookmarkHelpers.createBookmark1(), 203 BookmarkHelpers.createBookmark2(), 204 BookmarkHelpers.createFolder1() 205 }; 206 doMultipleFolderReparentingTest(expected); 207 } 208 209 // Insert 3 folders and 4 bookmarks in different orders 210 // and make sure they come out parented correctly 211 public void testMultipleFolderReparenting1() throws Exception { 212 Record[] expected = new Record[] { 213 BookmarkHelpers.createBookmark1(), 214 BookmarkHelpers.createBookmark2(), 215 BookmarkHelpers.createBookmark3(), 216 BookmarkHelpers.createFolder1(), 217 BookmarkHelpers.createBookmark4(), 218 BookmarkHelpers.createFolder3(), 219 BookmarkHelpers.createFolder2(), 220 }; 221 doMultipleFolderReparentingTest(expected); 222 } 223 224 public void testMultipleFolderReparenting2() throws Exception { 225 Record[] expected = new Record[] { 226 BookmarkHelpers.createBookmark1(), 227 BookmarkHelpers.createBookmark2(), 228 BookmarkHelpers.createBookmark3(), 229 BookmarkHelpers.createFolder1(), 230 BookmarkHelpers.createBookmark4(), 231 BookmarkHelpers.createFolder3(), 232 BookmarkHelpers.createFolder2(), 233 }; 234 doMultipleFolderReparentingTest(expected); 235 } 236 237 public void testMultipleFolderReparenting3() throws Exception { 238 Record[] expected = new Record[] { 239 BookmarkHelpers.createBookmark1(), 240 BookmarkHelpers.createBookmark2(), 241 BookmarkHelpers.createBookmark3(), 242 BookmarkHelpers.createFolder1(), 243 BookmarkHelpers.createBookmark4(), 244 BookmarkHelpers.createFolder3(), 245 BookmarkHelpers.createFolder2(), 246 }; 247 doMultipleFolderReparentingTest(expected); 248 } 249 250 private void doMultipleFolderReparentingTest(Record[] expected) throws Exception { 251 final RepositorySession session = createAndBeginSession(); 252 doStore(session, expected); 253 ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); 254 performWait(fetchAllRunnable(session, delegate)); 255 performWait(finishRunnable(session, new ExpectFinishDelegate())); 256 } 257 258 /* 259 * Test storing identical records with different guids. 260 * For bookmarks identical is defined by the following fields 261 * being the same: title, uri, type, parentName 262 */ 263 @Override 264 public void testStoreIdenticalExceptGuid() { 265 storeIdenticalExceptGuid(BookmarkHelpers.createBookmarkInMobileFolder1()); 266 } 267 268 /* 269 * More complicated situation in which we insert a folder 270 * followed by a couple of its children. We then insert 271 * the folder again but with a different guid. Children 272 * must still get correct parent when they are fetched. 273 * Store a record after with the new guid as the parent 274 * and make sure it works as well. 275 */ 276 public void testStoreIdenticalFoldersWithChildren() throws Exception { 277 final RepositorySession session = createAndBeginSession(); 278 Record record0 = BookmarkHelpers.createFolder1(); 279 280 // Get timestamp so that the conflicting folder that we store below is newer. 281 // Children won't come back on this fetch since they haven't been stored, so remove them 282 // before our delegate throws a failure. 283 BookmarkRecord rec0 = (BookmarkRecord) record0; 284 rec0.children = new JSONArray(); 285 performWait(storeRunnable(session, record0)); 286 287 ExpectFetchDelegate timestampDelegate = preparedExpectFetchDelegate(new Record[] { rec0 }); 288 performWait(fetchRunnable(session, new String[] { record0.guid }, timestampDelegate)); 289 290 DataAccessor helper = getDataAccessor(); 291 helper.dumpDB(); 292 closeDataAccessor(helper); 293 294 Record record1 = BookmarkHelpers.createBookmark1(); 295 Record record2 = BookmarkHelpers.createBookmark2(); 296 Record record3 = BookmarkHelpers.createFolder1(); 297 BookmarkRecord bmk3 = (BookmarkRecord) record3; 298 record3.guid = Utils.generateGuid(); 299 record3.lastModified = timestampDelegate.records.get(0).lastModified + 3000; 300 assertFalse(record0.guid.equals(record3.guid)); 301 302 // Store an additional record after inserting the duplicate folder 303 // with new GUID. Make sure it comes back as well. 304 Record record4 = BookmarkHelpers.createBookmark3(); 305 BookmarkRecord bmk4 = (BookmarkRecord) record4; 306 bmk4.parentID = bmk3.guid; 307 bmk4.parentName = bmk3.parentName; 308 309 doStore(session, new Record[] { 310 record1, record2, record3, bmk4 311 }); 312 BookmarkRecord bmk1 = (BookmarkRecord) record1; 313 bmk1.parentID = record3.guid; 314 BookmarkRecord bmk2 = (BookmarkRecord) record2; 315 bmk2.parentID = record3.guid; 316 Record[] expect = new Record[] { 317 bmk1, bmk2, record3 318 }; 319 fetchAllRunnable(session, preparedExpectFetchDelegate(expect)); 320 dispose(session); 321 } 322 323 @Override 324 public void testRemoteNewerTimeStamp() { 325 BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); 326 BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); 327 remoteNewerTimeStamp(local, remote); 328 } 329 330 @Override 331 public void testLocalNewerTimeStamp() { 332 BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); 333 BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); 334 localNewerTimeStamp(local, remote); 335 } 336 337 @Override 338 public void testDeleteRemoteNewer() { 339 BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); 340 BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); 341 deleteRemoteNewer(local, remote); 342 } 343 344 @Override 345 public void testDeleteLocalNewer() { 346 BookmarkRecord local = BookmarkHelpers.createBookmarkInMobileFolder1(); 347 BookmarkRecord remote = BookmarkHelpers.createBookmarkInMobileFolder2(); 348 deleteLocalNewer(local, remote); 349 } 350 351 @Override 352 public void testDeleteRemoteLocalNonexistent() { 353 BookmarkRecord remote = BookmarkHelpers.createBookmark2(); 354 deleteRemoteLocalNonexistent(remote); 355 } 356 357 @Override 358 public void testCleanMultipleRecords() { 359 cleanMultipleRecords( 360 BookmarkHelpers.createBookmarkInMobileFolder1(), 361 BookmarkHelpers.createBookmarkInMobileFolder2(), 362 BookmarkHelpers.createBookmark1(), 363 BookmarkHelpers.createBookmark2(), 364 BookmarkHelpers.createFolder1()); 365 } 366 367 public void testBasicPositioning() throws Exception { 368 final RepositorySession session = createAndBeginSession(); 369 Record[] expected = new Record[] { 370 BookmarkHelpers.createBookmark1(), 371 BookmarkHelpers.createFolder1(), 372 BookmarkHelpers.createBookmark2() 373 }; 374 System.out.println("TEST: Inserting " + expected[0].guid + ", " 375 + expected[1].guid + ", " 376 + expected[2].guid); 377 doStore(session, expected); 378 379 ExpectFetchDelegate delegate = preparedExpectFetchDelegate(expected); 380 performWait(fetchAllRunnable(session, delegate)); 381 382 int found = 0; 383 boolean foundFolder = false; 384 for (int i = 0; i < delegate.records.size(); i++) { 385 BookmarkRecord rec = (BookmarkRecord) delegate.records.get(i); 386 if (rec.guid.equals(expected[0].guid)) { 387 assertEquals(0, ((BookmarkRecord) delegate.records.get(i)).androidPosition); 388 found++; 389 } else if (rec.guid.equals(expected[2].guid)) { 390 assertEquals(1, ((BookmarkRecord) delegate.records.get(i)).androidPosition); 391 found++; 392 } else if (rec.guid.equals(expected[1].guid)) { 393 foundFolder = true; 394 } else { 395 System.out.println("TEST: found " + rec.guid); 396 } 397 } 398 assertTrue(foundFolder); 399 assertEquals(2, found); 400 dispose(session); 401 } 402 403 public void testSqlInjectPurgeDeleteAndUpdateByGuid() throws Exception { 404 // Some setup. 405 RepositorySession session = createAndBeginSession(); 406 DataAccessor db = getDataAccessor(); 407 408 ContentValues cv = new ContentValues(); 409 cv.put(BrowserContract.SyncColumns.IS_DELETED, 1); 410 411 // Create and insert 2 bookmarks, 2nd one is evil (attempts injection). 412 BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); 413 BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); 414 bmk2.guid = "' or '1'='1"; 415 416 db.insert(bmk1); 417 db.insert(bmk2); 418 419 // Test 1 - updateByGuid() handles evil bookmarks correctly. 420 db.updateByGuid(bmk2.guid, cv); 421 422 // Query bookmarks table. 423 Cursor cur = getAllBookmarks(); 424 int numBookmarks = cur.getCount(); 425 426 // Ensure only the evil bookmark is marked for deletion. 427 try { 428 cur.moveToFirst(); 429 while (!cur.isAfterLast()) { 430 String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); 431 boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; 432 433 if (guid.equals(bmk2.guid)) { 434 assertTrue(deleted); 435 } else { 436 assertFalse(deleted); 437 } 438 cur.moveToNext(); 439 } 440 } finally { 441 cur.close(); 442 } 443 444 // Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record. 445 try { 446 db.purgeDeleted(); 447 } catch (NullCursorException e) { 448 e.printStackTrace(); 449 } 450 451 cur = getAllBookmarks(); 452 int numBookmarksAfterDeletion = cur.getCount(); 453 454 // Ensure we have only 1 deleted row. 455 assertEquals(numBookmarksAfterDeletion, numBookmarks - 1); 456 457 // Ensure only the evil bookmark is deleted. 458 try { 459 cur.moveToFirst(); 460 while (!cur.isAfterLast()) { 461 String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); 462 boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1; 463 464 if (guid.equals(bmk2.guid)) { 465 fail("Evil guid was not deleted!"); 466 } else { 467 assertFalse(deleted); 468 } 469 cur.moveToNext(); 470 } 471 } finally { 472 cur.close(); 473 } 474 dispose(session); 475 } 476 477 protected Cursor getAllBookmarks() { 478 Context context = getApplicationContext(); 479 Cursor cur = context.getContentResolver().query(BrowserContractHelpers.BOOKMARKS_CONTENT_URI, 480 BrowserContractHelpers.BookmarkColumns, null, null, null); 481 return cur; 482 } 483 484 public void testSqlInjectFetch() throws Exception { 485 // Some setup. 486 RepositorySession session = createAndBeginSession(); 487 DataAccessor db = getDataAccessor(); 488 489 // Create and insert 4 bookmarks, last one is evil (attempts injection). 490 BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); 491 BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); 492 BookmarkRecord bmk3 = BookmarkHelpers.createBookmark3(); 493 BookmarkRecord bmk4 = BookmarkHelpers.createBookmark4(); 494 bmk4.guid = "' or '1'='1"; 495 496 db.insert(bmk1); 497 db.insert(bmk2); 498 db.insert(bmk3); 499 db.insert(bmk4); 500 501 // Perform a fetch. 502 Cursor cur = null; 503 try { 504 cur = db.fetch(new String[] { bmk3.guid, bmk4.guid }); 505 } catch (NullCursorException e1) { 506 e1.printStackTrace(); 507 } 508 509 // Ensure the correct number (2) of records were fetched and with the correct guids. 510 if (cur == null) { 511 fail("No records were fetched."); 512 } 513 514 try { 515 if (cur.getCount() != 2) { 516 fail("Wrong number of guids fetched!"); 517 } 518 cur.moveToFirst(); 519 while (!cur.isAfterLast()) { 520 String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); 521 if (!guid.equals(bmk3.guid) && !guid.equals(bmk4.guid)) { 522 fail("Wrong guids were fetched!"); 523 } 524 cur.moveToNext(); 525 } 526 } finally { 527 cur.close(); 528 } 529 dispose(session); 530 } 531 532 public void testSqlInjectDelete() throws Exception { 533 // Some setup. 534 RepositorySession session = createAndBeginSession(); 535 DataAccessor db = getDataAccessor(); 536 537 // Create and insert 2 bookmarks, 2nd one is evil (attempts injection). 538 BookmarkRecord bmk1 = BookmarkHelpers.createBookmark1(); 539 BookmarkRecord bmk2 = BookmarkHelpers.createBookmark2(); 540 bmk2.guid = "' or '1'='1"; 541 542 db.insert(bmk1); 543 db.insert(bmk2); 544 545 // Note size of table before delete. 546 Cursor cur = getAllBookmarks(); 547 int numBookmarks = cur.getCount(); 548 549 db.purgeGuid(bmk2.guid); 550 551 // Note size of table after delete. 552 cur = getAllBookmarks(); 553 int numBookmarksAfterDelete = cur.getCount(); 554 555 // Ensure size of table after delete is *only* 1 less. 556 assertEquals(numBookmarksAfterDelete, numBookmarks - 1); 557 558 try { 559 cur.moveToFirst(); 560 while (!cur.isAfterLast()) { 561 String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID); 562 if (guid.equals(bmk2.guid)) { 563 fail("Guid was not deleted!"); 564 } 565 cur.moveToNext(); 566 } 567 } finally { 568 cur.close(); 569 } 570 dispose(session); 571 } 572 573 /** 574 * Verify that data accessor's bulkInsert actually inserts. 575 * @throws NullCursorException 576 */ 577 public void testBulkInsert() throws Exception { 578 RepositorySession session = createAndBeginSession(); 579 DataAccessor db = getDataAccessor(); 580 581 // Have to set androidID of parent manually. 582 Cursor cur = db.fetch(new String[] { "mobile" } ); 583 assertEquals(1, cur.getCount()); 584 cur.moveToFirst(); 585 int mobileAndroidID = RepoUtils.getIntFromCursor(cur, BrowserContract.Bookmarks._ID); 586 587 BookmarkRecord bookmark1 = BookmarkHelpers.createBookmarkInMobileFolder1(); 588 BookmarkRecord bookmark2 = BookmarkHelpers.createBookmarkInMobileFolder2(); 589 bookmark1.androidParentID = mobileAndroidID; 590 bookmark2.androidParentID = mobileAndroidID; 591 ArrayList<Record> recordList = new ArrayList<Record>(); 592 recordList.add(bookmark1); 593 recordList.add(bookmark2); 594 db.bulkInsert(recordList); 595 596 String[] guids = new String[] { bookmark1.guid, bookmark2.guid }; 597 Record[] expected = new Record[] { bookmark1, bookmark2 }; 598 performWait(fetchRunnable(session, guids, expected)); 599 dispose(session); 600 } 601 } 602