MessageLoaderUtils.test.js (14068B)
1 import { MessageLoaderUtils } from "modules/ASRouter.sys.mjs"; 2 const { STARTPAGE_VERSION } = MessageLoaderUtils; 3 4 const FAKE_OPTIONS = { 5 storage: { 6 set() { 7 return Promise.resolve(); 8 }, 9 get() { 10 return Promise.resolve(); 11 }, 12 }, 13 dispatchToAS: () => {}, 14 }; 15 const FAKE_RESPONSE_HEADERS = { get() {} }; 16 17 describe("MessageLoaderUtils", () => { 18 let fetchStub; 19 let clock; 20 let sandbox; 21 22 beforeEach(() => { 23 sandbox = sinon.createSandbox(); 24 clock = sinon.useFakeTimers(); 25 fetchStub = sinon.stub(global, "fetch"); 26 }); 27 afterEach(() => { 28 sandbox.restore(); 29 clock.restore(); 30 fetchStub.restore(); 31 }); 32 33 describe("#loadMessagesForProvider", () => { 34 it("should return messages for a local provider with hardcoded messages", async () => { 35 const sourceMessage = { id: "foo" }; 36 const provider = { 37 id: "provider123", 38 type: "local", 39 messages: [sourceMessage], 40 }; 41 42 const result = await MessageLoaderUtils.loadMessagesForProvider( 43 provider, 44 FAKE_OPTIONS 45 ); 46 47 assert.isArray(result.messages); 48 // Does the message have the right properties? 49 const [message] = result.messages; 50 assert.propertyVal(message, "id", "foo"); 51 assert.propertyVal(message, "provider", "provider123"); 52 }); 53 it("should filter out local messages listed in the `exclude` field", async () => { 54 const sourceMessage = { id: "foo" }; 55 const provider = { 56 id: "provider123", 57 type: "local", 58 messages: [sourceMessage], 59 exclude: ["foo"], 60 }; 61 62 const result = await MessageLoaderUtils.loadMessagesForProvider( 63 provider, 64 FAKE_OPTIONS 65 ); 66 67 assert.lengthOf(result.messages, 0); 68 }); 69 it("should return messages for remote provider", async () => { 70 const sourceMessage = { id: "foo" }; 71 fetchStub.resolves({ 72 ok: true, 73 status: 200, 74 json: () => Promise.resolve({ messages: [sourceMessage] }), 75 headers: FAKE_RESPONSE_HEADERS, 76 }); 77 const provider = { 78 id: "provider123", 79 type: "remote", 80 url: "https://foo.com", 81 }; 82 83 const result = await MessageLoaderUtils.loadMessagesForProvider( 84 provider, 85 FAKE_OPTIONS 86 ); 87 assert.isArray(result.messages); 88 // Does the message have the right properties? 89 const [message] = result.messages; 90 assert.propertyVal(message, "id", "foo"); 91 assert.propertyVal(message, "provider", "provider123"); 92 assert.propertyVal(message, "provider_url", "https://foo.com"); 93 }); 94 describe("remote provider HTTP codes", () => { 95 const testMessage = { id: "foo" }; 96 const provider = { 97 id: "provider123", 98 type: "remote", 99 url: "https://foo.com", 100 updateCycleInMs: 300, 101 }; 102 const respJson = { messages: [testMessage] }; 103 104 function assertReturnsCorrectMessages(actual) { 105 assert.isArray(actual.messages); 106 // Does the message have the right properties? 107 const [message] = actual.messages; 108 assert.propertyVal(message, "id", testMessage.id); 109 assert.propertyVal(message, "provider", provider.id); 110 assert.propertyVal(message, "provider_url", provider.url); 111 } 112 113 it("should return messages for 200 response", async () => { 114 fetchStub.resolves({ 115 ok: true, 116 status: 200, 117 json: () => Promise.resolve(respJson), 118 headers: FAKE_RESPONSE_HEADERS, 119 }); 120 assertReturnsCorrectMessages( 121 await MessageLoaderUtils.loadMessagesForProvider( 122 provider, 123 FAKE_OPTIONS 124 ) 125 ); 126 }); 127 128 it("should return messages for a 302 response with json", async () => { 129 fetchStub.resolves({ 130 ok: true, 131 status: 302, 132 json: () => Promise.resolve(respJson), 133 headers: FAKE_RESPONSE_HEADERS, 134 }); 135 assertReturnsCorrectMessages( 136 await MessageLoaderUtils.loadMessagesForProvider( 137 provider, 138 FAKE_OPTIONS 139 ) 140 ); 141 }); 142 143 it("should return an empty array for a 204 response", async () => { 144 fetchStub.resolves({ 145 ok: true, 146 status: 204, 147 json: () => "", 148 headers: FAKE_RESPONSE_HEADERS, 149 }); 150 const result = await MessageLoaderUtils.loadMessagesForProvider( 151 provider, 152 FAKE_OPTIONS 153 ); 154 assert.deepEqual(result.messages, []); 155 }); 156 157 it("should return an empty array for a 500 response", async () => { 158 fetchStub.resolves({ 159 ok: false, 160 status: 500, 161 json: () => "", 162 headers: FAKE_RESPONSE_HEADERS, 163 }); 164 const result = await MessageLoaderUtils.loadMessagesForProvider( 165 provider, 166 FAKE_OPTIONS 167 ); 168 assert.deepEqual(result.messages, []); 169 }); 170 171 it("should return cached messages for a 304 response", async () => { 172 clock.tick(302); 173 const messages = [{ id: "message-1" }, { id: "message-2" }]; 174 const fakeStorage = { 175 set() { 176 return Promise.resolve(); 177 }, 178 get() { 179 return Promise.resolve({ 180 [provider.id]: { 181 version: STARTPAGE_VERSION, 182 url: provider.url, 183 messages, 184 etag: "etag0987654321", 185 lastFetched: 1, 186 }, 187 }); 188 }, 189 }; 190 fetchStub.resolves({ 191 ok: true, 192 status: 304, 193 json: () => "", 194 headers: FAKE_RESPONSE_HEADERS, 195 }); 196 const result = await MessageLoaderUtils.loadMessagesForProvider( 197 provider, 198 { ...FAKE_OPTIONS, storage: fakeStorage } 199 ); 200 assert.equal(result.messages.length, messages.length); 201 messages.forEach(message => { 202 assert.ok(result.messages.find(m => m.id === message.id)); 203 }); 204 }); 205 206 it("should return an empty array if json doesn't parse properly", async () => { 207 fetchStub.resolves({ 208 ok: false, 209 status: 200, 210 json: () => "", 211 headers: FAKE_RESPONSE_HEADERS, 212 }); 213 const result = await MessageLoaderUtils.loadMessagesForProvider( 214 provider, 215 FAKE_OPTIONS 216 ); 217 assert.deepEqual(result.messages, []); 218 }); 219 220 it("should report response parsing errors with MessageLoaderUtils.reportError", async () => { 221 const err = {}; 222 sandbox.spy(MessageLoaderUtils, "reportError"); 223 fetchStub.resolves({ 224 ok: true, 225 status: 200, 226 json: sandbox.stub().rejects(err), 227 headers: FAKE_RESPONSE_HEADERS, 228 }); 229 await MessageLoaderUtils.loadMessagesForProvider( 230 provider, 231 FAKE_OPTIONS 232 ); 233 234 assert.calledOnce(MessageLoaderUtils.reportError); 235 // Report that json parsing failed 236 assert.calledWith(MessageLoaderUtils.reportError, err); 237 }); 238 239 it("should report missing `messages` with MessageLoaderUtils.reportError", async () => { 240 sandbox.spy(MessageLoaderUtils, "reportError"); 241 fetchStub.resolves({ 242 ok: true, 243 status: 200, 244 json: sandbox.stub().resolves({}), 245 headers: FAKE_RESPONSE_HEADERS, 246 }); 247 await MessageLoaderUtils.loadMessagesForProvider( 248 provider, 249 FAKE_OPTIONS 250 ); 251 252 assert.calledOnce(MessageLoaderUtils.reportError); 253 // Report no messages returned 254 assert.calledWith( 255 MessageLoaderUtils.reportError, 256 "No messages returned from https://foo.com." 257 ); 258 }); 259 260 it("should report bad status responses with MessageLoaderUtils.reportError", async () => { 261 sandbox.spy(MessageLoaderUtils, "reportError"); 262 fetchStub.resolves({ 263 ok: false, 264 status: 500, 265 json: sandbox.stub().resolves({}), 266 headers: FAKE_RESPONSE_HEADERS, 267 }); 268 await MessageLoaderUtils.loadMessagesForProvider( 269 provider, 270 FAKE_OPTIONS 271 ); 272 273 assert.calledOnce(MessageLoaderUtils.reportError); 274 // Report no messages returned 275 assert.calledWith( 276 MessageLoaderUtils.reportError, 277 "Invalid response status 500 from https://foo.com." 278 ); 279 }); 280 281 it("should return an empty array if the request rejects", async () => { 282 fetchStub.rejects(new Error("something went wrong")); 283 const result = await MessageLoaderUtils.loadMessagesForProvider( 284 provider, 285 FAKE_OPTIONS 286 ); 287 assert.deepEqual(result.messages, []); 288 }); 289 }); 290 describe("remote provider caching", () => { 291 const provider = { 292 id: "provider123", 293 type: "remote", 294 url: "https://foo.com", 295 updateCycleInMs: 300, 296 }; 297 298 it("should return cached results if they aren't expired", async () => { 299 clock.tick(1); 300 const messages = [{ id: "message-1" }, { id: "message-2" }]; 301 const fakeStorage = { 302 set() { 303 return Promise.resolve(); 304 }, 305 get() { 306 return Promise.resolve({ 307 [provider.id]: { 308 version: STARTPAGE_VERSION, 309 url: provider.url, 310 messages, 311 etag: "etag0987654321", 312 lastFetched: Date.now(), 313 }, 314 }); 315 }, 316 }; 317 const result = await MessageLoaderUtils.loadMessagesForProvider( 318 provider, 319 { ...FAKE_OPTIONS, storage: fakeStorage } 320 ); 321 assert.equal(result.messages.length, messages.length); 322 messages.forEach(message => { 323 assert.ok(result.messages.find(m => m.id === message.id)); 324 }); 325 }); 326 327 it("should return fetch results if the cache messages are expired", async () => { 328 clock.tick(302); 329 const testMessage = { id: "foo" }; 330 const respJson = { messages: [testMessage] }; 331 const fakeStorage = { 332 set() { 333 return Promise.resolve(); 334 }, 335 get() { 336 return Promise.resolve({ 337 [provider.id]: { 338 version: STARTPAGE_VERSION, 339 url: provider.url, 340 messages: [{ id: "message-1" }, { id: "message-2" }], 341 etag: "etag0987654321", 342 lastFetched: 1, 343 }, 344 }); 345 }, 346 }; 347 fetchStub.resolves({ 348 ok: true, 349 status: 200, 350 json: () => Promise.resolve(respJson), 351 headers: FAKE_RESPONSE_HEADERS, 352 }); 353 const result = await MessageLoaderUtils.loadMessagesForProvider( 354 provider, 355 { ...FAKE_OPTIONS, storage: fakeStorage } 356 ); 357 assert.equal(result.messages.length, 1); 358 assert.equal(result.messages[0].id, testMessage.id); 359 }); 360 }); 361 it("should return an empty array for a remote provider with a blank URL without attempting a request", async () => { 362 const provider = { id: "provider123", type: "remote", url: "" }; 363 364 const result = await MessageLoaderUtils.loadMessagesForProvider( 365 provider, 366 FAKE_OPTIONS 367 ); 368 369 assert.notCalled(fetchStub); 370 assert.deepEqual(result.messages, []); 371 }); 372 it("should return .lastUpdated with the time at which the messages were fetched", async () => { 373 const sourceMessage = { id: "foo" }; 374 const provider = { 375 id: "provider123", 376 type: "remote", 377 url: "foo.com", 378 }; 379 380 fetchStub.resolves({ 381 ok: true, 382 status: 200, 383 json: () => 384 new Promise(resolve => { 385 clock.tick(42); 386 resolve({ messages: [sourceMessage] }); 387 }), 388 headers: FAKE_RESPONSE_HEADERS, 389 }); 390 391 const result = await MessageLoaderUtils.loadMessagesForProvider( 392 provider, 393 FAKE_OPTIONS 394 ); 395 396 assert.propertyVal(result, "lastUpdated", 42); 397 }); 398 }); 399 400 describe("#shouldProviderUpdate", () => { 401 it("should return true if the provider does not had a .lastUpdated property", () => { 402 assert.isTrue(MessageLoaderUtils.shouldProviderUpdate({ id: "foo" })); 403 }); 404 it("should return false if the provider does not had a .updateCycleInMs property and has a .lastUpdated", () => { 405 clock.tick(1); 406 assert.isFalse( 407 MessageLoaderUtils.shouldProviderUpdate({ id: "foo", lastUpdated: 0 }) 408 ); 409 }); 410 it("should return true if the time since .lastUpdated is greater than .updateCycleInMs", () => { 411 clock.tick(301); 412 assert.isTrue( 413 MessageLoaderUtils.shouldProviderUpdate({ 414 id: "foo", 415 lastUpdated: 0, 416 updateCycleInMs: 300, 417 }) 418 ); 419 }); 420 it("should return false if the time since .lastUpdated is less than .updateCycleInMs", () => { 421 clock.tick(299); 422 assert.isFalse( 423 MessageLoaderUtils.shouldProviderUpdate({ 424 id: "foo", 425 lastUpdated: 0, 426 updateCycleInMs: 300, 427 }) 428 ); 429 }); 430 }); 431 432 describe("#cleanupCache", () => { 433 it("should remove data for providers no longer active", async () => { 434 const fakeStorage = { 435 get: sinon.stub().returns( 436 Promise.resolve({ 437 "id-1": {}, 438 "id-2": {}, 439 "id-3": {}, 440 }) 441 ), 442 set: sinon.stub().returns(Promise.resolve()), 443 }; 444 const fakeProviders = [ 445 { id: "id-1", type: "remote" }, 446 { id: "id-3", type: "remote" }, 447 ]; 448 449 await MessageLoaderUtils.cleanupCache(fakeProviders, fakeStorage); 450 451 assert.calledOnce(fakeStorage.set); 452 assert.calledWith( 453 fakeStorage.set, 454 MessageLoaderUtils.REMOTE_LOADER_CACHE_KEY, 455 { "id-1": {}, "id-3": {} } 456 ); 457 }); 458 }); 459 });