michael@0: /* Any copyright is dedicated to the Public Domain. michael@0: http://creativecommons.org/publicdomain/zero/1.0/ */ michael@0: michael@0: Cu.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://services-common/async.js"); michael@0: Cu.import("resource://services-sync/engines/history.js"); michael@0: Cu.import("resource://services-sync/service.js"); michael@0: Cu.import("resource://services-sync/util.js"); michael@0: michael@0: const TIMESTAMP1 = (Date.now() - 103406528) * 1000; michael@0: const TIMESTAMP2 = (Date.now() - 6592903) * 1000; michael@0: const TIMESTAMP3 = (Date.now() - 123894) * 1000; michael@0: michael@0: function queryPlaces(uri, options) { michael@0: let query = PlacesUtils.history.getNewQuery(); michael@0: query.uri = uri; michael@0: let res = PlacesUtils.history.executeQuery(query, options); michael@0: res.root.containerOpen = true; michael@0: michael@0: let results = []; michael@0: for (let i = 0; i < res.root.childCount; i++) michael@0: results.push(res.root.getChild(i)); michael@0: res.root.containerOpen = false; michael@0: return results; michael@0: } michael@0: michael@0: function queryHistoryVisits(uri) { michael@0: let options = PlacesUtils.history.getNewQueryOptions(); michael@0: options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY; michael@0: options.resultType = Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT; michael@0: options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_ASCENDING; michael@0: return queryPlaces(uri, options); michael@0: } michael@0: michael@0: function onNextTitleChanged(callback) { michael@0: PlacesUtils.history.addObserver({ michael@0: onBeginUpdateBatch: function onBeginUpdateBatch() {}, michael@0: onEndUpdateBatch: function onEndUpdateBatch() {}, michael@0: onPageChanged: function onPageChanged() {}, michael@0: onTitleChanged: function onTitleChanged() { michael@0: PlacesUtils.history.removeObserver(this); michael@0: Utils.nextTick(callback); michael@0: }, michael@0: onVisit: function onVisit() {}, michael@0: onDeleteVisits: function onDeleteVisits() {}, michael@0: onPageExpired: function onPageExpired() {}, michael@0: onDeleteURI: function onDeleteURI() {}, michael@0: onClearHistory: function onClearHistory() {}, michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsINavHistoryObserver, michael@0: Ci.nsINavHistoryObserver_MOZILLA_1_9_1_ADDITIONS, michael@0: Ci.nsISupportsWeakReference michael@0: ]) michael@0: }, true); michael@0: } michael@0: michael@0: // Ensure exceptions from inside callbacks leads to test failures while michael@0: // we still clean up properly. michael@0: function ensureThrows(func) { michael@0: return function() { michael@0: try { michael@0: func.apply(this, arguments); michael@0: } catch (ex) { michael@0: PlacesUtils.history.removeAllPages(); michael@0: do_throw(ex); michael@0: } michael@0: }; michael@0: } michael@0: michael@0: let store = new HistoryEngine(Service)._store; michael@0: function applyEnsureNoFailures(records) { michael@0: do_check_eq(store.applyIncomingBatch(records).length, 0); michael@0: } michael@0: michael@0: let fxuri, fxguid, tburi, tbguid; michael@0: michael@0: function run_test() { michael@0: initTestLogging("Trace"); michael@0: run_next_test(); michael@0: } michael@0: michael@0: add_test(function test_store() { michael@0: _("Verify that we've got an empty store to work with."); michael@0: do_check_empty(store.getAllIDs()); michael@0: michael@0: _("Let's create an entry in the database."); michael@0: fxuri = Utils.makeURI("http://getfirefox.com/"); michael@0: michael@0: let place = { michael@0: uri: fxuri, michael@0: title: "Get Firefox!", michael@0: visits: [{ michael@0: visitDate: TIMESTAMP1, michael@0: transitionType: Ci.nsINavHistoryService.TRANSITION_LINK michael@0: }] michael@0: }; michael@0: PlacesUtils.asyncHistory.updatePlaces(place, { michael@0: handleError: function handleError() { michael@0: do_throw("Unexpected error in adding visit."); michael@0: }, michael@0: handleResult: function handleResult() {}, michael@0: handleCompletion: onVisitAdded michael@0: }); michael@0: michael@0: function onVisitAdded() { michael@0: _("Verify that the entry exists."); michael@0: let ids = Object.keys(store.getAllIDs()); michael@0: do_check_eq(ids.length, 1); michael@0: fxguid = ids[0]; michael@0: do_check_true(store.itemExists(fxguid)); michael@0: michael@0: _("If we query a non-existent record, it's marked as deleted."); michael@0: let record = store.createRecord("non-existent"); michael@0: do_check_true(record.deleted); michael@0: michael@0: _("Verify createRecord() returns a complete record."); michael@0: record = store.createRecord(fxguid); michael@0: do_check_eq(record.histUri, fxuri.spec); michael@0: do_check_eq(record.title, "Get Firefox!"); michael@0: do_check_eq(record.visits.length, 1); michael@0: do_check_eq(record.visits[0].date, TIMESTAMP1); michael@0: do_check_eq(record.visits[0].type, Ci.nsINavHistoryService.TRANSITION_LINK); michael@0: michael@0: _("Let's modify the record and have the store update the database."); michael@0: let secondvisit = {date: TIMESTAMP2, michael@0: type: Ci.nsINavHistoryService.TRANSITION_TYPED}; michael@0: onNextTitleChanged(ensureThrows(function() { michael@0: let queryres = queryHistoryVisits(fxuri); michael@0: do_check_eq(queryres.length, 2); michael@0: do_check_eq(queryres[0].time, TIMESTAMP1); michael@0: do_check_eq(queryres[0].title, "Hol Dir Firefox!"); michael@0: do_check_eq(queryres[1].time, TIMESTAMP2); michael@0: do_check_eq(queryres[1].title, "Hol Dir Firefox!"); michael@0: run_next_test(); michael@0: })); michael@0: applyEnsureNoFailures([ michael@0: {id: fxguid, michael@0: histUri: record.histUri, michael@0: title: "Hol Dir Firefox!", michael@0: visits: [record.visits[0], secondvisit]} michael@0: ]); michael@0: } michael@0: }); michael@0: michael@0: add_test(function test_store_create() { michael@0: _("Create a brand new record through the store."); michael@0: tbguid = Utils.makeGUID(); michael@0: tburi = Utils.makeURI("http://getthunderbird.com"); michael@0: onNextTitleChanged(ensureThrows(function() { michael@0: do_check_attribute_count(store.getAllIDs(), 2); michael@0: let queryres = queryHistoryVisits(tburi); michael@0: do_check_eq(queryres.length, 1); michael@0: do_check_eq(queryres[0].time, TIMESTAMP3); michael@0: do_check_eq(queryres[0].title, "The bird is the word!"); michael@0: run_next_test(); michael@0: })); michael@0: applyEnsureNoFailures([ michael@0: {id: tbguid, michael@0: histUri: tburi.spec, michael@0: title: "The bird is the word!", michael@0: visits: [{date: TIMESTAMP3, michael@0: type: Ci.nsINavHistoryService.TRANSITION_TYPED}]} michael@0: ]); michael@0: }); michael@0: michael@0: add_test(function test_null_title() { michael@0: _("Make sure we handle a null title gracefully (it can happen in some cases, e.g. for resource:// URLs)"); michael@0: let resguid = Utils.makeGUID(); michael@0: let resuri = Utils.makeURI("unknown://title"); michael@0: applyEnsureNoFailures([ michael@0: {id: resguid, michael@0: histUri: resuri.spec, michael@0: title: null, michael@0: visits: [{date: TIMESTAMP3, michael@0: type: Ci.nsINavHistoryService.TRANSITION_TYPED}]} michael@0: ]); michael@0: do_check_attribute_count(store.getAllIDs(), 3); michael@0: let queryres = queryHistoryVisits(resuri); michael@0: do_check_eq(queryres.length, 1); michael@0: do_check_eq(queryres[0].time, TIMESTAMP3); michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_invalid_records() { michael@0: _("Make sure we handle invalid URLs in places databases gracefully."); michael@0: let connection = PlacesUtils.history michael@0: .QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .DBConnection; michael@0: let stmt = connection.createAsyncStatement( michael@0: "INSERT INTO moz_places " michael@0: + "(url, title, rev_host, visit_count, last_visit_date) " michael@0: + "VALUES ('invalid-uri', 'Invalid URI', '.', 1, " + TIMESTAMP3 + ")" michael@0: ); michael@0: Async.querySpinningly(stmt); michael@0: stmt.finalize(); michael@0: // Add the corresponding visit to retain database coherence. michael@0: stmt = connection.createAsyncStatement( michael@0: "INSERT INTO moz_historyvisits " michael@0: + "(place_id, visit_date, visit_type, session) " michael@0: + "VALUES ((SELECT id FROM moz_places WHERE url = 'invalid-uri'), " michael@0: + TIMESTAMP3 + ", " + Ci.nsINavHistoryService.TRANSITION_TYPED + ", 1)" michael@0: ); michael@0: Async.querySpinningly(stmt); michael@0: stmt.finalize(); michael@0: do_check_attribute_count(store.getAllIDs(), 4); michael@0: michael@0: _("Make sure we report records with invalid URIs."); michael@0: let invalid_uri_guid = Utils.makeGUID(); michael@0: let failed = store.applyIncomingBatch([{ michael@0: id: invalid_uri_guid, michael@0: histUri: ":::::::::::::::", michael@0: title: "Doesn't have a valid URI", michael@0: visits: [{date: TIMESTAMP3, michael@0: type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} michael@0: ]); michael@0: do_check_eq(failed.length, 1); michael@0: do_check_eq(failed[0], invalid_uri_guid); michael@0: michael@0: _("Make sure we handle records with invalid GUIDs gracefully (ignore)."); michael@0: applyEnsureNoFailures([ michael@0: {id: "invalid", michael@0: histUri: "http://invalid.guid/", michael@0: title: "Doesn't have a valid GUID", michael@0: visits: [{date: TIMESTAMP3, michael@0: type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} michael@0: ]); michael@0: michael@0: _("Make sure we report records with invalid visits, gracefully handle non-integer dates."); michael@0: let no_date_visit_guid = Utils.makeGUID(); michael@0: let no_type_visit_guid = Utils.makeGUID(); michael@0: let invalid_type_visit_guid = Utils.makeGUID(); michael@0: let non_integer_visit_guid = Utils.makeGUID(); michael@0: failed = store.applyIncomingBatch([ michael@0: {id: no_date_visit_guid, michael@0: histUri: "http://no.date.visit/", michael@0: title: "Visit has no date", michael@0: visits: [{date: TIMESTAMP3}]}, michael@0: {id: no_type_visit_guid, michael@0: histUri: "http://no.type.visit/", michael@0: title: "Visit has no type", michael@0: visits: [{type: Ci.nsINavHistoryService.TRANSITION_EMBED}]}, michael@0: {id: invalid_type_visit_guid, michael@0: histUri: "http://invalid.type.visit/", michael@0: title: "Visit has invalid type", michael@0: visits: [{date: TIMESTAMP3, michael@0: type: Ci.nsINavHistoryService.TRANSITION_LINK - 1}]}, michael@0: {id: non_integer_visit_guid, michael@0: histUri: "http://non.integer.visit/", michael@0: title: "Visit has non-integer date", michael@0: visits: [{date: 1234.567, michael@0: type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} michael@0: ]); michael@0: do_check_eq(failed.length, 3); michael@0: failed.sort(); michael@0: let expected = [no_date_visit_guid, michael@0: no_type_visit_guid, michael@0: invalid_type_visit_guid].sort(); michael@0: for (let i = 0; i < expected.length; i++) { michael@0: do_check_eq(failed[i], expected[i]); michael@0: } michael@0: michael@0: _("Make sure we handle records with javascript: URLs gracefully."); michael@0: applyEnsureNoFailures([ michael@0: {id: Utils.makeGUID(), michael@0: histUri: "javascript:''", michael@0: title: "javascript:''", michael@0: visits: [{date: TIMESTAMP3, michael@0: type: Ci.nsINavHistoryService.TRANSITION_EMBED}]} michael@0: ]); michael@0: michael@0: _("Make sure we handle records without any visits gracefully."); michael@0: applyEnsureNoFailures([ michael@0: {id: Utils.makeGUID(), michael@0: histUri: "http://getfirebug.com", michael@0: title: "Get Firebug!", michael@0: visits: []} michael@0: ]); michael@0: michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function test_remove() { michael@0: _("Remove an existent record and a non-existent from the store."); michael@0: applyEnsureNoFailures([{id: fxguid, deleted: true}, michael@0: {id: Utils.makeGUID(), deleted: true}]); michael@0: do_check_false(store.itemExists(fxguid)); michael@0: let queryres = queryHistoryVisits(fxuri); michael@0: do_check_eq(queryres.length, 0); michael@0: michael@0: _("Make sure wipe works."); michael@0: store.wipe(); michael@0: do_check_empty(store.getAllIDs()); michael@0: queryres = queryHistoryVisits(fxuri); michael@0: do_check_eq(queryres.length, 0); michael@0: queryres = queryHistoryVisits(tburi); michael@0: do_check_eq(queryres.length, 0); michael@0: run_next_test(); michael@0: }); michael@0: michael@0: add_test(function cleanup() { michael@0: _("Clean up."); michael@0: PlacesUtils.history.removeAllPages(); michael@0: run_next_test(); michael@0: });