Wed, 31 Dec 2014 07:22:50 +0100
Correct previous dual key logic pending first delivery installment.
1 /* Any copyright is dedicated to the Public Domain.
2 http://creativecommons.org/publicdomain/zero/1.0/ */
4 package org.mozilla.gecko.background.db;
6 import java.util.ArrayList;
8 import org.json.simple.JSONObject;
9 import org.mozilla.gecko.background.sync.helpers.ExpectFetchDelegate;
10 import org.mozilla.gecko.background.sync.helpers.ExpectFinishDelegate;
11 import org.mozilla.gecko.background.sync.helpers.HistoryHelpers;
12 import org.mozilla.gecko.db.BrowserContract;
13 import org.mozilla.gecko.sync.Utils;
14 import org.mozilla.gecko.sync.repositories.InactiveSessionException;
15 import org.mozilla.gecko.sync.repositories.NullCursorException;
16 import org.mozilla.gecko.sync.repositories.Repository;
17 import org.mozilla.gecko.sync.repositories.RepositorySession;
18 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryDataAccessor;
19 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepository;
20 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserHistoryRepositorySession;
21 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepository;
22 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositoryDataAccessor;
23 import org.mozilla.gecko.sync.repositories.android.AndroidBrowserRepositorySession;
24 import org.mozilla.gecko.sync.repositories.android.BrowserContractHelpers;
25 import org.mozilla.gecko.sync.repositories.android.RepoUtils;
26 import org.mozilla.gecko.sync.repositories.delegates.RepositorySessionCreationDelegate;
27 import org.mozilla.gecko.sync.repositories.domain.HistoryRecord;
28 import org.mozilla.gecko.sync.repositories.domain.Record;
30 import android.content.ContentValues;
31 import android.content.Context;
32 import android.database.Cursor;
33 import android.net.Uri;
35 public class TestAndroidBrowserHistoryRepository extends AndroidBrowserRepositoryTestCase {
37 @Override
38 protected AndroidBrowserRepository getRepository() {
40 /**
41 * Override this chain in order to avoid our test code having to create two
42 * sessions all the time.
43 */
44 return new AndroidBrowserHistoryRepository() {
45 @Override
46 protected void sessionCreator(RepositorySessionCreationDelegate delegate, Context context) {
47 AndroidBrowserHistoryRepositorySession session;
48 session = new AndroidBrowserHistoryRepositorySession(this, context) {
49 @Override
50 protected synchronized void trackGUID(String guid) {
51 System.out.println("Ignoring trackGUID call: this is a test!");
52 }
53 };
54 delegate.onSessionCreated(session);
55 }
56 };
57 }
59 @Override
60 protected AndroidBrowserRepositoryDataAccessor getDataAccessor() {
61 return new AndroidBrowserHistoryDataAccessor(getApplicationContext());
62 }
64 @Override
65 protected void closeDataAccessor(AndroidBrowserRepositoryDataAccessor dataAccessor) {
66 if (!(dataAccessor instanceof AndroidBrowserHistoryDataAccessor)) {
67 throw new IllegalArgumentException("Only expecting a history data accessor.");
68 }
69 ((AndroidBrowserHistoryDataAccessor) dataAccessor).closeExtender();
70 }
72 @Override
73 public void testFetchAll() {
74 Record[] expected = new Record[2];
75 expected[0] = HistoryHelpers.createHistory3();
76 expected[1] = HistoryHelpers.createHistory2();
77 basicFetchAllTest(expected);
78 }
80 /*
81 * Test storing identical records with different guids.
82 * For bookmarks identical is defined by the following fields
83 * being the same: title, uri, type, parentName
84 */
85 @Override
86 public void testStoreIdenticalExceptGuid() {
87 storeIdenticalExceptGuid(HistoryHelpers.createHistory1());
88 }
90 @Override
91 public void testCleanMultipleRecords() {
92 cleanMultipleRecords(
93 HistoryHelpers.createHistory1(),
94 HistoryHelpers.createHistory2(),
95 HistoryHelpers.createHistory3(),
96 HistoryHelpers.createHistory4(),
97 HistoryHelpers.createHistory5()
98 );
99 }
101 @Override
102 public void testGuidsSinceReturnMultipleRecords() {
103 HistoryRecord record0 = HistoryHelpers.createHistory1();
104 HistoryRecord record1 = HistoryHelpers.createHistory2();
105 guidsSinceReturnMultipleRecords(record0, record1);
106 }
108 @Override
109 public void testGuidsSinceReturnNoRecords() {
110 guidsSinceReturnNoRecords(HistoryHelpers.createHistory3());
111 }
113 @Override
114 public void testFetchSinceOneRecord() {
115 fetchSinceOneRecord(HistoryHelpers.createHistory1(),
116 HistoryHelpers.createHistory2());
117 }
119 @Override
120 public void testFetchSinceReturnNoRecords() {
121 fetchSinceReturnNoRecords(HistoryHelpers.createHistory3());
122 }
124 @Override
125 public void testFetchOneRecordByGuid() {
126 fetchOneRecordByGuid(HistoryHelpers.createHistory1(),
127 HistoryHelpers.createHistory2());
128 }
130 @Override
131 public void testFetchMultipleRecordsByGuids() {
132 HistoryRecord record0 = HistoryHelpers.createHistory1();
133 HistoryRecord record1 = HistoryHelpers.createHistory2();
134 HistoryRecord record2 = HistoryHelpers.createHistory3();
135 fetchMultipleRecordsByGuids(record0, record1, record2);
136 }
138 @Override
139 public void testFetchNoRecordByGuid() {
140 fetchNoRecordByGuid(HistoryHelpers.createHistory1());
141 }
143 @Override
144 public void testWipe() {
145 doWipe(HistoryHelpers.createHistory2(), HistoryHelpers.createHistory3());
146 }
148 @Override
149 public void testStore() {
150 basicStoreTest(HistoryHelpers.createHistory1());
151 }
153 @Override
154 public void testRemoteNewerTimeStamp() {
155 HistoryRecord local = HistoryHelpers.createHistory1();
156 HistoryRecord remote = HistoryHelpers.createHistory2();
157 remoteNewerTimeStamp(local, remote);
158 }
160 @Override
161 public void testLocalNewerTimeStamp() {
162 HistoryRecord local = HistoryHelpers.createHistory1();
163 HistoryRecord remote = HistoryHelpers.createHistory2();
164 localNewerTimeStamp(local, remote);
165 }
167 @Override
168 public void testDeleteRemoteNewer() {
169 HistoryRecord local = HistoryHelpers.createHistory1();
170 HistoryRecord remote = HistoryHelpers.createHistory2();
171 deleteRemoteNewer(local, remote);
172 }
174 @Override
175 public void testDeleteLocalNewer() {
176 HistoryRecord local = HistoryHelpers.createHistory1();
177 HistoryRecord remote = HistoryHelpers.createHistory2();
178 deleteLocalNewer(local, remote);
179 }
181 @Override
182 public void testDeleteRemoteLocalNonexistent() {
183 deleteRemoteLocalNonexistent(HistoryHelpers.createHistory2());
184 }
186 /**
187 * Exists to provide access to record string logic.
188 */
189 protected class HelperHistorySession extends AndroidBrowserHistoryRepositorySession {
190 public HelperHistorySession(Repository repository, Context context) {
191 super(repository, context);
192 }
194 public boolean sameRecordString(HistoryRecord r1, HistoryRecord r2) {
195 return buildRecordString(r1).equals(buildRecordString(r2));
196 }
197 }
199 /**
200 * Verifies that two history records with the same URI but different
201 * titles will be reconciled locally.
202 */
203 public void testRecordStringCollisionAndEquality() {
204 final AndroidBrowserHistoryRepository repo = new AndroidBrowserHistoryRepository();
205 final HelperHistorySession testSession = new HelperHistorySession(repo, getApplicationContext());
207 final long now = RepositorySession.now();
209 final HistoryRecord record0 = new HistoryRecord(null, "history", now + 1, false);
210 final HistoryRecord record1 = new HistoryRecord(null, "history", now + 2, false);
211 final HistoryRecord record2 = new HistoryRecord(null, "history", now + 3, false);
213 record0.histURI = "http://example.com/foo";
214 record1.histURI = "http://example.com/foo";
215 record2.histURI = "http://example.com/bar";
216 record0.title = "Foo 0";
217 record1.title = "Foo 1";
218 record2.title = "Foo 2";
220 // Ensure that two records with the same URI produce the same record string,
221 // and two records with different URIs do not.
222 assertTrue(testSession.sameRecordString(record0, record1));
223 assertFalse(testSession.sameRecordString(record0, record2));
225 // Two records are congruent if they have the same URI and their
226 // identifiers match (which is why these all have null GUIDs).
227 assertTrue(record0.congruentWith(record0));
228 assertTrue(record0.congruentWith(record1));
229 assertTrue(record1.congruentWith(record0));
230 assertFalse(record0.congruentWith(record2));
231 assertFalse(record1.congruentWith(record2));
232 assertFalse(record2.congruentWith(record1));
233 assertFalse(record2.congruentWith(record0));
235 // None of these records are equal, because they have different titles.
236 // (Except for being equal to themselves, of course.)
237 assertTrue(record0.equalPayloads(record0));
238 assertTrue(record1.equalPayloads(record1));
239 assertTrue(record2.equalPayloads(record2));
240 assertFalse(record0.equalPayloads(record1));
241 assertFalse(record1.equalPayloads(record0));
242 assertFalse(record1.equalPayloads(record2));
243 }
245 /*
246 * Tests for adding some visits to a history record
247 * and doing a fetch.
248 */
249 @SuppressWarnings("unchecked")
250 public void testAddOneVisit() {
251 final RepositorySession session = createAndBeginSession();
253 HistoryRecord record0 = HistoryHelpers.createHistory3();
254 performWait(storeRunnable(session, record0));
256 // Add one visit to the count and put in a new
257 // last visited date.
258 ContentValues cv = new ContentValues();
259 int visits = record0.visits.size() + 1;
260 long newVisitTime = System.currentTimeMillis();
261 cv.put(BrowserContract.History.VISITS, visits);
262 cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime);
263 final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor();
264 dataAccessor.updateByGuid(record0.guid, cv);
266 // Add expected visit to record for verification.
267 JSONObject expectedVisit = new JSONObject();
268 expectedVisit.put("date", newVisitTime * 1000); // Microseconds.
269 expectedVisit.put("type", 1L);
270 record0.visits.add(expectedVisit);
272 performWait(fetchRunnable(session, new String[] { record0.guid }, new ExpectFetchDelegate(new Record[] { record0 })));
273 closeDataAccessor(dataAccessor);
274 }
276 @SuppressWarnings("unchecked")
277 public void testAddMultipleVisits() {
278 final RepositorySession session = createAndBeginSession();
280 HistoryRecord record0 = HistoryHelpers.createHistory4();
281 performWait(storeRunnable(session, record0));
283 // Add three visits to the count and put in a new
284 // last visited date.
285 ContentValues cv = new ContentValues();
286 int visits = record0.visits.size() + 3;
287 long newVisitTime = System.currentTimeMillis();
288 cv.put(BrowserContract.History.VISITS, visits);
289 cv.put(BrowserContract.History.DATE_LAST_VISITED, newVisitTime);
290 final AndroidBrowserRepositoryDataAccessor dataAccessor = getDataAccessor();
291 dataAccessor.updateByGuid(record0.guid, cv);
293 // Now shift to microsecond timing for visits.
294 long newMicroVisitTime = newVisitTime * 1000;
296 // Add expected visits to record for verification
297 JSONObject expectedVisit = new JSONObject();
298 expectedVisit.put("date", newMicroVisitTime);
299 expectedVisit.put("type", 1L);
300 record0.visits.add(expectedVisit);
301 expectedVisit = new JSONObject();
302 expectedVisit.put("date", newMicroVisitTime - 1000);
303 expectedVisit.put("type", 1L);
304 record0.visits.add(expectedVisit);
305 expectedVisit = new JSONObject();
306 expectedVisit.put("date", newMicroVisitTime - 2000);
307 expectedVisit.put("type", 1L);
308 record0.visits.add(expectedVisit);
310 ExpectFetchDelegate delegate = new ExpectFetchDelegate(new Record[] { record0 });
311 performWait(fetchRunnable(session, new String[] { record0.guid }, delegate));
313 Record fetched = delegate.records.get(0);
314 assertTrue(record0.equalPayloads(fetched));
315 closeDataAccessor(dataAccessor);
316 }
318 public void testInvalidHistoryItemIsSkipped() throws NullCursorException {
319 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
320 final AndroidBrowserRepositoryDataAccessor dbHelper = session.getDBHelper();
322 final long now = System.currentTimeMillis();
323 final HistoryRecord emptyURL = new HistoryRecord(Utils.generateGuid(), "history", now, false);
324 final HistoryRecord noVisits = new HistoryRecord(Utils.generateGuid(), "history", now, false);
325 final HistoryRecord aboutURL = new HistoryRecord(Utils.generateGuid(), "history", now, false);
327 emptyURL.fennecDateVisited = now;
328 emptyURL.fennecVisitCount = 1;
329 emptyURL.histURI = "";
330 emptyURL.title = "Something";
332 noVisits.fennecDateVisited = now;
333 noVisits.fennecVisitCount = 0;
334 noVisits.histURI = "http://example.org/novisits";
335 noVisits.title = "Something Else";
337 aboutURL.fennecDateVisited = now;
338 aboutURL.fennecVisitCount = 1;
339 aboutURL.histURI = "about:home";
340 aboutURL.title = "Fennec Home";
342 Uri one = dbHelper.insert(emptyURL);
343 Uri two = dbHelper.insert(noVisits);
344 Uri tre = dbHelper.insert(aboutURL);
345 assertNotNull(one);
346 assertNotNull(two);
347 assertNotNull(tre);
349 // The records are in the DB.
350 final Cursor all = dbHelper.fetchAll();
351 assertEquals(3, all.getCount());
352 all.close();
354 // But aren't returned by fetching.
355 performWait(fetchAllRunnable(session, new Record[] {}));
357 // And we'd ignore about:home if we downloaded it.
358 assertTrue(session.shouldIgnore(aboutURL));
360 session.abort();
361 }
363 public void testSqlInjectPurgeDelete() {
364 // Some setup.
365 RepositorySession session = createAndBeginSession();
366 final AndroidBrowserRepositoryDataAccessor db = getDataAccessor();
368 try {
369 ContentValues cv = new ContentValues();
370 cv.put(BrowserContract.SyncColumns.IS_DELETED, 1);
372 // Create and insert 2 history entries, 2nd one is evil (attempts injection).
373 HistoryRecord h1 = HistoryHelpers.createHistory1();
374 HistoryRecord h2 = HistoryHelpers.createHistory2();
375 h2.guid = "' or '1'='1";
377 db.insert(h1);
378 db.insert(h2);
380 // Test 1 - updateByGuid() handles evil history entries correctly.
381 db.updateByGuid(h2.guid, cv);
383 // Query history table.
384 Cursor cur = getAllHistory();
385 int numHistory = cur.getCount();
387 // Ensure only the evil history entry is marked for deletion.
388 try {
389 cur.moveToFirst();
390 while (!cur.isAfterLast()) {
391 String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
392 boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
394 if (guid.equals(h2.guid)) {
395 assertTrue(deleted);
396 } else {
397 assertFalse(deleted);
398 }
399 cur.moveToNext();
400 }
401 } finally {
402 cur.close();
403 }
405 // Test 2 - Ensure purgeDelete()'s call to delete() deletes only 1 record.
406 try {
407 db.purgeDeleted();
408 } catch (NullCursorException e) {
409 e.printStackTrace();
410 }
412 cur = getAllHistory();
413 int numHistoryAfterDeletion = cur.getCount();
415 // Ensure we have only 1 deleted row.
416 assertEquals(numHistoryAfterDeletion, numHistory - 1);
418 // Ensure only the evil history is deleted.
419 try {
420 cur.moveToFirst();
421 while (!cur.isAfterLast()) {
422 String guid = RepoUtils.getStringFromCursor(cur, BrowserContract.SyncColumns.GUID);
423 boolean deleted = RepoUtils.getLongFromCursor(cur, BrowserContract.SyncColumns.IS_DELETED) == 1;
425 if (guid.equals(h2.guid)) {
426 fail("Evil guid was not deleted!");
427 } else {
428 assertFalse(deleted);
429 }
430 cur.moveToNext();
431 }
432 } finally {
433 cur.close();
434 }
435 } finally {
436 closeDataAccessor(db);
437 session.abort();
438 }
439 }
441 protected Cursor getAllHistory() {
442 Context context = getApplicationContext();
443 Cursor cur = context.getContentResolver().query(BrowserContractHelpers.HISTORY_CONTENT_URI,
444 BrowserContractHelpers.HistoryColumns, null, null, null);
445 return cur;
446 }
448 public void testDataAccessorBulkInsert() throws NullCursorException {
449 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
450 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
452 ArrayList<HistoryRecord> records = new ArrayList<HistoryRecord>();
453 records.add(HistoryHelpers.createHistory1());
454 records.add(HistoryHelpers.createHistory2());
455 records.add(HistoryHelpers.createHistory3());
456 db.bulkInsert(records);
458 performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(records.toArray(new Record[records.size()]))));
459 session.abort();
460 }
462 public void testDataExtenderIsClosedBeforeBegin() {
463 // Create a session but don't begin() it.
464 final AndroidBrowserRepositorySession session = (AndroidBrowserRepositorySession) createSession();
465 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
467 // Confirm dataExtender is closed before beginning session.
468 assertTrue(db.getHistoryDataExtender().isClosed());
469 }
471 public void testDataExtenderIsClosedAfterFinish() throws InactiveSessionException {
472 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
473 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
475 // Perform an action that opens the dataExtender.
476 HistoryRecord h1 = HistoryHelpers.createHistory1();
477 db.insert(h1);
478 assertFalse(db.getHistoryDataExtender().isClosed());
480 // Check dataExtender is closed upon finish.
481 performWait(finishRunnable(session, new ExpectFinishDelegate()));
482 assertTrue(db.getHistoryDataExtender().isClosed());
483 }
485 public void testDataExtenderIsClosedAfterAbort() throws InactiveSessionException {
486 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession();
487 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper();
489 // Perform an action that opens the dataExtender.
490 HistoryRecord h1 = HistoryHelpers.createHistory1();
491 db.insert(h1);
492 assertFalse(db.getHistoryDataExtender().isClosed());
494 // Check dataExtender is closed upon abort.
495 session.abort();
496 assertTrue(db.getHistoryDataExtender().isClosed());
497 }
498 }