diff options
Diffstat (limited to 'browser/components/newtab/test/RemoteImagesTestUtils.jsm')
-rw-r--r-- | browser/components/newtab/test/RemoteImagesTestUtils.jsm | 352 |
1 files changed, 352 insertions, 0 deletions
diff --git a/browser/components/newtab/test/RemoteImagesTestUtils.jsm b/browser/components/newtab/test/RemoteImagesTestUtils.jsm new file mode 100644 index 0000000000..bc8594549d --- /dev/null +++ b/browser/components/newtab/test/RemoteImagesTestUtils.jsm @@ -0,0 +1,352 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js"); +const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm"); +const { RemoteImages, REMOTE_IMAGES_PATH } = ChromeUtils.import( + "resource://activity-stream/lib/RemoteImages.jsm" +); + +// This pref is used to override the Remote Settings server URL in tests. +// See SERVER_URL in services/settings/Utils.jsm for more details. +const RS_SERVER_PREF = "services.settings.server"; + +class RemoteSettingsRecord { + constructor(id) { + this.data = { + id, + last_modified: Date.now(), + }; + } + + set(attrs) { + const { id } = this.data; + Object.assign(this.data, attrs, { + id, + last_modified: Date.now(), + }); + } + + toJSON() { + return { data: this.data }; + } +} + +class RemoteSettingsCollection { + get ChildType() { + return RemoteSettingsRecord; + } + + constructor(id) { + this.data = { + id, + last_modified: Date.now(), + }; + + this.children = new Map(); + } + + get(id) { + return this.children.get(id); + } + + getOrCreate(id, { update = false } = {}) { + return (update ? this.update(id) : this.get(id)) ?? this.create(id); + } + + create(id) { + if (this.get(id)) { + throw new Error(`already have child ${id}`); + } + + let child = new this.ChildType(id); + this.children.set(id, child); + this.data.last_modified = Date.now(); + return child; + } + + update(id) { + let child = this.get(id); + if (child) { + this.data.last_modified = Date.now(); + } + return child; + } + + toJSON() { + return { + data: Array.from(this.children.values()).map(child => ({ + id: child.data.id, + last_modified: child.data.last_modified, + })), + }; + } +} + +class RemoteSettingsBucket extends RemoteSettingsCollection { + get ChildType() { + return RemoteSettingsCollection; + } +} + +class RemoteSettingsRoot extends RemoteSettingsCollection { + get ChildType() { + return RemoteSettingsBucket; + } + constructor() { + super("root"); + } +} + +class RemoteSettingsAttachment { + constructor(attrs) { + Object.assign(this, attrs); + } + + writeTo(response) { + const stream = NetUtil.newChannel({ + uri: NetUtil.newURI(this.url), + loadUsingSystemPrincipal: true, + }).open(); + + try { + response.setHeader("Content-Type", this.mimetype); + response.bodyOutputStream.writeFrom(stream, this.size); + response.setStatusLine(null, 200, "OK"); + } finally { + stream.close(); + } + } +} + +class RemoteSettingsServer { + constructor() { + this.server = new HttpServer(); + this.buckets = new RemoteSettingsRoot(); + this.attachments = new Map(); + + this._originalServerlURL = null; + + this.server.registerPathHandler("/v1/", (request, response) => { + response.setHeader("Content-Type", "application/json; charset=UTF-8"); + response.setStatusLine(null, 200, "OK"); + response.write( + JSON.stringify({ + capabilities: { + attachments: { + base_url: `${this.baseURL}attachments`, + }, + }, + }) + ); + }); + + const recordRegex = new RegExp( + "/v1/buckets/(?<bucketId>[^/]+)/collections/(?<collectionId>[^/]+)/records/(?<recordId>[^/]+)" + ); + this.server.registerPrefixHandler("/v1/buckets/", (request, response) => { + const match = recordRegex.exec(request.path); + if (!match) { + response.setStatusLine(null, 404, "Not Found"); + response.write("404"); + return; + } + + const record = this.buckets + .get(match.groups.bucketId) + ?.get(match.groups.collectionId) + ?.get(match.groups.recordId); + if (!record) { + response.setStatusLine(null, 404, "Not Found"); + response.write("404"); + return; + } + + response.setHeader("Content-Type", "application/json; charset=UTF-8"); + response.setStatusLine(null, 200, "OK"); + response.write(JSON.stringify(record)); + }); + + const ATTACHMENTS_PREFIX = "/attachments/"; + this.server.registerPrefixHandler( + ATTACHMENTS_PREFIX, + (request, response) => { + const attachmentId = request.path.substring(ATTACHMENTS_PREFIX.length); + const attachment = this.attachments.get(attachmentId); + + if (!attachment) { + response.setStatusLine(null, 400, "Not Found"); + response.write("404"); + return; + } + + attachment.writeTo(response); + } + ); + } + + start() { + this.server.start(-1); + + this._originalServerlURL = Services.prefs.getCharPref(RS_SERVER_PREF); + Services.prefs.setCharPref(RS_SERVER_PREF, `${this.baseURL}v1`); + } + + async stop() { + await new Promise(resolve => this.server.stop(resolve)); + + // If we use clearUserPref, then we will reset to the default branch value + // (i.e., the *real* RS server) which will cause test failures due to trying + // to access an outside URL. + Services.prefs.setCharPref(RS_SERVER_PREF, this._originalServerlURL); + this._originalServerlURL = null; + } + + get baseURL() { + return `http://localhost:${this.server.identity.primaryPort}/`; + } + + addRemoteImage(imageInfo) { + const { filename, recordId, mimetype, hash, url, size } = imageInfo; + + const location = `main/ms-images/${recordId}`; + + this.buckets + .getOrCreate("main", { update: true }) + .getOrCreate("ms-images", { update: true }) + .create(recordId) + .set({ + attachment: { + filename, + location, + hash, + mimetype, + size, + }, + }); + + this.attachments.set( + location, + new RemoteSettingsAttachment({ + mimetype, + size, + url, + }) + ); + } +} + +const RemoteImagesTestUtils = { + /** + * Serve a mock Remote Settings server with content for Remote Images + * + * @param imageInfo An entry describing the image. Should be one of + * |RemoteImagesTestUtils.images|. + * + * @returns A promise yielding a cleanup function. This function will stop the + * internal HTTP server and clean up all Remote Images state. + */ + serveRemoteImages(...imageInfos) { + const server = new RemoteSettingsServer(); + + for (const imageInfo of imageInfos) { + server.addRemoteImage(imageInfo); + } + + server.start(); + + return async () => { + await server.stop(); + await RemoteImagesTestUtils.wipeCache(); + }; + }, + + /** + * Wipe the Remote Images cache. + */ + async wipeCache() { + await RemoteImages.reset(); + + const children = await IOUtils.getChildren(REMOTE_IMAGES_PATH); + for (const child of children) { + await IOUtils.remove(child); + } + }, + + /** + * Trigger RemoteImages cleanup. + */ + triggerCleanup() { + return RemoteImages.forceCleanup(); + }, + + /** + * Write an image into the remote images directory. + * + * @param imageInfo An entry describing the image. Should be one of + * |RemoteImagesTestUtils.images|. + * + * @param filename An optional filename to save as inside the directory. If + * not provided, |imageInfo.recordId| will be used. + */ + async writeImage(imageInfo, filename = undefined) { + const data = new Uint8Array( + await fetch(imageInfo.url, { credentials: "omit" }).then(rsp => + rsp.arrayBuffer() + ) + ); + + await IOUtils.write( + PathUtils.join(REMOTE_IMAGES_PATH, filename ?? imageInfo.recordId), + data + ); + }, + + /** + * Return a RemoteImages database entry for the given image info. + * + * @param imageInfo An entry describing the image. Should be one of + * |RemoteImagesTestUtils.images|. + * + * @param lastLoaded The timestamp to use for when the image was last loaded + * (in UTC). If not provided, the current time is used. + */ + dbEntryFor(imageInfo, lastLoaded = undefined) { + return { + [imageInfo.recordId]: { + recordId: imageInfo.recordId, + hash: imageInfo.hash, + mimetype: imageInfo.mimetype, + lastLoaded: lastLoaded ?? Date.now(), + }, + }; + }, + + /** + * Remote Image entries. + */ + images: { + AboutRobots: { + filename: "about-robots.png", + recordId: "about-robots", + mimetype: "image/png", + hash: "29f1fe2cb5181152d2c01c0b2f12e5d9bb3379a61b94fb96de0f734eb360da62", + url: "chrome://browser/content/aboutRobots-icon.png", + size: 7599, + }, + + Mountain: { + filename: "mountain.svg", + recordId: "mountain", + mimetype: "image/svg+xml", + hash: "96902f3d784e1b5e49547c543a5c121442c64b180deb2c38246fada1d14597ac", + url: + "chrome://activity-stream/content/data/content/assets/remote/mountain.svg", + size: 1650, + }, + }, +}; + +const EXPORTED_SYMBOLS = ["RemoteImagesTestUtils", "RemoteSettingsServer"]; |