Merge pull request #970 from sarracini/replace_simple_storage

feat(addon): #928 Replace simplestorage to read/write from metadata db
This commit is contained in:
Ursula Sarracini 2016-07-28 16:03:31 -04:00 коммит произвёл GitHub
Родитель 4a77f683b9 0702bab578
Коммит 2fc7a046d5
8 изменённых файлов: 266 добавлений и 393 удалений

Просмотреть файл

@ -19,13 +19,16 @@ function getBackgroundRGB(site) {
if (site.favicon_color) {
return site.favicon_color;
}
if (site.favicons && site.favicons[0] && site.favicons[0].color) {
return site.favicons[0].color;
}
if (site.favicon_colors && site.favicon_colors[0] && site.favicon_colors[0].color) {
return site.favicon_colors[0].color;
}
const favicon = site.favicon_url || site.favicon;
const parsedUrl = site.parsedUrl || urlParse(site.url || "") ;
const label = prettyUrl(parsedUrl.hostname);
const {favicon, label} = selectSiteProperties(site);
return favicon ? DEFAULT_FAVICON_BG_COLOR : getRandomColor(label);
}
@ -126,12 +129,18 @@ module.exports.selectNewTabSites = createSelector(
}
);
function selectSiteProperties(site) {
const metadataFavicon = site.favicons && site.favicons[0] && site.favicons[0].url;
const favicon = site.favicon_url || metadataFavicon || site.favicon;
const parsedUrl = site.parsedUrl || urlParse(site.url || "") ;
const label = prettyUrl(parsedUrl.hostname);
return {favicon, parsedUrl, label};
}
const selectSiteIcon = createSelector(
site => site,
site => {
const favicon = site.favicon_url || site.favicon;
const parsedUrl = site.parsedUrl || urlParse(site.url || "") ;
const label = prettyUrl(parsedUrl.hostname);
const {favicon, parsedUrl, label} = selectSiteProperties(site);
const backgroundRGB = getBackgroundRGB(site);
const backgroundColor = site.background_color || toRGBString(...backgroundRGB, favicon ? BACKGROUND_FADE : 1);
const fontColor = getBlackOrWhite(...backgroundRGB);

Просмотреть файл

@ -211,6 +211,10 @@ describe("selectors", () => {
url: "http://foo.com",
favicon_url: "http://foo.com/favicon.ico",
favicon: "http://foo.com/favicon-16.ico",
favicons: [{
colors: [11, 11, 11],
url: "http://foo.com/metadatafavicon.ico"
}],
favicon_colors: [{color: [11, 11, 11]}]
};
let state;
@ -230,10 +234,14 @@ describe("selectors", () => {
it("should select favicon_url", () => {
assert.equal(state.favicon, siteWithFavicon.favicon_url);
});
it("should fall back to favicon_url", () => {
state = selectSiteIcon(Object.assign({}, siteWithFavicon, {favicon_url: null}));
it("should fall back to favicon", () => {
state = selectSiteIcon(Object.assign({}, siteWithFavicon, {favicon_url: null, favicons: null}));
assert.equal(state.favicon, siteWithFavicon.favicon);
});
it("should use favicons[0].url if exists", () => {
state = selectSiteIcon(Object.assign({}, siteWithFavicon, {favicon_url: null}));
assert.equal(state.favicon, siteWithFavicon.favicons[0].url);
});
it("should select the first letter of the hostname", () => {
state = selectSiteIcon(Object.assign({}, siteWithFavicon, {url: "http://kate.com"}));
assert.equal(state.firstLetter, "k");
@ -246,7 +254,7 @@ describe("selectors", () => {
assert.equal(state.backgroundColor, `rgba(11, 11, 11, ${selectSiteIcon.BACKGROUND_FADE})`);
});
it("should create an opaque background color if there is no favicon", () => {
state = selectSiteIcon(Object.assign({}, siteWithFavicon, {favicon_url: null, favicon: null}));
state = selectSiteIcon(Object.assign({}, siteWithFavicon, {favicon_url: null, favicon: null, favicons: null}));
assert.equal(state.backgroundColor, "rgba(11, 11, 11, 1)");
});
it("should create a random background color if no favicon color exists", () => {
@ -323,12 +331,18 @@ describe("getBackgroundRGB", () => {
[11, 11, 11]
);
});
it("should use favicons[0].color if available", () => {
assert.deepEqual(
getBackgroundRGB({url: "http://foo.com", favicons: [{color: [11, 11, 11]}]}),
[11, 11, 11]
);
});
it("should use a default bg if a favicon is supplied", () => {
const result = getBackgroundRGB({url: "http://foo.com", favicon_url: "adsd.ico"});
assert.ok(result);
assert.deepEqual(result, DEFAULT_FAVICON_BG_COLOR);
});
it("should use a random color if no favicon_colors or favicon", () => {
it("should use a random color if no favicon_colors or favicon or favicons[0].color", () => {
const result = getBackgroundRGB({url: "http://foo.com"});
assert.ok(result);
assert.notDeepEqual(result, DEFAULT_FAVICON_BG_COLOR);

Просмотреть файл

@ -46,7 +46,6 @@ const DEFAULT_OPTIONS = {
pageURL: data.url("content/activity-streams.html"),
onAddWorker: null,
onRemoveWorker: null,
previewCacheTimeout: 21600000, // every 6 hours, rebuild/repopulate the cache
placesCacheTimeout: 1800000, // every 30 minutes, rebuild/repopulate the cache
recommendationTTL: 3600000, // every hour, get a new recommendation
};
@ -102,14 +101,10 @@ function ActivityStreams(metadataStore, options = {}) {
this._populatingCache = {
places: false,
preview: false,
};
this._asyncBuildPlacesCache();
this._asyncBuildPreviewCache();
this._startPeriodicBuildPreviewCache(this.options.previewCacheTimeout);
// Only create RecommendationProvider if they are in the experiment
if (this._experimentProvider.data.recommendedHighlight) {
this._recommendationProvider = new RecommendationProvider(this._previewProvider, this._tabTracker);
@ -124,7 +119,6 @@ ActivityStreams.prototype = {
_pagemod: null,
_button: null,
_previewCacheTimeoutID: null,
_newRecommendationTimeoutID: null,
/**
@ -433,53 +427,6 @@ ActivityStreams.prototype = {
}
}),
/**
* Builds a preview cache with links from a normal content page load
*/
_asyncBuildPreviewCache: Task.async(function*() {
if (this._populatingCache && !this._populatingCache.preview) {
this._populatingCache.preview = true;
let placesLinks = [];
let promises = [];
promises.push(PlacesProvider.links.getTopFrecentSites().then(links => {
placesLinks.push(...links);
}));
promises.push(PlacesProvider.links.getRecentBookmarks().then(links => {
placesLinks.push(...links);
}));
promises.push(PlacesProvider.links.getRecentLinks().then(links => {
placesLinks.push(...links);
}));
promises.push(PlacesProvider.links.getHighlightsLinks().then(links => {
placesLinks.push(...links);
}));
yield Promise.all(promises);
const event = this._tabTracker.generateEvent({source: "BUILD_PREVIEW_CACHE"});
yield this._previewProvider.asyncBuildCache(placesLinks, event);
this._populatingCache.preview = false;
Services.obs.notifyObservers(null, "activity-streams-previews-cache-complete", null);
}
}),
/**
* Set up preview cache to be repopulated every 6 hours
*/
_startPeriodicBuildPreviewCache(previewCacheTimeout) {
if (previewCacheTimeout) {
// only set a timeout if a non-null value is provided otherwise this will
// effectively be an infinite loop
this._previewCacheTimeoutID = setTimeout(() => {
this._asyncBuildPreviewCache();
this._startPeriodicBuildPreviewCache(previewCacheTimeout);
}, previewCacheTimeout);
}
},
/**
* Start a timer to fetch a new recommendation every hour. This will only
* run for those in the experiment
@ -625,7 +572,6 @@ ActivityStreams.prototype = {
*/
unload(reason) { // eslint-disable-line no-unused-vars
let defaultUnload = () => {
clearTimeout(this._previewCacheTimeoutID);
clearTimeout(this._placesCacheTimeoutID);
if (this._newRecommendationTimeoutID) {
clearTimeout(this._newRecommendationTimeoutID);
@ -647,7 +593,6 @@ ActivityStreams.prototype = {
this._memoizer.uninit();
this._populatingCache = {
places: false,
preview: false,
};
};
@ -656,7 +601,6 @@ ActivityStreams.prototype = {
case "disable":
case "uninstall":
this._tabTracker.handleUserEvent({event: reason});
this._previewProvider.clearCache();
this._unsetHomePage();
defaultUnload();
break;

Просмотреть файл

@ -1,8 +1,7 @@
/* globals Task, require, exports, Services */
/* globals Task, require, exports */
"use strict";
const {Cu} = require("chrome");
const ss = require("sdk/simple-storage");
const simplePrefs = require("sdk/simple-prefs");
const self = require("sdk/self");
const {TippyTopProvider} = require("lib/TippyTopProvider");
@ -26,12 +25,9 @@ const URL_FILTERS = [
Cu.importGlobalProperties(["fetch"]);
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/Task.jsm");
Cu.import("resource://gre/modules/Services.jsm");
const DEFAULT_OPTIONS = {
cacheCleanupPeriod: 86400000, // a cache clearing job runs at most once every 24 hours
cacheRefreshAge: 259200000, // refresh a link every 3 days
cacheTTL: 2592000000, // cached items expire if they haven't been accessed in 30 days
metadataTTL: 3 * 24 * 60 * 60 * 1000, // 3 days for the metadata to live
proxyMaxLinks: 25, // number of links embedly proxy accepts per request
initFresh: false,
};
@ -47,38 +43,6 @@ function PreviewProvider(tabTracker, metadataStore, options = {}) {
PreviewProvider.prototype = {
/**
* Clean-up the preview cache
*/
cleanUpCacheMaybe(force = false) {
let currentTime = Date.now();
this._setupState(currentTime);
// exit if the last cleanup exceeds a threshold
if (!force && (currentTime - ss.storage.previewCacheState.lastCleanup) < this.options.cacheCleanupPeriod) {
return;
}
for (let key in ss.storage.embedlyData) {
// in case accessTime is not set, don't crash, but don't clean up
let accessTime = ss.storage.embedlyData[key].accessTime;
if (!accessTime) {
ss.storage.embedlyData[key].accessTime = Date.now();
}
if ((currentTime - accessTime) > this.options.cacheTTL) {
delete ss.storage.embedlyData[key];
}
}
ss.storage.previewCacheState.lastCleanup = currentTime;
Services.obs.notifyObservers(null, "activity-streams-preview-cache-cleanup", null);
},
_setupState(stateTime) {
if (!ss.storage.previewCacheState) {
ss.storage.previewCacheState = {lastCleanup: stateTime};
}
},
_onPrefChange(prefName) {
if (ALLOWED_PREFS.has(prefName)) {
switch (prefName) {
@ -86,12 +50,6 @@ PreviewProvider.prototype = {
this._embedlyEndpoint = simplePrefs.prefs[EMBEDLY_PREF];
break;
case ENABLED_PREF:
if (this.enabled) {
this.clearCache();
} else {
// if enabling, create cache
ss.storage.embedlyData = {};
}
this.enabled = simplePrefs.prefs[ENABLED_PREF];
break;
}
@ -108,7 +66,7 @@ PreviewProvider.prototype = {
},
/**
* Santize the URL to remove any unwanted or sensitive information about the link
* Sanitize the URL to remove any unwanted or sensitive information about the link
*/
_sanitizeURL(url) {
if (!url) {
@ -179,7 +137,7 @@ PreviewProvider.prototype = {
.map(link => {
const sanitizedURL = this._sanitizeURL(link.url);
const cacheKey = this._createCacheKey(sanitizedURL);
return Object.assign({}, link, {sanitized_url: sanitizedURL, cache_key: cacheKey});
return Object.assign({}, link, {sanitized_url: sanitizedURL, cache_key: cacheKey, places_url: link.url});
});
},
@ -211,9 +169,8 @@ PreviewProvider.prototype = {
this._asyncSaveLinks(processedLinks, event);
}
let cachedLinks = this._getEnhancedLinks(processedLinks, previewsOnly, event);
return this._getFaviconColors(cachedLinks).then(linksToSend => {
return linksToSend;
return this._asyncGetEnhancedLinks(processedLinks, previewsOnly, event).then(cachedLinks => {
return this._getFaviconColors(cachedLinks);
});
},
@ -222,14 +179,16 @@ PreviewProvider.prototype = {
* Also, collect some metrics on how many links were returned by PlacesProvider vs how
* how many were returned by the cache
*/
_getEnhancedLinks(processedLinks, previewsOnly, event) {
_asyncGetEnhancedLinks: Task.async(function*(processedLinks, previewsOnly, event) {
this._tabTracker.handlePerformanceEvent(event, "previewCacheRequest", processedLinks.length);
if (!this.enabled) {
return processedLinks;
}
let now = Date.now();
// Collect all items in the DB that we requested and create a mapping between that
// object's metadata and it's cache key
let dbLinks = yield this._asyncFindItemsInDB(processedLinks);
let existingLinks = new Map();
dbLinks.forEach(item => existingLinks.set(item.cache_key, item));
let results = processedLinks.map(link => {
if (!link) {
return link;
@ -238,54 +197,40 @@ PreviewProvider.prototype = {
// Add tippy top data, if available
link = this._tippyTopProvider.processSite(link);
if (link.cache_key && ss.storage.embedlyData[link.cache_key]) {
ss.storage.embedlyData[link.cache_key].accessTime = now;
return Object.assign({}, ss.storage.embedlyData[link.cache_key], link);
// Find the item in the map and return it if it exists
if (existingLinks.has(link.cache_key)) {
return Object.assign({}, existingLinks.get(link.cache_key), link);
} else {
return previewsOnly ? null : link;
}
})
.filter(link => link);
}).filter(link => link);
// gives the opportunity to have at least one run with an old
// preview cache. This is for the case where one hasn't opened
// the browser since `cacheTTL`
this.cleanUpCacheMaybe();
this._tabTracker.handlePerformanceEvent(event, "previewCacheHits", results.length);
this._tabTracker.handlePerformanceEvent(event, "previewCacheMisses", processedLinks.length - results.length);
return results;
},
}),
/**
* Determine if a cached link has expired
* Find the metadata for each link in the database
*/
_isLinkExpired(link) {
const cachedLink = ss.storage.embedlyData[link.cache_key];
if (!cachedLink) {
return false;
}
let currentTime = Date.now();
return (currentTime - cachedLink.refreshTime) > this.options.cacheRefreshAge;
},
/**
* Build the preview cache
*/
asyncBuildCache: Task.async(function*(placesLinks, event = {}) {
let processedLinks = this._processLinks(placesLinks);
yield this._asyncSaveLinks(processedLinks, event);
_asyncFindItemsInDB: Task.async(function*(links) {
const cacheKeyArray = links.map(link => link.cache_key);
let linksMetadata = yield this._metadataStore.asyncGetMetadataByCacheKey(cacheKeyArray);
return linksMetadata;
}),
/**
* Request links from embedly, optionally filtering out known links
*/
_asyncSaveLinks: Task.async(function*(processedLinks, event, updateAccessTime = true) {
_asyncSaveLinks: Task.async(function*(processedLinks, event) {
let dbLinks = yield this._asyncFindItemsInDB(processedLinks);
let existingLinks = new Set();
dbLinks.forEach(item => existingLinks.add(item.cache_key));
let linksList = this._uniqueLinks(processedLinks)
// If a request is in progress, don't re-request it
.filter(link => !this._alreadyRequested.has(link.cache_key))
// If we already have the link in the cache, don't request it again...
// ... UNLESS it has expired
.filter(link => !ss.storage.embedlyData[link.cache_key] || this._isLinkExpired(link));
// If we already have the link in the database don't request it again
.filter(link => !existingLinks.has(link.cache_key));
linksList.forEach(link => this._alreadyRequested.add(link.cache_key));
@ -297,7 +242,7 @@ PreviewProvider.prototype = {
}
// for each bundle of 25 links, create a new request to embedly
requestQueue.forEach(requestBundle => {
promises.push(this._asyncFetchAndCache(requestBundle, event, updateAccessTime));
promises.push(this._asyncFetchAndStore(requestBundle, event));
});
yield Promise.all(promises);
}),
@ -320,11 +265,11 @@ PreviewProvider.prototype = {
}),
/**
* Extracts data from embedly and caches it.
* Extracts data from embedly and saves in the MetadataStore
* Also, collect metrics on how many requests were made, how much time each
* request took to complete, and their success or failure status
*/
_asyncFetchAndCache: Task.async(function*(newLinks, event, updateAccessTime = true) {
_asyncFetchAndStore: Task.async(function*(newLinks, event) {
if (!this.enabled) {
return;
}
@ -342,18 +287,9 @@ PreviewProvider.prototype = {
let responseJson = yield response.json();
this._tabTracker.handlePerformanceEvent(event, "embedlyProxyRequestReceivedCount", responseJson.urls.length);
this._tabTracker.handlePerformanceEvent(event, "embedlyProxyRequestSucess", 1);
let currentTime = Date.now();
newLinks.forEach(link => {
let data = responseJson.urls[link.sanitized_url];
if (!data) {
return;
}
ss.storage.embedlyData[link.cache_key] = Object.assign({}, ss.storage.embedlyData[link.cache_key], data);
if (updateAccessTime) {
ss.storage.embedlyData[link.cache_key].accessTime = currentTime;
}
ss.storage.embedlyData[link.cache_key].refreshTime = currentTime;
});
let linksToInsert = newLinks.filter(link => responseJson.urls[link.sanitized_url])
.map(link => Object.assign({}, link, responseJson.urls[link.sanitized_url], {expired_at: (this.options.metadataTTL) + Date.now()}));
this._metadataStore.asyncInsert(linksToInsert);
} else {
this._tabTracker.handlePerformanceEvent(event, "embedlyProxyFailure", 1);
}
@ -367,25 +303,13 @@ PreviewProvider.prototype = {
}),
/**
* Initialize the simple storage
* Initialize Preview Provider
*/
init() {
this._alreadyRequested = new Set();
this._embedlyEndpoint = simplePrefs.prefs[EMBEDLY_PREF] + EMBEDLY_VERSION_QUERY + self.version;
this.enabled = simplePrefs.prefs[ENABLED_PREF];
if (!ss.storage.embedlyData || this.options.initFresh) {
ss.storage.embedlyData = {};
}
simplePrefs.on("", this._onPrefChange);
this._setupState(Date.now());
},
/**
* Clear out the preview cache
*/
clearCache() {
delete ss.storage.previewCacheState;
delete ss.storage.embedlyData;
},
/**

Просмотреть файл

@ -57,7 +57,9 @@ function getTestActivityStream(options = {}) {
const mockMetadataStore = {
asyncConnect() { return Promise.resolve();},
asyncReset() { return Promise.resolve();},
asyncClose() { return Promise.resolve();}
asyncClose() { return Promise.resolve();},
asyncInsert() { return Promise.resolve();},
asyncGetMetadataByCacheKey() { return Promise.resolve([]);},
};
let mockApp = new ActivityStreams(mockMetadataStore, options);
return mockApp;

Просмотреть файл

@ -1,71 +0,0 @@
/* globals XPCOMUtils */
"use strict";
const {Cu} = require("chrome");
const {before, after} = require("sdk/test/utils");
const test = require("sdk/test");
const {getTestActivityStream} = require("./lib/utils");
const simplePrefs = require("sdk/simple-prefs");
const {PlacesProvider} = require("lib/PlacesProvider");
const {PreviewProvider} = require("lib/PreviewProvider");
const {makeCachePromise, makeCountingCachePromise} = require("./lib/cachePromises");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
"resource://gre/modules/PlacesUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
let gInitialCachePref = simplePrefs.prefs["query.cache"];
let gPreviewProvider;
exports["test preview cache repopulation works"] = function*(assert) {
let placesCachePromise;
let previewsCachePromise;
placesCachePromise = makeCachePromise("places");
previewsCachePromise = makeCachePromise("previews");
let app = getTestActivityStream({previewCacheTimeout: 100});
yield placesCachePromise;
yield previewsCachePromise;
let expectedRepopulations = 3;
let previewsCountPromise = makeCountingCachePromise("previews", expectedRepopulations);
let numRepopulations = yield previewsCountPromise;
assert.equal(numRepopulations, expectedRepopulations, "preview cache successfully repopulated periodically");
app.unload();
};
exports["test places cache repopulation works"] = function*(assert) {
let placesCachePromise = makeCachePromise("places");
let app = getTestActivityStream({placesCacheTimeout: 100});
yield placesCachePromise;
let expectedRepopulations = 3;
let placesCountPromise = makeCountingCachePromise("places", expectedRepopulations);
let numRepopulations = yield placesCountPromise;
assert.equal(numRepopulations, expectedRepopulations, "places cache successfully repopulated periodically");
app.unload();
};
before(exports, function*() {
simplePrefs.prefs["query.cache"] = true;
PlacesProvider.links.init();
gPreviewProvider = new PreviewProvider({initFresh: true});
});
after(exports, function() {
gPreviewProvider.clearCache();
gPreviewProvider.uninit();
PlacesProvider.links.uninit();
simplePrefs.prefs["query.cache"] = gInitialCachePref || false;
});
test.run(exports);

Просмотреть файл

@ -0,0 +1,142 @@
/* globals require, exports */
"use strict";
const {before, after, waitUntil} = require("sdk/test/utils");
const simplePrefs = require("sdk/simple-prefs");
const {Loader} = require("sdk/test/loader");
const loader = Loader(module);
const httpd = loader.require("./lib/httpd");
const {PreviewProvider} = require("lib/PreviewProvider");
const {MetadataStore} = require("lib/MetadataStore");
const {metadataFixture} = require("./lib/MetastoreFixture.js");
const gMetadataStore = new MetadataStore();
const gPort = 8079;
let gPreviewProvider;
let gPrefEmbedly = simplePrefs.prefs["embedly.endpoint"];
let gPrefEnabled = simplePrefs.prefs["previews.enabled"];
exports.test_metadatastore_saves_new_links = function*(assert) {
// to start, put some links in the database
yield gMetadataStore.asyncInsert(metadataFixture);
let items = yield gMetadataStore.asyncExecuteQuery("SELECT * FROM page_metadata");
assert.equal(items.length, 3, "sanity check that we only have 3 items in the database to start");
// these are links we are going to request - the first two should get filtered out
const links = [
{cache_key: "https://www.mozilla.org/", places_url: "https://www.mozilla.org/"},
{cache_key: "https://www.mozilla.org/en-US/firefox/new/", places_url: "https://www.mozilla.org/en-US/firefox/new"},
{cache_key: "https://notinDB.com/", places_url: "https://www.notinDB.com/", sanitized_url: "https://www.notinDB.com/"}];
const fakeResponse = {"urls": {
"https://www.notinDB.com/": {
"embedlyMetaData": "some embedly metadata"
}
}};
let srv = httpd.startServerAsync(gPort);
srv.registerPathHandler("/previewProviderMetadataStore", function handle(request, response) {
response.setHeader("Content-Type", "application/json", false);
response.write(JSON.stringify(fakeResponse));
});
// only request the items that are not in the database and wait for them to
// successfully be inserted
yield gPreviewProvider._asyncSaveLinks(links);
// asyncSaveLinks doesn't yield on inserting in the db so we need to wait
// until it has successfully finished the transaction before checking
yield waitUntil(() => !gMetadataStore.transactionInProgress);
// check that it inserted the link that was filtered out
items = yield gMetadataStore.asyncExecuteQuery("SELECT * FROM page_metadata");
assert.equal(items.length, 4, "it should now have a length of 4");
assert.equal(items[3][1], links[2].cache_key, "it newly inserted the one we didn't already have");
yield new Promise(resolve => {
srv.stop(resolve);
});
};
exports.test_find_correct_links = function*(assert) {
yield gMetadataStore.asyncInsert(metadataFixture);
const links = [
{cache_key: "https://www.mozilla.org/", places_url: "https://www.mozilla.org/"},
{cache_key: "https://www.mozilla.org/en-US/firefox/new/", places_url: "https://www.mozilla.org/en-US/firefox/new"},
{cache_key: "https://www.notinDB.com/", places_url: "https://www.notinDB.com/"}];
// find the items in the database, based on their cache keys
const dbLinks = yield gPreviewProvider._asyncFindItemsInDB(links);
assert.equal(dbLinks.length, 2, "returned two items out of the three based on their cache_key");
assert.equal(dbLinks[0].cache_key, links[0].cache_key, "correctly returned the first link");
assert.equal(dbLinks[1].cache_key, links[1].cache_key, "correctly returned the second link");
};
exports.test_get_links_from_metadatastore = function*(assert) {
yield gMetadataStore.asyncInsert(metadataFixture);
const links = [
{cache_key: "https://www.mozilla.org/", places_url: "https://www.mozilla.org/"},
{cache_key: "https://www.mozilla.org/en-US/firefox/new/", places_url: "https://www.mozilla.org/en-US/firefox/new"},
{cache_key: "https://www.notinDB.com/", places_url: "https://www.notinDB.com/"}];
// get enhanced links - the third link should be returned as is since it
// is not yet in the database
let cachedLinks = yield gPreviewProvider._asyncGetEnhancedLinks(links);
assert.equal(cachedLinks.length, 3, "returned all 3 links");
assert.deepEqual(cachedLinks[2], links[2], "the third link was untouched");
// get enhanced links after third link has been inserted in db - the third
// link should now have more properties i.e title, description etc...
yield gMetadataStore.asyncInsert([links[2]]);
cachedLinks = yield gPreviewProvider._asyncGetEnhancedLinks(links);
assert.equal(cachedLinks.length, 3, "returned all 3 links");
assert.equal(cachedLinks[2].title, null, "the third link has a title field");
assert.equal(cachedLinks[2].description, null, "the third links has a description field");
assert.deepEqual(cachedLinks[2].images, [], "the third links has images field");
assert.deepEqual(cachedLinks[2].favicons, [], "the third links has favicons field");
};
function waitForAsyncReset() {
return waitUntil(function*() {
if (gMetadataStore.transactionInProgress) {
return false;
}
try {
let nMetadata = yield gMetadataStore.asyncExecuteQuery(
"SELECT count(*) as count FROM page_metadata",
{"columns": ["count"]});
let nImages = yield gMetadataStore.asyncExecuteQuery(
"SELECT count(*) as count FROM page_images",
{"columns": ["count"]});
let nMetadataImages = yield gMetadataStore.asyncExecuteQuery(
"SELECT count(*) as count FROM page_metadata_images",
{"columns": ["count"]});
return !nMetadata[0].count &&
!nImages[0].count &&
!nMetadataImages[0].count;
} catch (e) {
/* ignore whatever error that makes the query above fail */
return false;
}
}, 10);
}
before(exports, function*() {
simplePrefs.prefs["embedly.endpoint"] = `http://localhost:${gPort}/previewProviderMetadataStore`;
simplePrefs.prefs["previews.enabled"] = true;
yield gMetadataStore.asyncConnect();
let mockTabTracker = {handlePerformanceEvent: function() {}, generateEvent: function() {}};
gPreviewProvider = new PreviewProvider(mockTabTracker, gMetadataStore, {initFresh: true});
});
after(exports, function*() {
simplePrefs.prefs["embedly.endpoint"] = gPrefEmbedly;
simplePrefs.prefs["previews.enabled"] = gPrefEnabled;
yield gMetadataStore.asyncReset();
yield waitForAsyncReset();
yield gMetadataStore.asyncClose();
gPreviewProvider.uninit();
});
require("sdk/test").run(exports);

Просмотреть файл

@ -1,4 +1,4 @@
/* globals require, exports, Services, NetUtil */
/* globals require, exports, NetUtil */
"use strict";
@ -6,10 +6,8 @@ const {before, after} = require("sdk/test/utils");
const simplePrefs = require("sdk/simple-prefs");
const self = require("sdk/self");
const {Loader} = require("sdk/test/loader");
const {setTimeout} = require("sdk/timers");
const loader = Loader(module);
const httpd = loader.require("./lib/httpd");
const ss = require("sdk/simple-storage");
const {Cu} = require("chrome");
const {PreviewProvider} = require("lib/PreviewProvider");
const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);
@ -22,88 +20,14 @@ const URL_FILTERS = [
];
Cu.importGlobalProperties(["URL"]);
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/NetUtil.jsm");
const gPort = 8089;
let gPreviewProvider;
let gMetadataStore = [];
let gPrefEmbedly = simplePrefs.prefs["embedly.endpoint"];
let gPrefEnabled = simplePrefs.prefs["previews.enabled"];
exports.test_cache_invalidation = function*(assert) {
let currentTime = Date.now();
let fortyDaysAgo = currentTime - (40 * 24 * 60 * 60 * 1000);
ss.storage.embedlyData.item_1 = {accessTime: fortyDaysAgo};
ss.storage.embedlyData.item_2 = {accessTime: currentTime};
assert.equal(Object.keys(ss.storage.embedlyData).length, 2, "items set");
gPreviewProvider.cleanUpCacheMaybe(true);
assert.equal(Object.keys(ss.storage.embedlyData).length, 1, "items cleaned up");
};
exports.test_enabling = function*(assert) {
assert.equal(Object.keys(ss.storage.embedlyData).length, 0, "empty object");
simplePrefs.prefs["previews.enabled"] = false;
assert.equal(ss.storage.embedlyData, undefined, "embedlyData is undefined");
simplePrefs.prefs["previews.enabled"] = true;
assert.equal(Object.keys(ss.storage.embedlyData).length, 0, "empty object");
};
exports.test_access_update = function*(assert) {
let currentTime = Date.now();
let twoDaysAgo = currentTime - (2 * 24 * 60 * 60 * 1000);
ss.storage.embedlyData.item_1 = {accessTime: twoDaysAgo};
gPreviewProvider._getEnhancedLinks([{cache_key: "item_1"}]);
assert.ok(ss.storage.embedlyData.item_1.accessTime > twoDaysAgo, "access time is updated");
};
exports.test_long_hibernation = function*(assert) {
let currentTime = Date.now();
let fortyDaysAgo = currentTime - (40 * 24 * 60 * 60 * 1000);
ss.storage.embedlyData.item_1 = {accessTime: fortyDaysAgo};
gPreviewProvider._getEnhancedLinks([{cache_key: "item_1"}]);
assert.ok(ss.storage.embedlyData.item_1.accessTime >= currentTime, "access time is updated");
};
exports.test_periodic_cleanup = function*(assert) {
let oldThreshold = gPreviewProvider.options.cacheCleanupPeriod;
gPreviewProvider.options.cacheCleanupPeriod = 30;
let countingCleanupPromise = new Promise(resolve => {
let notif = "activity-streams-preview-cache-cleanup";
let count = 0;
let waitForNotif = (subject, topic, data) => {
if (topic === notif) {
count++;
if (count === 3) {
Services.obs.removeObserver(waitForNotif, notif);
resolve(count);
}
}
};
Services.obs.addObserver(waitForNotif, notif);
});
let countingRunsPromise = new Promise(resolve => {
let runCount = 0;
let periodicCleanups = () => {
setTimeout(() => {
gPreviewProvider.cleanUpCacheMaybe();
runCount++;
if (runCount >= 6) {
resolve(runCount);
} else {
periodicCleanups();
}
}, 20);
};
periodicCleanups();
});
let values = yield Promise.all([countingRunsPromise, countingCleanupPromise]);
assert.equal(JSON.stringify(values), JSON.stringify([6, 3]), "expected counts are obtained");
gPreviewProvider.options.cacheCleanupPeriod = oldThreshold;
};
exports.test_only_request_links_once = function*(assert) {
const msg1 = [{"url": "a.com", "sanitized_url": "a.com", "cache_key": "a.com"},
{"url": "b.com", "sanitized_url": "b.com", "cache_key": "b.com"},
@ -124,15 +48,18 @@ exports.test_only_request_links_once = function*(assert) {
request.bodyInputStream.available()
)
);
// count the times each url has been requested
data.urls.forEach(url => urlsRequested[url] = (urlsRequested[url] + 1) || 1);
response.setHeader("Content-Type", "application/json", false);
response.write(JSON.stringify({"urls": {urlsRequested}}));
});
// request 'b.com' and 'c.com' twice
gPreviewProvider._asyncSaveLinks(msg1);
yield gPreviewProvider._asyncSaveLinks(msg2);
for (let url in urlsRequested) {
// each url should have a count of just one
assert.equal(urlsRequested[url], 1, "URL was requested only once");
}
@ -141,49 +68,6 @@ exports.test_only_request_links_once = function*(assert) {
});
};
exports.test_is_link_expired = function(assert) {
const refreshTime = Date.now() - (gPreviewProvider.options.cacheRefreshAge + 1000);
ss.storage.embedlyData["a.com"] = {"url": "a.com", "sanitized_url": "a.com", "cache_key": "a.com", refreshTime};
assert.equal(gPreviewProvider._isLinkExpired({cache_key: "a.com"}), true, "expired link should return true");
ss.storage.embedlyData["b.com"] = {"url": "b.com", "sanitized_url": "b.com", "cache_key": "b.com", refreshTime: new Date()};
assert.equal(gPreviewProvider._isLinkExpired({cache_key: "b.com"}), false, "non-expired link should return false");
};
exports.test_request_links_if_expired = function*(assert) {
const oldTime = Date.now() - (gPreviewProvider.options.cacheRefreshAge + 1000);
const links = [{"url": "a.com", "sanitized_url": "a.com", "cache_key": "a.com"},
{"url": "b.com", "sanitized_url": "b.com", "cache_key": "b.com"},
{"url": "c.com", "sanitized_url": "c.com", "cache_key": "c.com"}];
links.forEach(link => {
ss.storage.embedlyData[link.cache_key] = Object.assign({}, link, {refreshTime: new Date()});
});
ss.storage.embedlyData["a.com"].refreshTime = oldTime;
assert.ok(gPreviewProvider._embedlyEndpoint, "The embedly endpoint is set");
let srv = httpd.startServerAsync(gPort);
let urlsRequested = [];
srv.registerPathHandler("/embedlyLinkData", function handle(request, response) {
let data = JSON.parse(
NetUtil.readInputStreamToString(
request.bodyInputStream,
request.bodyInputStream.available()
)
);
data.urls.forEach(url => urlsRequested.push(url));
response.setHeader("Content-Type", "application/json", false);
response.write(JSON.stringify({"urls": {urlsRequested}}));
});
yield gPreviewProvider._asyncSaveLinks(links);
assert.deepEqual(urlsRequested, ["a.com"], "we should only request the expired URL");
yield new Promise(resolve => {
srv.stop(resolve);
});
};
exports.test_filter_urls = function*(assert) {
const fakeData = {
get validLinks() {
@ -262,14 +146,17 @@ exports.test_process_links = function*(assert) {
{"url": "https://foo.com/", "title": "blah"}
];
// process the links
const processedLinks = gPreviewProvider._processLinks(fakeData);
assert.equal(fakeData.length, processedLinks.length, "should not deduplicate or remove any links");
// check that each link has added the correct fields
processedLinks.forEach((link, i) => {
assert.equal(link.url, fakeData[i].url, "each site has its original url");
assert.ok(link.sanitized_url, "links have sanitizedURLs");
assert.ok(link.cache_key, "links have cacheKeys");
assert.ok(link.sanitized_url, "link has a sanitized url");
assert.ok(link.cache_key, "link has a cache key");
assert.ok(link.places_url, "link has a places url");
});
};
@ -331,17 +218,21 @@ exports.test_throw_out_non_requested_responses = function*(assert) {
yield gPreviewProvider._asyncSaveLinks(fakeData);
// cache should contain example1.com and example2.com
assert.ok(ss.storage.embedlyData[fakeSite1.cache_key].embedlyMetaData, "first site was saved as expected");
assert.ok(ss.storage.embedlyData[fakeSite2.cache_key].embedlyMetaData, "second site was saved as expected");
// cache should not contain example3.com and example4.com
assert.throws(() => ss.storage.embedlyData[fakeSite3.cache_key].embedlyMetaData, "third site was not found in the cache");
assert.throws(() => ss.storage.embedlyData[fakeSite4.cache_key].embedlyMetaData, "fourth site was not found in the cache");
// database should contain example1.com and example2.com
assert.equal(gMetadataStore[0].length, 2, "saved two items");
assert.equal(gMetadataStore[0][0].url, fakeSite1.url, "first site was saved as expected");
assert.equal(gMetadataStore[0][1].url, fakeSite2.url, "second site was saved as expected");
// database should not contain example3.com and example4.com
gMetadataStore[0].forEach(item => {
assert.ok(item.url !== fakeSite3.url, "third site was not saved");
assert.ok(item.url !== fakeSite4.url, "fourth site was not saved");
});
yield new Promise(resolve => {
srv.stop(resolve);
});
},
};
exports.test_mock_embedly_request = function*(assert) {
const fakeSite = {
@ -355,8 +246,8 @@ exports.test_mock_embedly_request = function*(assert) {
"sanitized_url": "http://example.com/",
"cache_key": "example.com/"
};
const fakeData = [fakeSite];
const fakeDataCached = {"urls": {
const fakeRequest = [fakeSite];
const fakeResponse = {"urls": {
"http://example.com/": {
"embedlyMetaData": "some embedly metadata"
}
@ -370,18 +261,21 @@ exports.test_mock_embedly_request = function*(assert) {
// first, check that the version included in the query string
assert.deepEqual(`${request.queryString}`, `${embedlyVersionQuery}${self.version}`, "we're hitting the correct endpoint");
response.setHeader("Content-Type", "application/json", false);
response.write(JSON.stringify(fakeDataCached));
response.write(JSON.stringify(fakeResponse));
});
yield gPreviewProvider._asyncSaveLinks(fakeData);
// make a request to embedly with 'fakeSite'
yield gPreviewProvider._asyncSaveLinks(fakeRequest);
assert.deepEqual(ss.storage.embedlyData[fakeSite.cache_key].embedlyMetaData, "some embedly metadata", "the cache saved the embedly data");
assert.ok(ss.storage.embedlyData[fakeSite.cache_key].accessTime, "the cached saved a time stamp");
// we should have saved the fake site into the database
assert.deepEqual(gMetadataStore[0][0].embedlyMetaData, "some embedly metadata", "inserted and saved the embedly data");
assert.ok(gMetadataStore[0][0].expired_at, "an expiry time was added");
let cachedLinks = gPreviewProvider._getEnhancedLinks(fakeData);
// retrieve the contents of the database - don't go to embedly
let cachedLinks = yield gPreviewProvider._asyncGetEnhancedLinks(fakeRequest);
assert.equal(cachedLinks[0].lastVisitDate, fakeSite.lastVisitDate, "getEnhancedLinks should prioritize new data");
assert.equal(cachedLinks[0].bookmarkDateCreated, fakeSite.bookmarkDateCreated, "getEnhancedLinks should prioritize new data");
assert.ok(ss.storage.embedlyData[fakeSite.cache_key], "the cached link is now retrieved next time");
assert.deepEqual(gMetadataStore[0][0].cache_key, cachedLinks[0].cache_key, "the cached link is now retrieved next time");
yield new Promise(resolve => {
srv.stop(resolve);
@ -393,32 +287,47 @@ exports.test_get_enhanced_disabled = function*(assert) {
{url: "http://foo.com/", lastVisitDate: 1459537019061}
];
simplePrefs.prefs["previews.enabled"] = false;
let cachedLinks = gPreviewProvider._getEnhancedLinks(fakeData);
let cachedLinks = yield gPreviewProvider._asyncGetEnhancedLinks(fakeData);
assert.deepEqual(cachedLinks, fakeData, "if disabled, should return links as is");
};
exports.test_get_enhanced_previews_only = function*(assert) {
ss.storage.embedlyData["example.com/"] = {sanitized_url: "http://example.com/", cache_key: "example.com/", url: "http://example.com/"};
gMetadataStore[0] = {sanitized_url: "http://example.com/", cache_key: "example.com/", url: "http://example.com/"};
let links;
links = gPreviewProvider._getEnhancedLinks([{cache_key: "example.com/"}, {cache_key: "foo.com"}]);
links = yield gPreviewProvider._asyncGetEnhancedLinks([{cache_key: "example.com/"}, {cache_key: "foo.com"}]);
assert.equal(links.length, 2, "by default getEnhancedLinks returns links with and without previews");
links = gPreviewProvider._getEnhancedLinks([{cache_key: "example.com/"}, {cache_key: "foo.com"}], true);
links = yield gPreviewProvider._asyncGetEnhancedLinks([{cache_key: "example.com/"}, {cache_key: "foo.com"}], true);
assert.equal(links.length, 1, "when previewOnly is set, return only links with previews");
};
before(exports, function*() {
simplePrefs.prefs["embedly.endpoint"] = `http://localhost:${gPort}/embedlyLinkData`;
simplePrefs.prefs["previews.enabled"] = true;
let mockMetadataStore = {
asyncInsert: function(data) {
gMetadataStore.push(data);
return gMetadataStore;
},
asyncGetMetadataByCacheKey: function(cacheKeys) {
let items = [];
gMetadataStore.forEach(item => {
if (cacheKeys.includes(item.cache_key)) {
items.push(Object.assign({}, {cache_key: item.cache_key}, {title: `Title for ${item.cache_key}`}));
}
});
return items;
}
};
let mockTabTracker = {handlePerformanceEvent: function() {}, generateEvent: function() {}};
gPreviewProvider = new PreviewProvider(mockTabTracker, {initFresh: true});
gPreviewProvider = new PreviewProvider(mockTabTracker, mockMetadataStore, {initFresh: true});
});
after(exports, function*() {
simplePrefs.prefs["embedly.endpoint"] = gPrefEmbedly;
simplePrefs.prefs["previews.enabled"] = gPrefEnabled;
gPreviewProvider.clearCache();
gMetadataStore = [];
gPreviewProvider.uninit();
});