459 lines
14 KiB
JavaScript
459 lines
14 KiB
JavaScript
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",
|
|
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": {} }
|
|
);
|
|
});
|
|
});
|
|
});
|