Source code
Revision control
Copy as Markdown
Other Tools
import { MessageLoaderUtils } from "modules/ASRouter.sys.mjs";
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",
};
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");
});
describe("remote provider HTTP codes", () => {
const testMessage = { id: "foo" };
const provider = {
id: "provider123",
type: "remote",
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,
);
});
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,
);
});
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",
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": {} }
);
});
});
});