MessageLoaderUtils.test.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. import {GlobalOverrider} from "test/unit/utils";
  2. import {MessageLoaderUtils} from "lib/ASRouter.jsm";
  3. const {STARTPAGE_VERSION} = MessageLoaderUtils;
  4. const FAKE_OPTIONS = {
  5. storage: {
  6. set() {
  7. return Promise.resolve();
  8. },
  9. get() { return Promise.resolve(); },
  10. },
  11. dispatchToAS: () => {},
  12. };
  13. const FAKE_RESPONSE_HEADERS = {get() {}};
  14. describe("MessageLoaderUtils", () => {
  15. let fetchStub;
  16. let clock;
  17. let sandbox;
  18. beforeEach(() => {
  19. sandbox = sinon.createSandbox();
  20. clock = sinon.useFakeTimers();
  21. fetchStub = sinon.stub(global, "fetch");
  22. });
  23. afterEach(() => {
  24. sandbox.restore();
  25. clock.restore();
  26. fetchStub.restore();
  27. });
  28. describe("#loadMessagesForProvider", () => {
  29. it("should return messages for a local provider with hardcoded messages", async () => {
  30. const sourceMessage = {id: "foo"};
  31. const provider = {id: "provider123", type: "local", messages: [sourceMessage]};
  32. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  33. assert.isArray(result.messages);
  34. // Does the message have the right properties?
  35. const [message] = result.messages;
  36. assert.propertyVal(message, "id", "foo");
  37. assert.propertyVal(message, "provider", "provider123");
  38. });
  39. it("should filter out local messages listed in the `exclude` field", async () => {
  40. const sourceMessage = {id: "foo"};
  41. const provider = {id: "provider123", type: "local", messages: [sourceMessage], exclude: ["foo"]};
  42. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  43. assert.lengthOf(result.messages, 0);
  44. });
  45. it("should return messages for remote provider", async () => {
  46. const sourceMessage = {id: "foo"};
  47. fetchStub.resolves({ok: true, status: 200, json: () => Promise.resolve({messages: [sourceMessage]}), headers: FAKE_RESPONSE_HEADERS});
  48. const provider = {id: "provider123", type: "remote", url: "https://foo.com"};
  49. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  50. assert.isArray(result.messages);
  51. // Does the message have the right properties?
  52. const [message] = result.messages;
  53. assert.propertyVal(message, "id", "foo");
  54. assert.propertyVal(message, "provider", "provider123");
  55. assert.propertyVal(message, "provider_url", "https://foo.com");
  56. });
  57. describe("remote provider HTTP codes", () => {
  58. const testMessage = {id: "foo"};
  59. const provider = {id: "provider123", type: "remote", url: "https://foo.com", updateCycleInMs: 300};
  60. const respJson = {messages: [testMessage]};
  61. function assertReturnsCorrectMessages(actual) {
  62. assert.isArray(actual.messages);
  63. // Does the message have the right properties?
  64. const [message] = actual.messages;
  65. assert.propertyVal(message, "id", testMessage.id);
  66. assert.propertyVal(message, "provider", provider.id);
  67. assert.propertyVal(message, "provider_url", provider.url);
  68. }
  69. it("should return messages for 200 response", async () => {
  70. fetchStub.resolves({ok: true, status: 200, json: () => Promise.resolve(respJson), headers: FAKE_RESPONSE_HEADERS});
  71. assertReturnsCorrectMessages(await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS));
  72. });
  73. it("should return messages for a 302 response with json", async () => {
  74. fetchStub.resolves({ok: true, status: 302, json: () => Promise.resolve(respJson), headers: FAKE_RESPONSE_HEADERS});
  75. assertReturnsCorrectMessages(await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS));
  76. });
  77. it("should return an empty array for a 204 response", async () => {
  78. fetchStub.resolves({ok: true, status: 204, json: () => "", headers: FAKE_RESPONSE_HEADERS});
  79. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  80. assert.deepEqual(result.messages, []);
  81. });
  82. it("should return an empty array for a 500 response", async () => {
  83. fetchStub.resolves({ok: false, status: 500, json: () => "", headers: FAKE_RESPONSE_HEADERS});
  84. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  85. assert.deepEqual(result.messages, []);
  86. });
  87. it("should return cached messages for a 304 response", async () => {
  88. clock.tick(302);
  89. const messages = [{id: "message-1"}, {id: "message-2"}];
  90. const fakeStorage = {
  91. set() {
  92. return Promise.resolve();
  93. },
  94. get() {
  95. return Promise.resolve({
  96. [provider.id]: {
  97. version: STARTPAGE_VERSION,
  98. url: provider.url,
  99. messages,
  100. etag: "etag0987654321",
  101. lastFetched: 1,
  102. },
  103. });
  104. },
  105. };
  106. fetchStub.resolves({ok: true, status: 304, json: () => "", headers: FAKE_RESPONSE_HEADERS});
  107. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, {...FAKE_OPTIONS, storage: fakeStorage});
  108. assert.equal(result.messages.length, messages.length);
  109. messages.forEach(message => {
  110. assert.ok(result.messages.find(m => m.id === message.id));
  111. });
  112. });
  113. it("should return an empty array if json doesn't parse properly", async () => {
  114. fetchStub.resolves({ok: false, status: 200, json: () => "", headers: FAKE_RESPONSE_HEADERS});
  115. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  116. assert.deepEqual(result.messages, []);
  117. });
  118. it("should report response parsing errors with MessageLoaderUtils.reportError", async () => {
  119. const err = {};
  120. sandbox.spy(MessageLoaderUtils, "reportError");
  121. fetchStub.resolves({ok: true, status: 200, json: sandbox.stub().rejects(err), headers: FAKE_RESPONSE_HEADERS});
  122. await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  123. assert.calledOnce(MessageLoaderUtils.reportError);
  124. // Report that json parsing failed
  125. assert.calledWith(MessageLoaderUtils.reportError, err);
  126. });
  127. it("should report missing `messages` with MessageLoaderUtils.reportError", async () => {
  128. sandbox.spy(MessageLoaderUtils, "reportError");
  129. fetchStub.resolves({ok: true, status: 200, json: sandbox.stub().resolves({}), headers: FAKE_RESPONSE_HEADERS});
  130. await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  131. assert.calledOnce(MessageLoaderUtils.reportError);
  132. // Report no messages returned
  133. assert.calledWith(MessageLoaderUtils.reportError, "No messages returned from https://foo.com.");
  134. });
  135. it("should report bad status responses with MessageLoaderUtils.reportError", async () => {
  136. sandbox.spy(MessageLoaderUtils, "reportError");
  137. fetchStub.resolves({ok: false, status: 500, json: sandbox.stub().resolves({}), headers: FAKE_RESPONSE_HEADERS});
  138. await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  139. assert.calledOnce(MessageLoaderUtils.reportError);
  140. // Report no messages returned
  141. assert.calledWith(MessageLoaderUtils.reportError, "Invalid response status 500 from https://foo.com.");
  142. });
  143. it("should return an empty array if the request rejects", async () => {
  144. fetchStub.rejects(new Error("something went wrong"));
  145. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  146. assert.deepEqual(result.messages, []);
  147. });
  148. });
  149. describe("remote provider caching", () => {
  150. const provider = {id: "provider123", type: "remote", url: "https://foo.com", updateCycleInMs: 300};
  151. it("should return cached results if they aren't expired", async () => {
  152. clock.tick(1);
  153. const messages = [{id: "message-1"}, {id: "message-2"}];
  154. const fakeStorage = {
  155. set() { return Promise.resolve(); },
  156. get() {
  157. return Promise.resolve({
  158. [provider.id]: {
  159. version: STARTPAGE_VERSION,
  160. url: provider.url,
  161. messages,
  162. etag: "etag0987654321",
  163. lastFetched: Date.now(),
  164. },
  165. });
  166. },
  167. };
  168. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, {...FAKE_OPTIONS, storage: fakeStorage});
  169. assert.equal(result.messages.length, messages.length);
  170. messages.forEach(message => {
  171. assert.ok(result.messages.find(m => m.id === message.id));
  172. });
  173. });
  174. it("should return fetch results if the cache messages are expired", async () => {
  175. clock.tick(302);
  176. const testMessage = {id: "foo"};
  177. const respJson = {messages: [testMessage]};
  178. const fakeStorage = {
  179. set() { return Promise.resolve(); },
  180. get() {
  181. return Promise.resolve({
  182. [provider.id]: {
  183. version: STARTPAGE_VERSION,
  184. url: provider.url,
  185. messages: [{id: "message-1"}, {id: "message-2"}],
  186. etag: "etag0987654321",
  187. lastFetched: 1,
  188. },
  189. });
  190. },
  191. };
  192. fetchStub.resolves({ok: true, status: 200, json: () => Promise.resolve(respJson), headers: FAKE_RESPONSE_HEADERS});
  193. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, {...FAKE_OPTIONS, storage: fakeStorage});
  194. assert.equal(result.messages.length, 1);
  195. assert.equal(result.messages[0].id, testMessage.id);
  196. });
  197. });
  198. it("should return an empty array for a remote provider with a blank URL without attempting a request", async () => {
  199. const provider = {id: "provider123", type: "remote", url: ""};
  200. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  201. assert.notCalled(fetchStub);
  202. assert.deepEqual(result.messages, []);
  203. });
  204. it("should return .lastUpdated with the time at which the messages were fetched", async () => {
  205. const sourceMessage = {id: "foo"};
  206. const provider = {
  207. id: "provider123",
  208. type: "remote",
  209. url: "foo.com",
  210. };
  211. fetchStub.resolves({
  212. ok: true,
  213. status: 200,
  214. json: () => new Promise(resolve => {
  215. clock.tick(42);
  216. resolve({messages: [sourceMessage]});
  217. }),
  218. headers: FAKE_RESPONSE_HEADERS,
  219. });
  220. const result = await MessageLoaderUtils.loadMessagesForProvider(provider, FAKE_OPTIONS);
  221. assert.propertyVal(result, "lastUpdated", 42);
  222. });
  223. });
  224. describe("#shouldProviderUpdate", () => {
  225. it("should return true if the provider does not had a .lastUpdated property", () => {
  226. assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({id: "foo"}));
  227. });
  228. it("should return false if the provider does not had a .updateCycleInMs property and has a .lastUpdated", () => {
  229. clock.tick(1);
  230. assert.isFalse(MessageLoaderUtils.shouldProviderUpdate({id: "foo", lastUpdated: 0}));
  231. });
  232. it("should return true if the time since .lastUpdated is greater than .updateCycleInMs", () => {
  233. clock.tick(301);
  234. assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({id: "foo", lastUpdated: 0, updateCycleInMs: 300}));
  235. });
  236. it("should return false if the time since .lastUpdated is less than .updateCycleInMs", () => {
  237. clock.tick(299);
  238. assert.isFalse(MessageLoaderUtils.shouldProviderUpdate({id: "foo", lastUpdated: 0, updateCycleInMs: 300}));
  239. });
  240. });
  241. describe("#_loadAddonIconInURLBar", () => {
  242. let notificationContainerEl;
  243. let browser;
  244. let getContainerStub;
  245. beforeEach(() => {
  246. notificationContainerEl = {style: {}};
  247. browser = {ownerDocument: {getElementById() { return {}; }}};
  248. getContainerStub = sandbox.stub(browser.ownerDocument, "getElementById");
  249. });
  250. it("should return for empty args", () => {
  251. MessageLoaderUtils._loadAddonIconInURLBar();
  252. assert.notCalled(getContainerStub);
  253. });
  254. it("should return if notification popup box not found", () => {
  255. getContainerStub.returns(null);
  256. MessageLoaderUtils._loadAddonIconInURLBar(browser);
  257. assert.calledOnce(getContainerStub);
  258. });
  259. it("should unhide notification popup box with display style as none", () => {
  260. getContainerStub.returns(notificationContainerEl);
  261. notificationContainerEl.style.display = "none";
  262. MessageLoaderUtils._loadAddonIconInURLBar(browser);
  263. assert.calledWith(browser.ownerDocument.getElementById, "notification-popup-box");
  264. assert.equal(notificationContainerEl.style.display, "block");
  265. });
  266. it("should unhide notification popup box with display style empty", () => {
  267. getContainerStub.returns(notificationContainerEl);
  268. notificationContainerEl.style.display = "";
  269. MessageLoaderUtils._loadAddonIconInURLBar(browser);
  270. assert.calledWith(browser.ownerDocument.getElementById, "notification-popup-box");
  271. assert.equal(notificationContainerEl.style.display, "block");
  272. });
  273. });
  274. describe("#installAddonFromURL", () => {
  275. let globals;
  276. let getInstallStub;
  277. let installAddonStub;
  278. beforeEach(() => {
  279. globals = new GlobalOverrider();
  280. getInstallStub = sandbox.stub();
  281. installAddonStub = sandbox.stub();
  282. sandbox.stub(MessageLoaderUtils, "_loadAddonIconInURLBar").returns(null);
  283. globals.set("AddonManager", {
  284. getInstallForURL: getInstallStub,
  285. installAddonFromWebpage: installAddonStub,
  286. });
  287. });
  288. afterEach(() => {
  289. globals.restore();
  290. });
  291. it("should call the Addons API when passed a valid URL", async () => {
  292. getInstallStub.resolves(null);
  293. installAddonStub.resolves(null);
  294. await MessageLoaderUtils.installAddonFromURL({}, "foo.com");
  295. assert.calledOnce(getInstallStub);
  296. assert.calledOnce(installAddonStub);
  297. // Verify that the expected installation source has been passed to the getInstallForURL
  298. // method (See Bug 1496167 for a rationale).
  299. assert.calledWithExactly(getInstallStub, "foo.com", {telemetryInfo: {source: "amo"}});
  300. });
  301. it("should not call the Addons API on invalid URLs", async () => {
  302. sandbox.stub(global.Services.scriptSecurityManager, "getSystemPrincipal").throws();
  303. await MessageLoaderUtils.installAddonFromURL({}, "https://foo.com");
  304. assert.notCalled(getInstallStub);
  305. assert.notCalled(installAddonStub);
  306. });
  307. });
  308. describe("#cleanupCache", () => {
  309. it("should remove data for providers no longer active", async () => {
  310. const fakeStorage = {
  311. get: sinon.stub().returns(Promise.resolve({
  312. "id-1": {},
  313. "id-2": {},
  314. "id-3": {},
  315. })),
  316. set: sinon.stub().returns(Promise.resolve()),
  317. };
  318. const fakeProviders = [{id: "id-1", type: "remote"}, {id: "id-3", type: "remote"}];
  319. await MessageLoaderUtils.cleanupCache(fakeProviders, fakeStorage);
  320. assert.calledOnce(fakeStorage.set);
  321. assert.calledWith(fakeStorage.set, MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, {"id-1": {}, "id-3": {}});
  322. });
  323. });
  324. });