diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index d8c3ee582..fea8bcc69 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -192,31 +192,32 @@ class AlertStore { this.filters.setFilters(initialFilters); } - // fetch is throttled to once per 500ms - fetch = action( - throttle(() => { - this.status.setInProgress(); + fetch = action(() => { + this.status.setInProgress(); - const alertsURI = - FormatUnseeBackendURI("alerts.json?") + - FormatAPIFilterQuery(this.filters.values.map(f => f.raw)); + const alertsURI = + FormatUnseeBackendURI("alerts.json?") + + FormatAPIFilterQuery(this.filters.values.map(f => f.raw)); - fetch(alertsURI) - .then(result => result.json()) - .then(result => { - this.parseAPIResponse(result); - }) - .catch(err => - this.handleFetchError( - `Request for ${alertsURI} failed with "${err.message}"` - ) + return fetch(alertsURI) + .then(result => result.json()) + .then(result => { + return this.parseAPIResponse(result); + }) + .catch(err => { + console.trace(err); + return this.handleFetchError( + `Request for ${alertsURI} failed with "${err.message}"` ); - }, 500) - ); + }); + }); + + fetchWithThrottle = throttle(this.fetch, 500); parseAPIResponse = action(result => { if (result.error) { this.handleFetchError(result.error); + return; } const queryFilters = new Set( @@ -226,7 +227,9 @@ class AlertStore { .sort() ); const responseFilters = new Set(result.filters.map(m => m.text).sort()); - if (JSON.stringify(queryFilters) !== JSON.stringify(responseFilters)) { + if ( + JSON.stringify([...queryFilters]) !== JSON.stringify([...responseFilters]) + ) { console.info( `Got response with filters=${responseFilters} while expecting results for ${queryFilters}, ignoring` ); diff --git a/ui/src/Stores/AlertStore.test.js b/ui/src/Stores/AlertStore.test.js index f168b0686..67bd24a4a 100644 --- a/ui/src/Stores/AlertStore.test.js +++ b/ui/src/Stores/AlertStore.test.js @@ -1,3 +1,6 @@ +import { ConsoleMock } from "__mocks__/Console"; +import { FetchMock, EmptyAPIResponse } from "__mocks__/Fetch"; + import { AlertStore, AlertStoreStatuses, @@ -5,6 +8,16 @@ import { DecodeLocationSearch } from "Stores/AlertStore"; +beforeEach(() => { + // wipe REACT_APP_BACKEND_URI env on each run as it's used by some tests + delete process.env.REACT_APP_BACKEND_URI; +}); + +afterEach(() => { + // same after each + delete process.env.REACT_APP_BACKEND_URI; +}); + describe("AlertStore.status", () => { it("status is initially idle with no error", () => { const store = new AlertStore([]); @@ -116,11 +129,6 @@ describe("AlertStore.filters", () => { }); describe("FormatUnseeBackendURI", () => { - beforeEach(() => { - // wipe REACT_APP_BACKEND_URI env on each run as it's used by some tests - delete process.env.REACT_APP_BACKEND_URI; - }); - it("FormatUnseeBackendURI without REACT_APP_BACKEND_URI env returns ./ prefixed URIs", () => { const uri = FormatUnseeBackendURI("foo/bar"); expect(uri).toEqual("./foo/bar"); @@ -182,3 +190,88 @@ describe("DecodeLocationSearch", () => { }); }); }); + +describe("AlertStore.fetch", () => { + it("parseAPIResponse() rejects a response with mismatched filters", () => { + const consoleSpy = ConsoleMock("info"); + + const response = EmptyAPIResponse(); + const store = new AlertStore([]); + store.parseAPIResponse(response); + + expect(store.status.value).toEqual(AlertStoreStatuses.Idle); + // there should be no filters set on AlertStore instance since we started + // with 0 and rejected response with 1 filter + expect(store.filters.values).toHaveLength(0); + // console.info should have been called since we emited a warning + expect(consoleSpy).toHaveBeenCalledTimes(1); + + consoleSpy.mockRestore(); + }); + + it("parseAPIResponse() works for a single filter 'label=value'", () => { + const response = EmptyAPIResponse(); + + const store = new AlertStore(["label=value"]); + store.parseAPIResponse(response); + + expect(store.status.value).toEqual(AlertStoreStatuses.Idle); + expect(store.info.version).toBe("fakeVersion"); + }); + + it("fetch() works with valid response", async () => { + const response = EmptyAPIResponse(); + global.fetch = FetchMock(response); + + const store = new AlertStore(["label=value"]); + await expect(store.fetch()).resolves.toBeUndefined(); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(store.status.value).toEqual(AlertStoreStatuses.Idle); + expect(store.info.version).toBe("fakeVersion"); + + global.fetch.mockRestore(); + }); + + it("fetch() handles response with error correctly", async () => { + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + json: () => ({ + error: "Fetch error" + }) + }) + ); + + const store = new AlertStore([]); + await expect(store.fetch()).resolves.toBeUndefined(); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(store.status.value).toEqual(AlertStoreStatuses.Failure); + expect(store.info.version).toBe("unknown"); + + global.fetch.mockRestore(); + }); + + it("fetch() handles response that throws an error correctly", async () => { + const consoleSpy = ConsoleMock("trace"); + global.fetch = jest.fn().mockImplementation(() => + Promise.resolve({ + json: () => { + throw new Error("Failed fetch"); + } + }) + ); + + const store = new AlertStore([]); + await expect(store.fetch()).resolves.toHaveProperty("error"); + + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(store.status.value).toEqual(AlertStoreStatuses.Failure); + expect(store.info.version).toBe("unknown"); + // there should be a trace of the error + expect(consoleSpy).toHaveBeenCalledTimes(1); + + consoleSpy.mockRestore(); + global.fetch.mockRestore(); + }); +});