|
1 /* Any copyright is dedicated to the Public Domain. |
|
2 http://creativecommons.org/publicdomain/zero/1.0/ */ |
|
3 |
|
4 Cu.import("resource://gre/modules/PlacesUtils.jsm"); |
|
5 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
6 Cu.import("resource://services-common/async.js"); |
|
7 Cu.import("resource://services-sync/engines/history.js"); |
|
8 Cu.import("resource://services-sync/service.js"); |
|
9 Cu.import("resource://services-sync/util.js"); |
|
10 |
|
11 const TIMESTAMP1 = (Date.now() - 103406528) * 1000; |
|
12 const TIMESTAMP2 = (Date.now() - 6592903) * 1000; |
|
13 const TIMESTAMP3 = (Date.now() - 123894) * 1000; |
|
14 |
|
15 function queryPlaces(uri, options) { |
|
16 let query = PlacesUtils.history.getNewQuery(); |
|
17 query.uri = uri; |
|
18 let res = PlacesUtils.history.executeQuery(query, options); |
|
19 res.root.containerOpen = true; |
|
20 |
|
21 let results = []; |
|
22 for (let i = 0; i < res.root.childCount; i++) |
|
23 results.push(res.root.getChild(i)); |
|
24 res.root.containerOpen = false; |
|
25 return results; |
|
26 } |
|
27 |
|
28 function queryHistoryVisits(uri) { |
|
29 let options = PlacesUtils.history.getNewQueryOptions(); |
|
30 options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; |
|
31 options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; |
|
32 options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING; |
|
33 return queryPlaces(uri, options); |
|
34 } |
|
35 |
|
36 function onNextTitleChanged(callback) { |
|
37 PlacesUtils.history.addObserver({ |
|
38 onBeginUpdateBatch: function onBeginUpdateBatch() {}, |
|
39 onEndUpdateBatch: function onEndUpdateBatch() {}, |
|
40 onPageChanged: function onPageChanged() {}, |
|
41 onTitleChanged: function onTitleChanged() { |
|
42 PlacesUtils.history.removeObserver(this); |
|
43 Utils.nextTick(callback); |
|
44 }, |
|
45 onVisit: function onVisit() {}, |
|
46 onDeleteVisits: function onDeleteVisits() {}, |
|
47 onPageExpired: function onPageExpired() {}, |
|
48 onDeleteURI: function onDeleteURI() {}, |
|
49 onClearHistory: function onClearHistory() {}, |
|
50 QueryInterface: XPCOMUtils.generateQI([ |
|
51 Ci.nsINavHistoryObserver, |
|
52 Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS, |
|
53 Ci.nsISupportsWeakReference |
|
54 ]) |
|
55 }, true); |
|
56 } |
|
57 |
|
58 // Ensure exceptions from inside callbacks leads to test failures while |
|
59 // we still clean up properly. |
|
60 function ensureThrows(func) { |
|
61 return function() { |
|
62 try { |
|
63 func.apply(this, arguments); |
|
64 } catch (ex) { |
|
65 PlacesUtils.history.removeAllPages(); |
|
66 do_throw(ex); |
|
67 } |
|
68 }; |
|
69 } |
|
70 |
|
71 let store = new HistoryEngine(Service)._store; |
|
72 function applyEnsureNoFailures(records) { |
|
73 do_check_eq(store.applyIncomingBatch(records).length, 0); |
|
74 } |
|
75 |
|
76 let fxuri, fxguid, tburi, tbguid; |
|
77 |
|
78 function run_test() { |
|
79 initTestLogging("Trace"); |
|
80 run_next_test(); |
|
81 } |
|
82 |
|
83 add_test(function test_store() { |
|
84 _("Verify that we've got an empty store to work with."); |
|
85 do_check_empty(store.getAllIDs()); |
|
86 |
|
87 _("Let's create an entry in the database."); |
|
88 fxuri = Utils.makeURI("http://getfirefox.com/"); |
|
89 |
|
90 let place = { |
|
91 uri: fxuri, |
|
92 title: "Get Firefox!", |
|
93 visits: [{ |
|
94 visitDate: TIMESTAMP1, |
|
95 transitionType: Ci.nsINavHistoryService.TRANSITION_LINK |
|
96 }] |
|
97 }; |
|
98 PlacesUtils.asyncHistory.updatePlaces(place, { |
|
99 handleError: function handleError() { |
|
100 do_throw("Unexpected error in adding visit."); |
|
101 }, |
|
102 handleResult: function handleResult() {}, |
|
103 handleCompletion: onVisitAdded |
|
104 }); |
|
105 |
|
106 function onVisitAdded() { |
|
107 _("Verify that the entry exists."); |
|
108 let ids = Object.keys(store.getAllIDs()); |
|
109 do_check_eq(ids.length, 1); |
|
110 fxguid = ids[0]; |
|
111 do_check_true(store.itemExists(fxguid)); |
|
112 |
|
113 _("If we query a non-existent record, it's marked as deleted."); |
|
114 let record = store.createRecord("non-existent"); |
|
115 do_check_true(record.deleted); |
|
116 |
|
117 _("Verify createRecord() returns a complete record."); |
|
118 record = store.createRecord(fxguid); |
|
119 do_check_eq(record.histUri, fxuri.spec); |
|
120 do_check_eq(record.title, "Get Firefox!"); |
|
121 do_check_eq(record.visits.length, 1); |
|
122 do_check_eq(record.visits[0].date, TIMESTAMP1); |
|
123 do_check_eq(record.visits[0].type, Ci.nsINavHistoryService.TRANSITION_LINK); |
|
124 |
|
125 _("Let's modify the record and have the store update the database."); |
|
126 let secondvisit = {date: TIMESTAMP2, |
|
127 type: Ci.nsINavHistoryService.TRANSITION_TYPED}; |
|
128 onNextTitleChanged(ensureThrows(function() { |
|
129 let queryres = queryHistoryVisits(fxuri); |
|
130 do_check_eq(queryres.length, 2); |
|
131 do_check_eq(queryres[0].time, TIMESTAMP1); |
|
132 do_check_eq(queryres[0].title, "Hol Dir Firefox!"); |
|
133 do_check_eq(queryres[1].time, TIMESTAMP2); |
|
134 do_check_eq(queryres[1].title, "Hol Dir Firefox!"); |
|
135 run_next_test(); |
|
136 })); |
|
137 applyEnsureNoFailures([ |
|
138 {id: fxguid, |
|
139 histUri: record.histUri, |
|
140 title: "Hol Dir Firefox!", |
|
141 visits: [record.visits[0], secondvisit]} |
|
142 ]); |
|
143 } |
|
144 }); |
|
145 |
|
146 add_test(function test_store_create() { |
|
147 _("Create a brand new record through the store."); |
|
148 tbguid = Utils.makeGUID(); |
|
149 tburi = Utils.makeURI("http://getthunderbird.com"); |
|
150 onNextTitleChanged(ensureThrows(function() { |
|
151 do_check_attribute_count(store.getAllIDs(), 2); |
|
152 let queryres = queryHistoryVisits(tburi); |
|
153 do_check_eq(queryres.length, 1); |
|
154 do_check_eq(queryres[0].time, TIMESTAMP3); |
|
155 do_check_eq(queryres[0].title, "The bird is the word!"); |
|
156 run_next_test(); |
|
157 })); |
|
158 applyEnsureNoFailures([ |
|
159 {id: tbguid, |
|
160 histUri: tburi.spec, |
|
161 title: "The bird is the word!", |
|
162 visits: [{date: TIMESTAMP3, |
|
163 type: Ci.nsINavHistoryService.TRANSITION_TYPED}]} |
|
164 ]); |
|
165 }); |
|
166 |
|
167 add_test(function test_null_title() { |
|
168 _("Make sure we handle a null title gracefully (it can happen in some cases, e.g. for resource:// URLs)"); |
|
169 let resguid = Utils.makeGUID(); |
|
170 let resuri = Utils.makeURI("unknown://title"); |
|
171 applyEnsureNoFailures([ |
|
172 {id: resguid, |
|
173 histUri: resuri.spec, |
|
174 title: null, |
|
175 visits: [{date: TIMESTAMP3, |
|
176 type: Ci.nsINavHistoryService.TRANSITION_TYPED}]} |
|
177 ]); |
|
178 do_check_attribute_count(store.getAllIDs(), 3); |
|
179 let queryres = queryHistoryVisits(resuri); |
|
180 do_check_eq(queryres.length, 1); |
|
181 do_check_eq(queryres[0].time, TIMESTAMP3); |
|
182 run_next_test(); |
|
183 }); |
|
184 |
|
185 add_test(function test_invalid_records() { |
|
186 _("Make sure we handle invalid URLs in places databases gracefully."); |
|
187 let connection = PlacesUtils.history |
|
188 .QueryInterface(Ci.nsPIPlacesDatabase) |
|
189 .DBConnection; |
|
190 let stmt = connection.createAsyncStatement( |
|
191 "INSERT INTO moz_places " |
|
192 + "(url, title, rev_host, visit_count, last_visit_date) " |
|
193 + "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")" |
|
194 ); |
|
195 Async.querySpinningly(stmt); |
|
196 stmt.finalize(); |
|
197 // Add the corresponding visit to retain database coherence. |
|
198 stmt = connection.createAsyncStatement( |
|
199 "INSERT INTO moz_historyvisits " |
|
200 + "(place_id, visit_date, visit_type, session) " |
|
201 + "VALUES ((SELECT id FROM moz_places WHERE url = 'invalid-uri'), " |
|
202 + TIMESTAMP3 + ", " + Ci.nsINavHistoryService.TRANSITION_TYPED + ", 1)" |
|
203 ); |
|
204 Async.querySpinningly(stmt); |
|
205 stmt.finalize(); |
|
206 do_check_attribute_count(store.getAllIDs(), 4); |
|
207 |
|
208 _("Make sure we report records with invalid URIs."); |
|
209 let invalid_uri_guid = Utils.makeGUID(); |
|
210 let failed = store.applyIncomingBatch([{ |
|
211 id: invalid_uri_guid, |
|
212 histUri: ":::::::::::::::", |
|
213 title: "Doesn't have a valid URI", |
|
214 visits: [{date: TIMESTAMP3, |
|
215 type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} |
|
216 ]); |
|
217 do_check_eq(failed.length, 1); |
|
218 do_check_eq(failed[0], invalid_uri_guid); |
|
219 |
|
220 _("Make sure we handle records with invalid GUIDs gracefully (ignore)."); |
|
221 applyEnsureNoFailures([ |
|
222 {id: "invalid", |
|
223 histUri: "http://invalid.guid/", |
|
224 title: "Doesn't have a valid GUID", |
|
225 visits: [{date: TIMESTAMP3, |
|
226 type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} |
|
227 ]); |
|
228 |
|
229 _("Make sure we report records with invalid visits, gracefully handle non-integer dates."); |
|
230 let no_date_visit_guid = Utils.makeGUID(); |
|
231 let no_type_visit_guid = Utils.makeGUID(); |
|
232 let invalid_type_visit_guid = Utils.makeGUID(); |
|
233 let non_integer_visit_guid = Utils.makeGUID(); |
|
234 failed = store.applyIncomingBatch([ |
|
235 {id: no_date_visit_guid, |
|
236 histUri: "http://no.date.visit/", |
|
237 title: "Visit has no date", |
|
238 visits: [{date: TIMESTAMP3}]}, |
|
239 {id: no_type_visit_guid, |
|
240 histUri: "http://no.type.visit/", |
|
241 title: "Visit has no type", |
|
242 visits: [{type: Ci.nsINavHistoryService.TRANSITION_EMBED}]}, |
|
243 {id: invalid_type_visit_guid, |
|
244 histUri: "http://invalid.type.visit/", |
|
245 title: "Visit has invalid type", |
|
246 visits: [{date: TIMESTAMP3, |
|
247 type: Ci.nsINavHistoryService.TRANSITION_LINK - 1}]}, |
|
248 {id: non_integer_visit_guid, |
|
249 histUri: "http://non.integer.visit/", |
|
250 title: "Visit has non-integer date", |
|
251 visits: [{date: 1234.567, |
|
252 type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} |
|
253 ]); |
|
254 do_check_eq(failed.length, 3); |
|
255 failed.sort(); |
|
256 let expected = [no_date_visit_guid, |
|
257 no_type_visit_guid, |
|
258 invalid_type_visit_guid].sort(); |
|
259 for (let i = 0; i < expected.length; i++) { |
|
260 do_check_eq(failed[i], expected[i]); |
|
261 } |
|
262 |
|
263 _("Make sure we handle records with javascript: URLs gracefully."); |
|
264 applyEnsureNoFailures([ |
|
265 {id: Utils.makeGUID(), |
|
266 histUri: "javascript:''", |
|
267 title: "javascript:''", |
|
268 visits: [{date: TIMESTAMP3, |
|
269 type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} |
|
270 ]); |
|
271 |
|
272 _("Make sure we handle records without any visits gracefully."); |
|
273 applyEnsureNoFailures([ |
|
274 {id: Utils.makeGUID(), |
|
275 histUri: "http://getfirebug.com", |
|
276 title: "Get Firebug!", |
|
277 visits: []} |
|
278 ]); |
|
279 |
|
280 run_next_test(); |
|
281 }); |
|
282 |
|
283 add_test(function test_remove() { |
|
284 _("Remove an existent record and a non-existent from the store."); |
|
285 applyEnsureNoFailures([{id: fxguid, deleted: true}, |
|
286 {id: Utils.makeGUID(), deleted: true}]); |
|
287 do_check_false(store.itemExists(fxguid)); |
|
288 let queryres = queryHistoryVisits(fxuri); |
|
289 do_check_eq(queryres.length, 0); |
|
290 |
|
291 _("Make sure wipe works."); |
|
292 store.wipe(); |
|
293 do_check_empty(store.getAllIDs()); |
|
294 queryres = queryHistoryVisits(fxuri); |
|
295 do_check_eq(queryres.length, 0); |
|
296 queryres = queryHistoryVisits(tburi); |
|
297 do_check_eq(queryres.length, 0); |
|
298 run_next_test(); |
|
299 }); |
|
300 |
|
301 add_test(function cleanup() { |
|
302 _("Clean up."); |
|
303 PlacesUtils.history.removeAllPages(); |
|
304 run_next_test(); |
|
305 }); |