|
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.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; |
|
29 |
|
30 import android.content.ContentValues; |
|
31 import android.content.Context; |
|
32 import android.database.Cursor; |
|
33 import android.net.Uri; |
|
34 |
|
35 public class TestAndroidBrowserHistoryRepository extends AndroidBrowserRepositoryTestCase { |
|
36 |
|
37 @Override |
|
38 protected AndroidBrowserRepository getRepository() { |
|
39 |
|
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 } |
|
58 |
|
59 @Override |
|
60 protected AndroidBrowserRepositoryDataAccessor getDataAccessor() { |
|
61 return new AndroidBrowserHistoryDataAccessor(getApplicationContext()); |
|
62 } |
|
63 |
|
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 } |
|
71 |
|
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 } |
|
79 |
|
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 } |
|
89 |
|
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 } |
|
100 |
|
101 @Override |
|
102 public void testGuidsSinceReturnMultipleRecords() { |
|
103 HistoryRecord record0 = HistoryHelpers.createHistory1(); |
|
104 HistoryRecord record1 = HistoryHelpers.createHistory2(); |
|
105 guidsSinceReturnMultipleRecords(record0, record1); |
|
106 } |
|
107 |
|
108 @Override |
|
109 public void testGuidsSinceReturnNoRecords() { |
|
110 guidsSinceReturnNoRecords(HistoryHelpers.createHistory3()); |
|
111 } |
|
112 |
|
113 @Override |
|
114 public void testFetchSinceOneRecord() { |
|
115 fetchSinceOneRecord(HistoryHelpers.createHistory1(), |
|
116 HistoryHelpers.createHistory2()); |
|
117 } |
|
118 |
|
119 @Override |
|
120 public void testFetchSinceReturnNoRecords() { |
|
121 fetchSinceReturnNoRecords(HistoryHelpers.createHistory3()); |
|
122 } |
|
123 |
|
124 @Override |
|
125 public void testFetchOneRecordByGuid() { |
|
126 fetchOneRecordByGuid(HistoryHelpers.createHistory1(), |
|
127 HistoryHelpers.createHistory2()); |
|
128 } |
|
129 |
|
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 } |
|
137 |
|
138 @Override |
|
139 public void testFetchNoRecordByGuid() { |
|
140 fetchNoRecordByGuid(HistoryHelpers.createHistory1()); |
|
141 } |
|
142 |
|
143 @Override |
|
144 public void testWipe() { |
|
145 doWipe(HistoryHelpers.createHistory2(), HistoryHelpers.createHistory3()); |
|
146 } |
|
147 |
|
148 @Override |
|
149 public void testStore() { |
|
150 basicStoreTest(HistoryHelpers.createHistory1()); |
|
151 } |
|
152 |
|
153 @Override |
|
154 public void testRemoteNewerTimeStamp() { |
|
155 HistoryRecord local = HistoryHelpers.createHistory1(); |
|
156 HistoryRecord remote = HistoryHelpers.createHistory2(); |
|
157 remoteNewerTimeStamp(local, remote); |
|
158 } |
|
159 |
|
160 @Override |
|
161 public void testLocalNewerTimeStamp() { |
|
162 HistoryRecord local = HistoryHelpers.createHistory1(); |
|
163 HistoryRecord remote = HistoryHelpers.createHistory2(); |
|
164 localNewerTimeStamp(local, remote); |
|
165 } |
|
166 |
|
167 @Override |
|
168 public void testDeleteRemoteNewer() { |
|
169 HistoryRecord local = HistoryHelpers.createHistory1(); |
|
170 HistoryRecord remote = HistoryHelpers.createHistory2(); |
|
171 deleteRemoteNewer(local, remote); |
|
172 } |
|
173 |
|
174 @Override |
|
175 public void testDeleteLocalNewer() { |
|
176 HistoryRecord local = HistoryHelpers.createHistory1(); |
|
177 HistoryRecord remote = HistoryHelpers.createHistory2(); |
|
178 deleteLocalNewer(local, remote); |
|
179 } |
|
180 |
|
181 @Override |
|
182 public void testDeleteRemoteLocalNonexistent() { |
|
183 deleteRemoteLocalNonexistent(HistoryHelpers.createHistory2()); |
|
184 } |
|
185 |
|
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 } |
|
193 |
|
194 public boolean sameRecordString(HistoryRecord r1, HistoryRecord r2) { |
|
195 return buildRecordString(r1).equals(buildRecordString(r2)); |
|
196 } |
|
197 } |
|
198 |
|
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()); |
|
206 |
|
207 final long now = RepositorySession.now(); |
|
208 |
|
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); |
|
212 |
|
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"; |
|
219 |
|
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)); |
|
224 |
|
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)); |
|
234 |
|
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 } |
|
244 |
|
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(); |
|
252 |
|
253 HistoryRecord record0 = HistoryHelpers.createHistory3(); |
|
254 performWait(storeRunnable(session, record0)); |
|
255 |
|
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); |
|
265 |
|
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); |
|
271 |
|
272 performWait(fetchRunnable(session, new String[] { record0.guid }, new ExpectFetchDelegate(new Record[] { record0 }))); |
|
273 closeDataAccessor(dataAccessor); |
|
274 } |
|
275 |
|
276 @SuppressWarnings("unchecked") |
|
277 public void testAddMultipleVisits() { |
|
278 final RepositorySession session = createAndBeginSession(); |
|
279 |
|
280 HistoryRecord record0 = HistoryHelpers.createHistory4(); |
|
281 performWait(storeRunnable(session, record0)); |
|
282 |
|
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); |
|
292 |
|
293 // Now shift to microsecond timing for visits. |
|
294 long newMicroVisitTime = newVisitTime * 1000; |
|
295 |
|
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); |
|
309 |
|
310 ExpectFetchDelegate delegate = new ExpectFetchDelegate(new Record[] { record0 }); |
|
311 performWait(fetchRunnable(session, new String[] { record0.guid }, delegate)); |
|
312 |
|
313 Record fetched = delegate.records.get(0); |
|
314 assertTrue(record0.equalPayloads(fetched)); |
|
315 closeDataAccessor(dataAccessor); |
|
316 } |
|
317 |
|
318 public void testInvalidHistoryItemIsSkipped() throws NullCursorException { |
|
319 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); |
|
320 final AndroidBrowserRepositoryDataAccessor dbHelper = session.getDBHelper(); |
|
321 |
|
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); |
|
326 |
|
327 emptyURL.fennecDateVisited = now; |
|
328 emptyURL.fennecVisitCount = 1; |
|
329 emptyURL.histURI = ""; |
|
330 emptyURL.title = "Something"; |
|
331 |
|
332 noVisits.fennecDateVisited = now; |
|
333 noVisits.fennecVisitCount = 0; |
|
334 noVisits.histURI = "http://example.org/novisits"; |
|
335 noVisits.title = "Something Else"; |
|
336 |
|
337 aboutURL.fennecDateVisited = now; |
|
338 aboutURL.fennecVisitCount = 1; |
|
339 aboutURL.histURI = "about:home"; |
|
340 aboutURL.title = "Fennec Home"; |
|
341 |
|
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); |
|
348 |
|
349 // The records are in the DB. |
|
350 final Cursor all = dbHelper.fetchAll(); |
|
351 assertEquals(3, all.getCount()); |
|
352 all.close(); |
|
353 |
|
354 // But aren't returned by fetching. |
|
355 performWait(fetchAllRunnable(session, new Record[] {})); |
|
356 |
|
357 // And we'd ignore about:home if we downloaded it. |
|
358 assertTrue(session.shouldIgnore(aboutURL)); |
|
359 |
|
360 session.abort(); |
|
361 } |
|
362 |
|
363 public void testSqlInjectPurgeDelete() { |
|
364 // Some setup. |
|
365 RepositorySession session = createAndBeginSession(); |
|
366 final AndroidBrowserRepositoryDataAccessor db = getDataAccessor(); |
|
367 |
|
368 try { |
|
369 ContentValues cv = new ContentValues(); |
|
370 cv.put(BrowserContract.SyncColumns.IS_DELETED, 1); |
|
371 |
|
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"; |
|
376 |
|
377 db.insert(h1); |
|
378 db.insert(h2); |
|
379 |
|
380 // Test 1 - updateByGuid() handles evil history entries correctly. |
|
381 db.updateByGuid(h2.guid, cv); |
|
382 |
|
383 // Query history table. |
|
384 Cursor cur = getAllHistory(); |
|
385 int numHistory = cur.getCount(); |
|
386 |
|
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; |
|
393 |
|
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 } |
|
404 |
|
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 } |
|
411 |
|
412 cur = getAllHistory(); |
|
413 int numHistoryAfterDeletion = cur.getCount(); |
|
414 |
|
415 // Ensure we have only 1 deleted row. |
|
416 assertEquals(numHistoryAfterDeletion, numHistory - 1); |
|
417 |
|
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; |
|
424 |
|
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 } |
|
440 |
|
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 } |
|
447 |
|
448 public void testDataAccessorBulkInsert() throws NullCursorException { |
|
449 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); |
|
450 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); |
|
451 |
|
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); |
|
457 |
|
458 performWait(fetchAllRunnable(session, preparedExpectFetchDelegate(records.toArray(new Record[records.size()])))); |
|
459 session.abort(); |
|
460 } |
|
461 |
|
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(); |
|
466 |
|
467 // Confirm dataExtender is closed before beginning session. |
|
468 assertTrue(db.getHistoryDataExtender().isClosed()); |
|
469 } |
|
470 |
|
471 public void testDataExtenderIsClosedAfterFinish() throws InactiveSessionException { |
|
472 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); |
|
473 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); |
|
474 |
|
475 // Perform an action that opens the dataExtender. |
|
476 HistoryRecord h1 = HistoryHelpers.createHistory1(); |
|
477 db.insert(h1); |
|
478 assertFalse(db.getHistoryDataExtender().isClosed()); |
|
479 |
|
480 // Check dataExtender is closed upon finish. |
|
481 performWait(finishRunnable(session, new ExpectFinishDelegate())); |
|
482 assertTrue(db.getHistoryDataExtender().isClosed()); |
|
483 } |
|
484 |
|
485 public void testDataExtenderIsClosedAfterAbort() throws InactiveSessionException { |
|
486 final AndroidBrowserHistoryRepositorySession session = (AndroidBrowserHistoryRepositorySession) createAndBeginSession(); |
|
487 AndroidBrowserHistoryDataAccessor db = (AndroidBrowserHistoryDataAccessor) session.getDBHelper(); |
|
488 |
|
489 // Perform an action that opens the dataExtender. |
|
490 HistoryRecord h1 = HistoryHelpers.createHistory1(); |
|
491 db.insert(h1); |
|
492 assertFalse(db.getHistoryDataExtender().isClosed()); |
|
493 |
|
494 // Check dataExtender is closed upon abort. |
|
495 session.abort(); |
|
496 assertTrue(db.getHistoryDataExtender().isClosed()); |
|
497 } |
|
498 } |