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