1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/services/sync/tests/unit/test_history_store.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,305 @@ 1.4 +/* Any copyright is dedicated to the Public Domain. 1.5 + http://creativecommons.org/publicdomain/zero/1.0/ */ 1.6 + 1.7 +Cu.import("resource://gre/modules/PlacesUtils.jsm"); 1.8 +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 1.9 +Cu.import("resource://services-common/async.js"); 1.10 +Cu.import("resource://services-sync/engines/history.js"); 1.11 +Cu.import("resource://services-sync/service.js"); 1.12 +Cu.import("resource://services-sync/util.js"); 1.13 + 1.14 +const TIMESTAMP1 = (Date.now() - 103406528) * 1000; 1.15 +const TIMESTAMP2 = (Date.now() - 6592903) * 1000; 1.16 +const TIMESTAMP3 = (Date.now() - 123894) * 1000; 1.17 + 1.18 +function queryPlaces(uri, options) { 1.19 + let query = PlacesUtils.history.getNewQuery(); 1.20 + query.uri = uri; 1.21 + let res = PlacesUtils.history.executeQuery(query, options); 1.22 + res.root.containerOpen = true; 1.23 + 1.24 + let results = []; 1.25 + for (let i = 0; i < res.root.childCount; i++) 1.26 + results.push(res.root.getChild(i)); 1.27 + res.root.containerOpen = false; 1.28 + return results; 1.29 +} 1.30 + 1.31 +function queryHistoryVisits(uri) { 1.32 + let options = PlacesUtils.history.getNewQueryOptions(); 1.33 + options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; 1.34 + options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; 1.35 + options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING; 1.36 + return queryPlaces(uri, options); 1.37 +} 1.38 + 1.39 +function onNextTitleChanged(callback) { 1.40 + PlacesUtils.history.addObserver({ 1.41 + onBeginUpdateBatch: function onBeginUpdateBatch() {}, 1.42 + onEndUpdateBatch: function onEndUpdateBatch() {}, 1.43 + onPageChanged: function onPageChanged() {}, 1.44 + onTitleChanged: function onTitleChanged() { 1.45 + PlacesUtils.history.removeObserver(this); 1.46 + Utils.nextTick(callback); 1.47 + }, 1.48 + onVisit: function onVisit() {}, 1.49 + onDeleteVisits: function onDeleteVisits() {}, 1.50 + onPageExpired: function onPageExpired() {}, 1.51 + onDeleteURI: function onDeleteURI() {}, 1.52 + onClearHistory: function onClearHistory() {}, 1.53 + QueryInterface: XPCOMUtils.generateQI([ 1.54 + Ci.nsINavHistoryObserver, 1.55 + Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS, 1.56 + Ci.nsISupportsWeakReference 1.57 + ]) 1.58 + }, true); 1.59 +} 1.60 + 1.61 +// Ensure exceptions from inside callbacks leads to test failures while 1.62 +// we still clean up properly. 1.63 +function ensureThrows(func) { 1.64 + return function() { 1.65 + try { 1.66 + func.apply(this, arguments); 1.67 + } catch (ex) { 1.68 + PlacesUtils.history.removeAllPages(); 1.69 + do_throw(ex); 1.70 + } 1.71 + }; 1.72 +} 1.73 + 1.74 +let store = new HistoryEngine(Service)._store; 1.75 +function applyEnsureNoFailures(records) { 1.76 + do_check_eq(store.applyIncomingBatch(records).length, 0); 1.77 +} 1.78 + 1.79 +let fxuri, fxguid, tburi, tbguid; 1.80 + 1.81 +function run_test() { 1.82 + initTestLogging("Trace"); 1.83 + run_next_test(); 1.84 +} 1.85 + 1.86 +add_test(function test_store() { 1.87 + _("Verify that we've got an empty store to work with."); 1.88 + do_check_empty(store.getAllIDs()); 1.89 + 1.90 + _("Let's create an entry in the database."); 1.91 + fxuri = Utils.makeURI("http://getfirefox.com/"); 1.92 + 1.93 + let place = { 1.94 + uri: fxuri, 1.95 + title: "Get Firefox!", 1.96 + visits: [{ 1.97 + visitDate: TIMESTAMP1, 1.98 + transitionType: Ci.nsINavHistoryService.TRANSITION_LINK 1.99 + }] 1.100 + }; 1.101 + PlacesUtils.asyncHistory.updatePlaces(place, { 1.102 + handleError: function handleError() { 1.103 + do_throw("Unexpected error in adding visit."); 1.104 + }, 1.105 + handleResult: function handleResult() {}, 1.106 + handleCompletion: onVisitAdded 1.107 + }); 1.108 + 1.109 + function onVisitAdded() { 1.110 + _("Verify that the entry exists."); 1.111 + let ids = Object.keys(store.getAllIDs()); 1.112 + do_check_eq(ids.length, 1); 1.113 + fxguid = ids[0]; 1.114 + do_check_true(store.itemExists(fxguid)); 1.115 + 1.116 + _("If we query a non-existent record, it's marked as deleted."); 1.117 + let record = store.createRecord("non-existent"); 1.118 + do_check_true(record.deleted); 1.119 + 1.120 + _("Verify createRecord() returns a complete record."); 1.121 + record = store.createRecord(fxguid); 1.122 + do_check_eq(record.histUri, fxuri.spec); 1.123 + do_check_eq(record.title, "Get Firefox!"); 1.124 + do_check_eq(record.visits.length, 1); 1.125 + do_check_eq(record.visits[0].date, TIMESTAMP1); 1.126 + do_check_eq(record.visits[0].type, Ci.nsINavHistoryService.TRANSITION_LINK); 1.127 + 1.128 + _("Let's modify the record and have the store update the database."); 1.129 + let secondvisit = {date: TIMESTAMP2, 1.130 + type: Ci.nsINavHistoryService.TRANSITION_TYPED}; 1.131 + onNextTitleChanged(ensureThrows(function() { 1.132 + let queryres = queryHistoryVisits(fxuri); 1.133 + do_check_eq(queryres.length, 2); 1.134 + do_check_eq(queryres[0].time, TIMESTAMP1); 1.135 + do_check_eq(queryres[0].title, "Hol Dir Firefox!"); 1.136 + do_check_eq(queryres[1].time, TIMESTAMP2); 1.137 + do_check_eq(queryres[1].title, "Hol Dir Firefox!"); 1.138 + run_next_test(); 1.139 + })); 1.140 + applyEnsureNoFailures([ 1.141 + {id: fxguid, 1.142 + histUri: record.histUri, 1.143 + title: "Hol Dir Firefox!", 1.144 + visits: [record.visits[0], secondvisit]} 1.145 + ]); 1.146 + } 1.147 +}); 1.148 + 1.149 +add_test(function test_store_create() { 1.150 + _("Create a brand new record through the store."); 1.151 + tbguid = Utils.makeGUID(); 1.152 + tburi = Utils.makeURI("http://getthunderbird.com"); 1.153 + onNextTitleChanged(ensureThrows(function() { 1.154 + do_check_attribute_count(store.getAllIDs(), 2); 1.155 + let queryres = queryHistoryVisits(tburi); 1.156 + do_check_eq(queryres.length, 1); 1.157 + do_check_eq(queryres[0].time, TIMESTAMP3); 1.158 + do_check_eq(queryres[0].title, "The bird is the word!"); 1.159 + run_next_test(); 1.160 + })); 1.161 + applyEnsureNoFailures([ 1.162 + {id: tbguid, 1.163 + histUri: tburi.spec, 1.164 + title: "The bird is the word!", 1.165 + visits: [{date: TIMESTAMP3, 1.166 + type: Ci.nsINavHistoryService.TRANSITION_TYPED}]} 1.167 + ]); 1.168 +}); 1.169 + 1.170 +add_test(function test_null_title() { 1.171 + _("Make sure we handle a null title gracefully (it can happen in some cases, e.g. for resource:// URLs)"); 1.172 + let resguid = Utils.makeGUID(); 1.173 + let resuri = Utils.makeURI("unknown://title"); 1.174 + applyEnsureNoFailures([ 1.175 + {id: resguid, 1.176 + histUri: resuri.spec, 1.177 + title: null, 1.178 + visits: [{date: TIMESTAMP3, 1.179 + type: Ci.nsINavHistoryService.TRANSITION_TYPED}]} 1.180 + ]); 1.181 + do_check_attribute_count(store.getAllIDs(), 3); 1.182 + let queryres = queryHistoryVisits(resuri); 1.183 + do_check_eq(queryres.length, 1); 1.184 + do_check_eq(queryres[0].time, TIMESTAMP3); 1.185 + run_next_test(); 1.186 +}); 1.187 + 1.188 +add_test(function test_invalid_records() { 1.189 + _("Make sure we handle invalid URLs in places databases gracefully."); 1.190 + let connection = PlacesUtils.history 1.191 + .QueryInterface(Ci.nsPIPlacesDatabase) 1.192 + .DBConnection; 1.193 + let stmt = connection.createAsyncStatement( 1.194 + "INSERT INTO moz_places " 1.195 + + "(url, title, rev_host, visit_count, last_visit_date) " 1.196 + + "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")" 1.197 + ); 1.198 + Async.querySpinningly(stmt); 1.199 + stmt.finalize(); 1.200 + // Add the corresponding visit to retain database coherence. 1.201 + stmt = connection.createAsyncStatement( 1.202 + "INSERT INTO moz_historyvisits " 1.203 + + "(place_id, visit_date, visit_type, session) " 1.204 + + "VALUES ((SELECT id FROM moz_places WHERE url = 'invalid-uri'), " 1.205 + + TIMESTAMP3 + ", " + Ci.nsINavHistoryService.TRANSITION_TYPED + ", 1)" 1.206 + ); 1.207 + Async.querySpinningly(stmt); 1.208 + stmt.finalize(); 1.209 + do_check_attribute_count(store.getAllIDs(), 4); 1.210 + 1.211 + _("Make sure we report records with invalid URIs."); 1.212 + let invalid_uri_guid = Utils.makeGUID(); 1.213 + let failed = store.applyIncomingBatch([{ 1.214 + id: invalid_uri_guid, 1.215 + histUri: ":::::::::::::::", 1.216 + title: "Doesn't have a valid URI", 1.217 + visits: [{date: TIMESTAMP3, 1.218 + type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} 1.219 + ]); 1.220 + do_check_eq(failed.length, 1); 1.221 + do_check_eq(failed[0], invalid_uri_guid); 1.222 + 1.223 + _("Make sure we handle records with invalid GUIDs gracefully (ignore)."); 1.224 + applyEnsureNoFailures([ 1.225 + {id: "invalid", 1.226 + histUri: "http://invalid.guid/", 1.227 + title: "Doesn't have a valid GUID", 1.228 + visits: [{date: TIMESTAMP3, 1.229 + type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} 1.230 + ]); 1.231 + 1.232 + _("Make sure we report records with invalid visits, gracefully handle non-integer dates."); 1.233 + let no_date_visit_guid = Utils.makeGUID(); 1.234 + let no_type_visit_guid = Utils.makeGUID(); 1.235 + let invalid_type_visit_guid = Utils.makeGUID(); 1.236 + let non_integer_visit_guid = Utils.makeGUID(); 1.237 + failed = store.applyIncomingBatch([ 1.238 + {id: no_date_visit_guid, 1.239 + histUri: "http://no.date.visit/", 1.240 + title: "Visit has no date", 1.241 + visits: [{date: TIMESTAMP3}]}, 1.242 + {id: no_type_visit_guid, 1.243 + histUri: "http://no.type.visit/", 1.244 + title: "Visit has no type", 1.245 + visits: [{type: Ci.nsINavHistoryService.TRANSITION_EMBED}]}, 1.246 + {id: invalid_type_visit_guid, 1.247 + histUri: "http://invalid.type.visit/", 1.248 + title: "Visit has invalid type", 1.249 + visits: [{date: TIMESTAMP3, 1.250 + type: Ci.nsINavHistoryService.TRANSITION_LINK - 1}]}, 1.251 + {id: non_integer_visit_guid, 1.252 + histUri: "http://non.integer.visit/", 1.253 + title: "Visit has non-integer date", 1.254 + visits: [{date: 1234.567, 1.255 + type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} 1.256 + ]); 1.257 + do_check_eq(failed.length, 3); 1.258 + failed.sort(); 1.259 + let expected = [no_date_visit_guid, 1.260 + no_type_visit_guid, 1.261 + invalid_type_visit_guid].sort(); 1.262 + for (let i = 0; i < expected.length; i++) { 1.263 + do_check_eq(failed[i], expected[i]); 1.264 + } 1.265 + 1.266 + _("Make sure we handle records with javascript: URLs gracefully."); 1.267 + applyEnsureNoFailures([ 1.268 + {id: Utils.makeGUID(), 1.269 + histUri: "javascript:''", 1.270 + title: "javascript:''", 1.271 + visits: [{date: TIMESTAMP3, 1.272 + type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} 1.273 + ]); 1.274 + 1.275 + _("Make sure we handle records without any visits gracefully."); 1.276 + applyEnsureNoFailures([ 1.277 + {id: Utils.makeGUID(), 1.278 + histUri: "http://getfirebug.com", 1.279 + title: "Get Firebug!", 1.280 + visits: []} 1.281 + ]); 1.282 + 1.283 + run_next_test(); 1.284 +}); 1.285 + 1.286 +add_test(function test_remove() { 1.287 + _("Remove an existent record and a non-existent from the store."); 1.288 + applyEnsureNoFailures([{id: fxguid, deleted: true}, 1.289 + {id: Utils.makeGUID(), deleted: true}]); 1.290 + do_check_false(store.itemExists(fxguid)); 1.291 + let queryres = queryHistoryVisits(fxuri); 1.292 + do_check_eq(queryres.length, 0); 1.293 + 1.294 + _("Make sure wipe works."); 1.295 + store.wipe(); 1.296 + do_check_empty(store.getAllIDs()); 1.297 + queryres = queryHistoryVisits(fxuri); 1.298 + do_check_eq(queryres.length, 0); 1.299 + queryres = queryHistoryVisits(tburi); 1.300 + do_check_eq(queryres.length, 0); 1.301 + run_next_test(); 1.302 +}); 1.303 + 1.304 +add_test(function cleanup() { 1.305 + _("Clean up."); 1.306 + PlacesUtils.history.removeAllPages(); 1.307 + run_next_test(); 1.308 +});