summaryrefslogtreecommitdiffstats
path: root/browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js')
-rw-r--r--browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js459
1 files changed, 459 insertions, 0 deletions
diff --git a/browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js b/browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js
new file mode 100644
index 0000000000..d855f89d27
--- /dev/null
+++ b/browser/components/newtab/test/unit/asrouter/MessageLoaderUtils.test.js
@@ -0,0 +1,459 @@
+import { MessageLoaderUtils } from "lib/ASRouter.jsm";
+const { STARTPAGE_VERSION } = MessageLoaderUtils;
+
+const FAKE_OPTIONS = {
+ storage: {
+ set() {
+ return Promise.resolve();
+ },
+ get() {
+ return Promise.resolve();
+ },
+ },
+ dispatchToAS: () => {},
+};
+const FAKE_RESPONSE_HEADERS = { get() {} };
+
+describe("MessageLoaderUtils", () => {
+ let fetchStub;
+ let clock;
+ let sandbox;
+
+ beforeEach(() => {
+ sandbox = sinon.createSandbox();
+ clock = sinon.useFakeTimers();
+ fetchStub = sinon.stub(global, "fetch");
+ });
+ afterEach(() => {
+ sandbox.restore();
+ clock.restore();
+ fetchStub.restore();
+ });
+
+ describe("#loadMessagesForProvider", () => {
+ it("should return messages for a local provider with hardcoded messages", async () => {
+ const sourceMessage = { id: "foo" };
+ const provider = {
+ id: "provider123",
+ type: "local",
+ messages: [sourceMessage],
+ };
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.isArray(result.messages);
+ // Does the message have the right properties?
+ const [message] = result.messages;
+ assert.propertyVal(message, "id", "foo");
+ assert.propertyVal(message, "provider", "provider123");
+ });
+ it("should filter out local messages listed in the `exclude` field", async () => {
+ const sourceMessage = { id: "foo" };
+ const provider = {
+ id: "provider123",
+ type: "local",
+ messages: [sourceMessage],
+ exclude: ["foo"],
+ };
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.lengthOf(result.messages, 0);
+ });
+ it("should return messages for remote provider", async () => {
+ const sourceMessage = { id: "foo" };
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve({ messages: [sourceMessage] }),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ const provider = {
+ id: "provider123",
+ type: "remote",
+ url: "https://foo.com",
+ };
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+ assert.isArray(result.messages);
+ // Does the message have the right properties?
+ const [message] = result.messages;
+ assert.propertyVal(message, "id", "foo");
+ assert.propertyVal(message, "provider", "provider123");
+ assert.propertyVal(message, "provider_url", "https://foo.com");
+ });
+ describe("remote provider HTTP codes", () => {
+ const testMessage = { id: "foo" };
+ const provider = {
+ id: "provider123",
+ type: "remote",
+ url: "https://foo.com",
+ updateCycleInMs: 300,
+ };
+ const respJson = { messages: [testMessage] };
+
+ function assertReturnsCorrectMessages(actual) {
+ assert.isArray(actual.messages);
+ // Does the message have the right properties?
+ const [message] = actual.messages;
+ assert.propertyVal(message, "id", testMessage.id);
+ assert.propertyVal(message, "provider", provider.id);
+ assert.propertyVal(message, "provider_url", provider.url);
+ }
+
+ it("should return messages for 200 response", async () => {
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve(respJson),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ assertReturnsCorrectMessages(
+ await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ )
+ );
+ });
+
+ it("should return messages for a 302 response with json", async () => {
+ fetchStub.resolves({
+ ok: true,
+ status: 302,
+ json: () => Promise.resolve(respJson),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ assertReturnsCorrectMessages(
+ await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ )
+ );
+ });
+
+ it("should return an empty array for a 204 response", async () => {
+ fetchStub.resolves({
+ ok: true,
+ status: 204,
+ json: () => "",
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+ assert.deepEqual(result.messages, []);
+ });
+
+ it("should return an empty array for a 500 response", async () => {
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ json: () => "",
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+ assert.deepEqual(result.messages, []);
+ });
+
+ it("should return cached messages for a 304 response", async () => {
+ clock.tick(302);
+ const messages = [{ id: "message-1" }, { id: "message-2" }];
+ const fakeStorage = {
+ set() {
+ return Promise.resolve();
+ },
+ get() {
+ return Promise.resolve({
+ [provider.id]: {
+ version: STARTPAGE_VERSION,
+ url: provider.url,
+ messages,
+ etag: "etag0987654321",
+ lastFetched: 1,
+ },
+ });
+ },
+ };
+ fetchStub.resolves({
+ ok: true,
+ status: 304,
+ json: () => "",
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ { ...FAKE_OPTIONS, storage: fakeStorage }
+ );
+ assert.equal(result.messages.length, messages.length);
+ messages.forEach(message => {
+ assert.ok(result.messages.find(m => m.id === message.id));
+ });
+ });
+
+ it("should return an empty array if json doesn't parse properly", async () => {
+ fetchStub.resolves({
+ ok: false,
+ status: 200,
+ json: () => "",
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+ assert.deepEqual(result.messages, []);
+ });
+
+ it("should report response parsing errors with MessageLoaderUtils.reportError", async () => {
+ const err = {};
+ sandbox.spy(MessageLoaderUtils, "reportError");
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: sandbox.stub().rejects(err),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.calledOnce(MessageLoaderUtils.reportError);
+ // Report that json parsing failed
+ assert.calledWith(MessageLoaderUtils.reportError, err);
+ });
+
+ it("should report missing `messages` with MessageLoaderUtils.reportError", async () => {
+ sandbox.spy(MessageLoaderUtils, "reportError");
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: sandbox.stub().resolves({}),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.calledOnce(MessageLoaderUtils.reportError);
+ // Report no messages returned
+ assert.calledWith(
+ MessageLoaderUtils.reportError,
+ "No messages returned from https://foo.com."
+ );
+ });
+
+ it("should report bad status responses with MessageLoaderUtils.reportError", async () => {
+ sandbox.spy(MessageLoaderUtils, "reportError");
+ fetchStub.resolves({
+ ok: false,
+ status: 500,
+ json: sandbox.stub().resolves({}),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.calledOnce(MessageLoaderUtils.reportError);
+ // Report no messages returned
+ assert.calledWith(
+ MessageLoaderUtils.reportError,
+ "Invalid response status 500 from https://foo.com."
+ );
+ });
+
+ it("should return an empty array if the request rejects", async () => {
+ fetchStub.rejects(new Error("something went wrong"));
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+ assert.deepEqual(result.messages, []);
+ });
+ });
+ describe("remote provider caching", () => {
+ const provider = {
+ id: "provider123",
+ type: "remote",
+ url: "https://foo.com",
+ updateCycleInMs: 300,
+ };
+
+ it("should return cached results if they aren't expired", async () => {
+ clock.tick(1);
+ const messages = [{ id: "message-1" }, { id: "message-2" }];
+ const fakeStorage = {
+ set() {
+ return Promise.resolve();
+ },
+ get() {
+ return Promise.resolve({
+ [provider.id]: {
+ version: STARTPAGE_VERSION,
+ url: provider.url,
+ messages,
+ etag: "etag0987654321",
+ lastFetched: Date.now(),
+ },
+ });
+ },
+ };
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ { ...FAKE_OPTIONS, storage: fakeStorage }
+ );
+ assert.equal(result.messages.length, messages.length);
+ messages.forEach(message => {
+ assert.ok(result.messages.find(m => m.id === message.id));
+ });
+ });
+
+ it("should return fetch results if the cache messages are expired", async () => {
+ clock.tick(302);
+ const testMessage = { id: "foo" };
+ const respJson = { messages: [testMessage] };
+ const fakeStorage = {
+ set() {
+ return Promise.resolve();
+ },
+ get() {
+ return Promise.resolve({
+ [provider.id]: {
+ version: STARTPAGE_VERSION,
+ url: provider.url,
+ messages: [{ id: "message-1" }, { id: "message-2" }],
+ etag: "etag0987654321",
+ lastFetched: 1,
+ },
+ });
+ },
+ };
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () => Promise.resolve(respJson),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ { ...FAKE_OPTIONS, storage: fakeStorage }
+ );
+ assert.equal(result.messages.length, 1);
+ assert.equal(result.messages[0].id, testMessage.id);
+ });
+ });
+ it("should return an empty array for a remote provider with a blank URL without attempting a request", async () => {
+ const provider = { id: "provider123", type: "remote", url: "" };
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.notCalled(fetchStub);
+ assert.deepEqual(result.messages, []);
+ });
+ it("should return .lastUpdated with the time at which the messages were fetched", async () => {
+ const sourceMessage = { id: "foo" };
+ const provider = {
+ id: "provider123",
+ type: "remote",
+ url: "foo.com",
+ };
+
+ fetchStub.resolves({
+ ok: true,
+ status: 200,
+ json: () =>
+ new Promise(resolve => {
+ clock.tick(42);
+ resolve({ messages: [sourceMessage] });
+ }),
+ headers: FAKE_RESPONSE_HEADERS,
+ });
+
+ const result = await MessageLoaderUtils.loadMessagesForProvider(
+ provider,
+ FAKE_OPTIONS
+ );
+
+ assert.propertyVal(result, "lastUpdated", 42);
+ });
+ });
+
+ describe("#shouldProviderUpdate", () => {
+ it("should return true if the provider does not had a .lastUpdated property", () => {
+ assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({ id: "foo" }));
+ });
+ it("should return false if the provider does not had a .updateCycleInMs property and has a .lastUpdated", () => {
+ clock.tick(1);
+ assert.isFalse(
+ MessageLoaderUtils.shouldProviderUpdate({ id: "foo", lastUpdated: 0 })
+ );
+ });
+ it("should return true if the time since .lastUpdated is greater than .updateCycleInMs", () => {
+ clock.tick(301);
+ assert.isTrue(
+ MessageLoaderUtils.shouldProviderUpdate({
+ id: "foo",
+ lastUpdated: 0,
+ updateCycleInMs: 300,
+ })
+ );
+ });
+ it("should return false if the time since .lastUpdated is less than .updateCycleInMs", () => {
+ clock.tick(299);
+ assert.isFalse(
+ MessageLoaderUtils.shouldProviderUpdate({
+ id: "foo",
+ lastUpdated: 0,
+ updateCycleInMs: 300,
+ })
+ );
+ });
+ });
+
+ describe("#cleanupCache", () => {
+ it("should remove data for providers no longer active", async () => {
+ const fakeStorage = {
+ get: sinon.stub().returns(
+ Promise.resolve({
+ "id-1": {},
+ "id-2": {},
+ "id-3": {},
+ })
+ ),
+ set: sinon.stub().returns(Promise.resolve()),
+ };
+ const fakeProviders = [
+ { id: "id-1", type: "remote" },
+ { id: "id-3", type: "remote" },
+ ];
+
+ await MessageLoaderUtils.cleanupCache(fakeProviders, fakeStorage);
+
+ assert.calledOnce(fakeStorage.set);
+ assert.calledWith(
+ fakeStorage.set,
+ MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY,
+ { "id-1": {}, "id-3": {} }
+ );
+ });
+ });
+});