test_FaviconProvider.js (12441B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 "use strict"; 5 6 ChromeUtils.defineESModuleGetters(this, { 7 FaviconProvider: "resource:///modules/topsites/TopSites.sys.mjs", 8 HttpServer: "resource://testing-common/httpd.sys.mjs", 9 NewTabUtils: "resource://gre/modules/NewTabUtils.sys.mjs", 10 NetUtil: "resource://gre/modules/NetUtil.sys.mjs", 11 PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", 12 PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", 13 TestUtils: "resource://testing-common/TestUtils.sys.mjs", 14 sinon: "resource://testing-common/Sinon.sys.mjs", 15 }); 16 17 let TEST_FAVICON_FILE; 18 let TEST_DOMAIN; 19 let TEST_PAGE_URL; 20 let TEST_FAVICON_URL; 21 22 const TEST_SVG_DATA_URL = Services.io.newURI( 23 "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy5" + 24 "3My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAgMTAwIiBmaWxs" + 25 "PSIjNDI0ZTVhIj4NCiAgPGNpcmNsZSBjeD0iNTAiIGN5PSI1MCIgcj0iN" + 26 "DQiIHN0cm9rZT0iIzQyNGU1YSIgc3Ryb2tlLXdpZHRoPSIxMSIgZmlsbD" + 27 "0ibm9uZSIvPg0KICA8Y2lyY2xlIGN4PSI1MCIgY3k9IjI0LjYiIHI9IjY" + 28 "uNCIvPg0KICA8cmVjdCB4PSI0NSIgeT0iMzkuOSIgd2lkdGg9IjEwLjEi" + 29 "IGhlaWdodD0iNDEuOCIvPg0KPC9zdmc%2BDQo%3D" 30 ); 31 32 add_setup(async function setup() { 33 info("Setup profile to use Places DB"); 34 do_get_profile(true); 35 36 info("Setup favicon data"); 37 TEST_FAVICON_FILE = do_get_file("favicon.png"); 38 39 info("Setup http server that returns favicon content"); 40 const httpServer = new HttpServer(); 41 httpServer.registerPathHandler("/favicon.png", (request, response) => { 42 const inputStream = Cc[ 43 "@mozilla.org/network/file-input-stream;1" 44 ].createInstance(Ci.nsIFileInputStream); 45 inputStream.init(TEST_FAVICON_FILE, 0x01, -1, null); 46 const size = inputStream.available(); 47 const faviconData = NetUtil.readInputStreamToString(inputStream, size); 48 49 response.setStatusLine(request.httpVersion, 200, "Ok"); 50 response.setHeader("Content-Type", "image/png", false); 51 response.bodyOutputStream.write(faviconData, faviconData.length); 52 }); 53 httpServer.start(-1); 54 55 TEST_DOMAIN = "localhost"; 56 TEST_PAGE_URL = Services.io.newURI( 57 `http://${TEST_DOMAIN}:${httpServer.identity.primaryPort}` 58 ); 59 TEST_FAVICON_URL = Services.io.newURI( 60 `http://${TEST_DOMAIN}:${httpServer.identity.primaryPort}/favicon.png` 61 ); 62 63 info("Setup visit data in DB"); 64 await PlacesTestUtils.addVisits(TEST_PAGE_URL); 65 66 // Save the original favicon service 67 let originalFaviconService = PlacesUtils.favicons; 68 69 registerCleanupFunction(async () => { 70 // Restore the original favicon service 71 PlacesUtils.favicons = originalFaviconService; 72 await new Promise(resolve => httpServer.stop(resolve)); 73 await PlacesUtils.history.clear(); 74 }); 75 }); 76 77 // Test for getFaviconDataURLFromNetwork() function. 78 add_task(async function test_getFaviconDataURLFromNetwork() { 79 const feed = new FaviconProvider(); 80 81 info("Get favicon data via FaviconProvider"); 82 const result = await feed.getFaviconDataURLFromNetwork(TEST_FAVICON_URL); 83 Assert.equal( 84 result.spec, 85 // eslint-disable-next-line no-use-before-define 86 await readFileDataAsDataURL(TEST_FAVICON_FILE, "image/png"), 87 "getFaviconDataURLFromNetwork returns correct data url" 88 ); 89 }); 90 91 // Test for fetchIcon() function. If there is valid favicon data for the page in 92 // DB, not update. 93 add_task(async function test_fetchIcon_with_valid_favicon() { 94 const feed = new FaviconProvider(); 95 96 info("Setup stub to use dummy site data from FaviconProvider.getSite()"); 97 const sandbox = sinon.createSandbox(); 98 sandbox 99 .stub(feed, "getSite") 100 .resolves({ domain: TEST_DOMAIN, image_url: TEST_FAVICON_URL.spec }); 101 102 info("Setup valid favicon data in DB"); 103 await PlacesUtils.favicons.setFaviconForPage( 104 TEST_PAGE_URL, 105 TEST_FAVICON_URL, 106 TEST_SVG_DATA_URL 107 ); 108 109 info("Call FaviconProvider.fetchIcon()"); 110 await feed.fetchIcon(TEST_PAGE_URL.spec); 111 112 info("Check the database"); 113 const result = await PlacesUtils.favicons.getFaviconForPage(TEST_PAGE_URL); 114 Assert.equal(result.mimeType, "image/svg+xml"); 115 Assert.equal(result.width, 65535); 116 117 info("Clean up"); 118 await PlacesTestUtils.clearFavicons(); 119 }); 120 121 // Test for fetchIcon() function. If there is favicon data in DB but invalid or 122 // is not in DB, get favicon data from network, and update the DB with it. 123 add_task(async function test_fetchIcon_with_invalid_favicon() { 124 for (const dummyFaviconInfo of [ 125 null, 126 { iconUri: TEST_PAGE_URL, faviconSize: 0 }, 127 ]) { 128 info(`Test for ${dummyFaviconInfo}`); 129 const feed = new FaviconProvider(); 130 131 info("Setup stub to use dummy site data from FaviconProvider.getSite()"); 132 const sandbox = sinon.createSandbox(); 133 sandbox 134 .stub(feed, "getSite") 135 .resolves({ domain: TEST_DOMAIN, image_url: TEST_FAVICON_URL.spec }); 136 137 info("Setup stub to simulate invalid favicon"); 138 sandbox.stub(feed, "getFaviconInfo").resolves(dummyFaviconInfo); 139 140 info("Call FaviconProvider.fetchIcon()"); 141 await feed.fetchIcon(TEST_PAGE_URL.spec); 142 143 info("Check the database"); 144 const result = await PlacesUtils.favicons.getFaviconForPage(TEST_PAGE_URL); 145 // eslint-disable-next-line no-use-before-define 146 const expectedFaviconData = readFileData(TEST_FAVICON_FILE); 147 Assert.equal(result.uri.spec, `${TEST_FAVICON_URL.spec}#tippytop`); 148 Assert.deepEqual(result.rawData, expectedFaviconData); 149 Assert.equal(result.mimeType, "image/png"); 150 Assert.equal(result.width, 16); 151 152 info("Clean up"); 153 await PlacesTestUtils.clearFavicons(); 154 sandbox.restore(); 155 } 156 }); 157 158 // Test for fetchIconFromRedirects() function. If there is valid favicon data 159 // for the redirected page in DB, copy the favicon data to the destination page as well. 160 add_task(async function test_fetchIconFromRedirects_with_valid_favicon() { 161 const feed = new FaviconProvider(); 162 163 info("Setup stub to use dummy site data from FaviconProvider.getSite()"); 164 const sandbox = sinon.createSandbox(); 165 sandbox 166 .stub(NewTabUtils.activityStreamProvider, "executePlacesQuery") 167 .resolves([ 168 { visit_id: 1, url: TEST_DOMAIN }, 169 { visit_id: 2, url: TEST_PAGE_URL.spec }, 170 ]); 171 172 info("Setup valid favicon data in DB"); 173 await PlacesUtils.favicons.setFaviconForPage( 174 TEST_PAGE_URL, 175 TEST_FAVICON_URL, 176 TEST_SVG_DATA_URL 177 ); 178 179 info("Setup destination"); 180 const destination = Services.io.newURI("http://destination.localhost/"); 181 await PlacesTestUtils.addVisits(destination); 182 183 info("Call FaviconProvider.fetchIconFromRedirects()"); 184 await feed.fetchIconFromRedirects(destination.spec); 185 186 info("Check the database"); 187 await TestUtils.waitForCondition(async () => { 188 const result = await PlacesUtils.favicons.getFaviconForPage(destination); 189 return !!result; 190 }); 191 const sourceResult = 192 await PlacesUtils.favicons.getFaviconForPage(TEST_PAGE_URL); 193 const destinationResult = 194 await PlacesUtils.favicons.getFaviconForPage(destination); 195 Assert.deepEqual(destinationResult.rawData, sourceResult.rawData); 196 Assert.equal(destinationResult.mimeType, sourceResult.mimeType); 197 Assert.equal(destinationResult.width, sourceResult.width); 198 199 info("Clean up"); 200 await PlacesTestUtils.clearFavicons(); 201 sandbox.restore(); 202 }); 203 204 // Copy from toolkit/components/places/tests/head_common.js 205 function readInputStreamData(aStream) { 206 let bistream = Cc["@mozilla.org/binaryinputstream;1"].createInstance( 207 Ci.nsIBinaryInputStream 208 ); 209 try { 210 bistream.setInputStream(aStream); 211 let expectedData = []; 212 let avail; 213 while ((avail = bistream.available())) { 214 expectedData = expectedData.concat(bistream.readByteArray(avail)); 215 } 216 return expectedData; 217 } finally { 218 bistream.close(); 219 } 220 } 221 222 function readFileData(aFile) { 223 let inputStream = Cc[ 224 "@mozilla.org/network/file-input-stream;1" 225 ].createInstance(Ci.nsIFileInputStream); 226 // init the stream as RD_ONLY, -1 == default permissions. 227 inputStream.init(aFile, 0x01, -1, null); 228 229 // Check the returned size versus the expected size. 230 let size = inputStream.available(); 231 let bytes = readInputStreamData(inputStream); 232 if (size !== bytes.length) { 233 throw new Error("Didn't read expected number of bytes"); 234 } 235 return bytes; 236 } 237 238 async function fileDataToDataURL(data, mimeType) { 239 const dataURL = await new Promise(resolve => { 240 const buffer = new Uint8ClampedArray(data); 241 const blob = new Blob([buffer], { type: mimeType }); 242 const reader = new FileReader(); 243 reader.onload = e => { 244 resolve(e.target.result); 245 }; 246 reader.readAsDataURL(blob); 247 }); 248 return dataURL; 249 } 250 251 async function readFileDataAsDataURL(file, mimeType) { 252 const data = readFileData(file); 253 return fileDataToDataURL(data, mimeType); 254 } 255 256 const FAKE_SMALLPNG_DATA_URI = 257 "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg=="; 258 259 // Test: fetchIcon should fetch favicon from network if no data in DB 260 add_task(async function test_fetchIcon_withNetworkFetch() { 261 const sandbox = sinon.createSandbox(); 262 let feed = new FaviconProvider(); 263 let url = "https://mozilla.org/"; 264 265 // Set up mocks 266 PlacesUtils.favicons = { 267 getFaviconForPage: sandbox.stub().returns(Promise.resolve(null)), 268 setFaviconForPage: sandbox.spy(), 269 copyFavicons: sandbox.spy(), 270 }; 271 feed.getSite = sandbox 272 .stub() 273 .returns( 274 Promise.resolve({ domain: "mozilla.org", image_url: `${url}/icon.png` }) 275 ); 276 277 // Mock the getFaviconDataURLFromNetwork method 278 sandbox 279 .stub(feed, "getFaviconDataURLFromNetwork") 280 .resolves({ spec: FAKE_SMALLPNG_DATA_URI }); 281 282 await feed.fetchIcon(url); 283 284 // Assertions 285 Assert.equal(PlacesUtils.favicons.setFaviconForPage.calledOnce, true); 286 Assert.equal( 287 PlacesUtils.favicons.setFaviconForPage.firstCall.args[2].spec, 288 FAKE_SMALLPNG_DATA_URI 289 ); 290 291 sandbox.restore(); 292 }); 293 294 // Test: fetchIcon should fetch favicon from network if invalid data in DB 295 add_task(async function test_fetchIcon_withInvalidDataInDb() { 296 const sandbox = sinon.createSandbox(); 297 // Set up mocks 298 PlacesUtils.favicons = { 299 // Invalid since no width. 300 getFaviconForPage: sandbox 301 .stub() 302 .returns(Promise.resolve({ iconUri: { spec: FAKE_SMALLPNG_DATA_URI } })), 303 setFaviconForPage: sandbox.spy(), 304 copyFavicons: sandbox.spy(), 305 }; 306 307 let feed = new FaviconProvider(); 308 let url = "https://mozilla.org/"; 309 feed.getSite = sandbox 310 .stub() 311 .returns( 312 Promise.resolve({ domain: "mozilla.org", image_url: `${url}/icon.png` }) 313 ); 314 315 // Mock the getFaviconDataURLFromNetwork method 316 sandbox 317 .stub(feed, "getFaviconDataURLFromNetwork") 318 .resolves({ spec: FAKE_SMALLPNG_DATA_URI }); 319 320 await feed.fetchIcon(url); 321 322 // Assertions 323 Assert.equal(PlacesUtils.favicons.setFaviconForPage.calledOnce, true); 324 Assert.equal( 325 PlacesUtils.favicons.setFaviconForPage.firstCall.args[2].spec, 326 FAKE_SMALLPNG_DATA_URI 327 ); 328 329 sandbox.restore(); 330 }); 331 332 // Test: fetchIcon should not set favicon if valid data exists in DB 333 add_task(async function test_fetchIcon_withValidDataInDb() { 334 const sandbox = sinon.createSandbox(); 335 // Set up mocks 336 PlacesUtils.favicons = { 337 getFaviconForPage: sandbox.stub().returns( 338 Promise.resolve({ 339 iconUri: { spec: FAKE_SMALLPNG_DATA_URI }, 340 width: 100, 341 }) 342 ), 343 setFaviconForPage: sandbox.spy(), 344 copyFavicons: sandbox.spy(), 345 }; 346 let feed = new FaviconProvider(); 347 let url = "https://mozilla.org/"; 348 feed.getSite = sandbox 349 .stub() 350 .returns( 351 Promise.resolve({ domain: "mozilla.org", image_url: `${url}/icon.png` }) 352 ); 353 354 await feed.fetchIcon(url); 355 356 // Assertions 357 Assert.equal(PlacesUtils.favicons.setFaviconForPage.called, false); 358 359 sandbox.restore(); 360 }); 361 362 // Test: fetchIcon should not set favicon if the URL is not in TippyTop data 363 add_task(async function test_fetchIcon_withNoTippyTopData() { 364 const sandbox = sinon.createSandbox(); 365 let feed = new FaviconProvider(); 366 // Set up mocks 367 PlacesUtils.favicons = { 368 getFaviconForPage: sandbox.stub().returns(Promise.resolve(null)), 369 setFaviconForPage: sandbox.spy(), 370 copyFavicons: sandbox.spy(), 371 }; 372 feed.getSite = sandbox.stub().returns(Promise.resolve(null)); 373 374 await feed.fetchIcon("https://example.com"); 375 376 // Assertions 377 Assert.equal(PlacesUtils.favicons.setFaviconForPage.called, false); 378 379 sandbox.restore(); 380 });