fix(ui): remove alertmanager API V1 support

This commit is contained in:
Łukasz Mierzwa
2020-02-12 16:09:55 +00:00
parent 908c2b96d5
commit d0f59977b0
20 changed files with 142 additions and 383 deletions
+2 -1
View File
@@ -20942,7 +20942,8 @@
"semver": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.1.3.tgz",
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA=="
"integrity": "sha512-ekM0zfiA9SCBlsKa2X1hxyxiI4L3B6EbVJkkdgQXnSEEaHlGdvyodMruTiulSRWMMB4NeIuYNMC9rTKTz97GxA==",
"dev": true
},
"send": {
"version": "0.17.1",
-1
View File
@@ -56,7 +56,6 @@
"react-select": "3.0.8",
"react-tippy": "1.3.4",
"react-transition-group": "4.3.0",
"semver": "7.1.3",
"typeface-open-sans": "0.0.75",
"typescript": "3.7.5"
},
+14 -31
View File
@@ -6,8 +6,6 @@ import { observer } from "mobx-react";
import moment from "moment";
import satisfies from "semver/functions/satisfies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCheck } from "@fortawesome/free-solid-svg-icons/faCheck";
import { faCheckCircle } from "@fortawesome/free-solid-svg-icons/faCheckCircle";
@@ -142,38 +140,23 @@ const AlertAck = observer(
return;
}
const isOpenAPI = satisfies(am.version, ">=0.16.0");
const uri = isOpenAPI
? `${am.uri}/api/v2/silences`
: `${am.uri}/api/v1/silences`;
this.submitState.silencesByCluster[cluster].fetch = FetchPost(uri, {
body: JSON.stringify(
this.submitState.silencesByCluster[cluster].payload
),
headers: {
"Content-Type": "application/json",
...am.headers
this.submitState.silencesByCluster[cluster].fetch = FetchPost(
`${am.uri}/api/v2/silences`,
{
body: JSON.stringify(
this.submitState.silencesByCluster[cluster].payload
),
headers: {
"Content-Type": "application/json",
...am.headers
}
}
})
)
.then(result => {
if (isOpenAPI) {
if (result.ok) {
return result
.json()
.then(r => this.submitState.markDone(cluster));
} else {
this.maybeTryAgainAfterError(cluster);
}
if (result.ok) {
return result.json().then(r => this.submitState.markDone(cluster));
} else {
return result
.json()
.then(r =>
r.status === "success"
? this.submitState.markDone(cluster)
: this.maybeTryAgainAfterError(cluster)
);
this.maybeTryAgainAfterError(cluster);
}
})
.catch(() => {
+9 -83
View File
@@ -38,7 +38,7 @@ beforeEach(() => {
readonly: false,
headers: { foo: "bar" },
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default"]
}
@@ -102,22 +102,8 @@ describe("<AlertAck />", () => {
expect(toDiffableHtml(tree.html())).toMatch(/fa-exclamation-circle/);
});
it("[v1] uses faCheckCircle after successful fetch", async () => {
fetch.mockResponse(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
const tree = MountedAlertAck();
const button = tree.find("span.badge");
button.simulate("click");
await expect(
tree.instance().submitState.silencesByCluster["default"].fetch
).resolves.toBeUndefined();
expect(toDiffableHtml(tree.html())).toMatch(/fa-check-circle/);
});
it("[v2] uses faCheckCircle after successful fetch", async () => {
it("uses faCheckCircle after successful fetch", async () => {
fetch.mockResponse(JSON.stringify({ silenceID: "123" }));
alertStore.data.upstreams.instances[0].version = "0.16.2";
const tree = MountedAlertAck();
const button = tree.find("span.badge");
button.simulate("click");
@@ -166,9 +152,7 @@ describe("<AlertAck />", () => {
});
it("sends correct payload", () => {
fetch.mockResponse(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
fetch.mockResponse(JSON.stringify({ silenceID: "123456789" }));
silenceFormStore.data.author = "karma/ui";
MountAndClick();
@@ -262,71 +246,13 @@ describe("<AlertAck />", () => {
});
});
it("[v1] sends POST request to /api/v1/silences", () => {
MountAndClick();
const uri = fetch.mock.calls[0][0];
expect(uri).toBe("http://localhost/api/v1/silences");
});
it("[v2] sends POST request to /api/v2/silences", () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
it("sends POST request to /api/v2/silences", () => {
MountAndClick();
const uri = fetch.mock.calls[0][0];
expect(uri).toBe("http://localhost/api/v2/silences");
});
it("[v1] will retry on another cluster member after fetch failure", async () => {
fetch
.mockResponseOnce(JSON.stringify({ status: "error" }))
.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
alertStore.data.upstreams = {
clusters: { default: ["default", "fallback"] },
instances: [
{
name: "default",
uri: "http://am1.example.com",
publicURI: "http://am1.example.com",
readonly: false,
headers: {},
error: "",
version: "0.15.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
},
{
name: "fallback",
uri: "http://am2.example.com",
publicURI: "http://am2.example.com",
readonly: false,
headers: {},
error: "",
version: "0.15.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
}
]
};
const tree = MountedAlertAck();
const button = tree.find("span.badge");
button.simulate("click");
await expect(
tree.instance().submitState.silencesByCluster["default"].fetch
).resolves.toBeUndefined();
expect(fetch.mock.calls[0][0]).toBe(
"http://am2.example.com/api/v1/silences"
);
await expect(
tree.instance().submitState.silencesByCluster["default"].fetch
).resolves.toBeUndefined();
expect(fetch.mock.calls[1][0]).toBe(
"http://am1.example.com/api/v1/silences"
);
});
it("[v2] will retry on another cluster member after fetch failure", async () => {
it("will retry on another cluster member after fetch failure", async () => {
fetch
.mockResponseOnce("error message", { status: 500 })
.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
@@ -340,7 +266,7 @@ describe("<AlertAck />", () => {
readonly: false,
headers: {},
error: "",
version: "0.16.2",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
},
@@ -351,7 +277,7 @@ describe("<AlertAck />", () => {
readonly: false,
headers: {},
error: "",
version: "0.16.2",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
}
@@ -389,7 +315,7 @@ describe("<AlertAck />", () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
}
@@ -403,7 +329,7 @@ describe("<AlertAck />", () => {
tree.instance().submitState.silencesByCluster["default"].fetch
).resolves.toBeUndefined();
expect(fetch.mock.calls[0][0]).toBe(
"http://am1.example.com/api/v1/silences"
"http://am1.example.com/api/v2/silences"
);
expect(consoleSpy).toHaveBeenCalledTimes(1);
});
@@ -28,7 +28,7 @@ beforeEach(() => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["am1"]
}
@@ -26,7 +26,7 @@ beforeEach(() => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["am1"]
}
@@ -4,8 +4,6 @@ import PropTypes from "prop-types";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import satisfies from "semver/functions/satisfies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -115,26 +113,6 @@ const DeleteSilenceModalContent = observer(
.filter(u => u.cluster === this.props.cluster)
.slice(0, 1)[0];
parseAlertmanagerResponse = response => {
/*
{"status": "success"}
or
{
"status": "error",
"errorType": "bad_data",
"error": "silence 706959fd-4590-4e21-b983-859ba6ec0e1a already expired"
}
*/
if (response.status === "success") {
this.deleteState.setError(null);
} else if (response.status === "error" && response.error) {
this.deleteState.setError(response.error);
} else {
this.deleteState.setError(JSON.stringify(response));
}
this.deleteState.setDone();
};
onFetchPreview = () => {
const { silence } = this.props;
@@ -169,32 +147,25 @@ const DeleteSilenceModalContent = observer(
const alertmanager = this.getAlertmanager();
const isOpenAPI = satisfies(alertmanager.version, ">=0.16.0");
const uri = isOpenAPI
? `${alertmanager.uri}/api/v2/silence/${silence.id}`
: `${alertmanager.uri}/api/v1/silence/${silence.id}`;
this.deleteState.fetch = FetchDelete(uri, {
headers: alertmanager.headers
})
this.deleteState.fetch = FetchDelete(
`${alertmanager.uri}/api/v2/silence/${silence.id}`,
{
headers: alertmanager.headers
}
)
.then(result => {
if (isOpenAPI) {
if (result.ok) {
this.deleteState.setError(null);
this.deleteState.setDone();
} else {
result.text().then(this.deleteState.setError);
this.deleteState.setDone();
}
if (result.ok) {
this.deleteState.setError(null);
this.deleteState.setDone();
} else {
result.json().then(this.parseAlertmanagerResponse);
result.text().then(this.deleteState.setError);
this.deleteState.setDone();
}
})
.catch(err => {
console.trace(err);
this.deleteState.setDone();
this.deleteState.setError(
return this.deleteState.setError(
`Delete request failed with: ${err.message}`
);
});
@@ -18,7 +18,7 @@ beforeEach(() => {
silenceFormStore = new SilenceFormStore();
cluster = "am";
silence = MockSilence();
fetch.mockResponse(JSON.stringify(MockAPIResponse()));
fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
alertStore.data.upstreams = {
instances: [
@@ -28,7 +28,7 @@ beforeEach(() => {
uri: "http://localhost:9093",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {},
clusterMembers: ["am1"]
}
@@ -94,6 +94,17 @@ const VerifyResponse = async response => {
return tree;
};
const VerifyError = async error => {
const tree = MountedDeleteSilenceModalContent();
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
fetch.mockResponseOnce(error, { status: 500 });
tree.find(".btn-danger").simulate("click");
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
return tree;
};
describe("<DeleteSilence />", () => {
it("label is 'Delete' by default", () => {
const tree = MountedDeleteSilence();
@@ -147,8 +158,10 @@ describe("<DeleteSilenceModalContent />", () => {
expect(fetch).toHaveBeenCalled();
});
it("renders ErrorMessage on failed fetch", async () => {
jest.spyOn(console, "trace").mockImplementation(() => {});
it("renders ErrorMessage on failed preview fetch", async () => {
const consoleSpy = jest
.spyOn(console, "trace")
.mockImplementation(() => {});
fetch.resetMocks();
fetch.mockReject(new Error("Fetch error"));
@@ -156,9 +169,10 @@ describe("<DeleteSilenceModalContent />", () => {
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
expect(consoleSpy).toHaveBeenCalledTimes(1);
});
it("renders ErrorMessage on fetch with non-JSON response", async () => {
it("renders ErrorMessage on preview fetch with non-JSON response", async () => {
fetch.mockResponseOnce("not json");
jest.spyOn(console, "trace").mockImplementation(() => {});
fetch.resetMocks();
@@ -170,16 +184,7 @@ describe("<DeleteSilenceModalContent />", () => {
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("[v1] sends a DELETE request after clicking 'Confirm' button", async () => {
await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
"http://localhost:9093/api/v1/silence/04d37636-2350-4878-b382-e0b50353230f"
);
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
});
it("[v2] sends a DELETE request after clicking 'Confirm' button", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
it("sends a DELETE request after clicking 'Confirm' button", async () => {
await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
"http://localhost:9093/api/v2/silence/04d37636-2350-4878-b382-e0b50353230f"
@@ -187,27 +192,11 @@ describe("<DeleteSilenceModalContent />", () => {
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
});
it("[v1] sends headers from alertmanager config", async () => {
it("sends headers from alertmanager config", async () => {
alertStore.data.upstreams.instances[0].headers = {
Authorization: "Basic ***"
};
await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
"http://localhost:9093/api/v1/silence/04d37636-2350-4878-b382-e0b50353230f"
);
expect(fetch.mock.calls[1][1]).toMatchObject({
credentials: "include",
method: "DELETE",
headers: { Authorization: "Basic ***" }
});
});
it("[v1] sends headers from alertmanager config", async () => {
alertStore.data.upstreams.instances[0].headers = {
Authorization: "Basic ***"
};
alertStore.data.upstreams.instances[0].version = "0.16.2";
await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
"http://localhost:9093/api/v2/silence/04d37636-2350-4878-b382-e0b50353230f"
);
@@ -221,7 +210,7 @@ describe("<DeleteSilenceModalContent />", () => {
it("'Confirm' button is no-op after successful DELETE", async () => {
const tree = await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
"http://localhost:9093/api/v1/silence/04d37636-2350-4878-b382-e0b50353230f"
"http://localhost:9093/api/v2/silence/04d37636-2350-4878-b382-e0b50353230f"
);
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
@@ -232,58 +221,37 @@ describe("<DeleteSilenceModalContent />", () => {
expect(fetch.mock.calls).toHaveLength(2);
});
it("renders SuccessMessage on 'success' response status", async () => {
it("renders SuccessMessage on successful response status", async () => {
const tree = await VerifyResponse({ status: "success" });
tree.update();
expect(tree.find("SuccessMessage")).toHaveLength(1);
});
it("renders ErrorMessage on 'error' response status", async () => {
const tree = await VerifyResponse({ status: "error", error: "fake error" });
it("renders ErrorMessage on failed delete fetch request", async () => {
const tree = await VerifyError("mock error");
tree.find(".btn-danger").simulate("click");
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("renders ErrorMessage on unhandled response status", async () => {
const tree = await VerifyResponse({ status: "foo bar" });
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("renders ErrorMessage on unhandled response body", async () => {
const tree = await VerifyResponse({ foo: "bar" });
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("[v1] renders ErrorMessage on failed fetch request", async () => {
const tree = MountedDeleteSilenceModalContent();
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
jest.spyOn(console, "trace").mockImplementation(() => {});
it("renders ErrorMessage on failed delete fetch", async () => {
const consoleSpy = jest
.spyOn(console, "trace")
.mockImplementation(() => {});
fetch.resetMocks();
fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
fetch.mockReject(new Error("Fetch error"));
tree.find(".btn-danger").simulate("click");
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("[v2] renders ErrorMessage on failed fetch request", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
const tree = MountedDeleteSilenceModalContent();
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
jest.spyOn(console, "trace").mockImplementation(() => {});
fetch.resetMocks();
fetch.mockResponseOnce("500 Internal Server Error", { status: 500 });
tree.find(".btn-danger").simulate("click");
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
expect(consoleSpy).toHaveBeenCalledTimes(1);
});
});
@@ -47,7 +47,7 @@ const MockMultipleClusters = () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
},
@@ -58,7 +58,7 @@ const MockMultipleClusters = () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "fallback"]
},
@@ -69,7 +69,7 @@ const MockMultipleClusters = () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "second",
clusterMembers: ["second"]
}
@@ -36,7 +36,7 @@ beforeEach(() => {
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -32,7 +32,7 @@ storiesOf("ManagedSilence", module)
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -50,7 +50,7 @@ storiesOf("ManagedSilence", module)
readonly: true,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ro",
clusterMembers: ["readonly"]
}
@@ -35,7 +35,7 @@ beforeEach(() => {
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -98,7 +98,7 @@ describe("<ManagedSilence />", () => {
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
});
});
@@ -114,7 +114,7 @@ describe("<ManagedSilence />", () => {
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
},
{
@@ -125,7 +125,7 @@ describe("<ManagedSilence />", () => {
publicURI: "http://example.com",
readonly: true,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -143,7 +143,7 @@ describe("<ManagedSilence />", () => {
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
});
});
@@ -30,7 +30,7 @@ beforeEach(() => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
},
@@ -41,7 +41,7 @@ beforeEach(() => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
},
@@ -52,7 +52,7 @@ beforeEach(() => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "am3",
clusterMembers: ["am3"]
}
@@ -38,7 +38,7 @@ beforeEach(() => {
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -28,7 +28,7 @@ beforeEach(() => {
uri: "http://localhost:9093",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -4,8 +4,6 @@ import PropTypes from "prop-types";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import satisfies from "semver/functions/satisfies";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
@@ -33,18 +31,18 @@ const SubmitIcon = observer(({ stateValue }) => {
return <FontAwesomeIcon icon={faCircleNotch} spin />;
});
const SilenceLink = ({ uri, silenceId }) => (
const SilenceLink = ({ uri, silenceID }) => (
<a
href={`${uri}/#/silences/${silenceId}`}
href={`${uri}/#/silences/${silenceID}`}
target="_blank"
rel="noopener noreferrer"
>
{silenceId}
{silenceID}
</a>
);
SilenceLink.propTypes = {
uri: PropTypes.string.isRequired,
silenceId: PropTypes.string.isRequired
silenceID: PropTypes.string.isRequired
};
const SilenceSubmitProgress = observer(
@@ -84,7 +82,7 @@ const SilenceSubmitProgress = observer(
maybeTryAgainAfterError = err => {
if (this.submitState.membersToTry.length) {
this.handleAlertmanagerRequest();
return this.handleAlertmanagerRequest();
} else {
this.submitState.markFailed(err.message);
}
@@ -106,17 +104,10 @@ const SilenceSubmitProgress = observer(
if (am === undefined) {
const err = `Alertmanager instance "${member}" not found`;
console.error(err);
this.maybeTryAgainAfterError(err);
return;
return this.maybeTryAgainAfterError(err);
}
const isOpenAPI = satisfies(am.version, ">=0.16.0");
const uri = isOpenAPI
? `${am.uri}/api/v2/silences`
: `${am.uri}/api/v1/silences`;
this.submitState.fetch = FetchPost(uri, {
this.submitState.fetch = FetchPost(`${am.uri}/api/v2/silences`, {
body: JSON.stringify(payload),
headers: {
"Content-Type": "application/json",
@@ -124,51 +115,27 @@ const SilenceSubmitProgress = observer(
}
})
.then(result => {
if (isOpenAPI) {
if (result.ok) {
return result
.json()
.then(r => this.parseOpenAPIResponse(am.publicURI, r));
} else {
return result.text().then(text => {
this.submitState.markFailed(text);
return text;
});
}
} else {
if (result.ok) {
return result
.json()
.then(r => this.parseAlertmanagerResponse(am.publicURI, r));
.then(r => this.parseOpenAPIResponse(am.publicURI, r));
} else {
return result.text().then(text => {
this.submitState.markFailed(text);
return text;
});
}
})
.catch(err => {
this.maybeTryAgainAfterError(err);
});
.catch(this.maybeTryAgainAfterError);
};
parseOpenAPIResponse = (uri, response) => {
const link = <SilenceLink uri={uri} silenceId={response.silenceID} />;
const link = <SilenceLink uri={uri} silenceID={response.silenceID} />;
this.submitState.markDone(link);
// return silenceId so we can assert it in tests
// return silenceID so we can assert it in tests
return response.silenceID;
};
parseAlertmanagerResponse = (uri, response) => {
if (response.status === "success") {
const link = (
<SilenceLink uri={uri} silenceId={response.data.silenceId} />
);
this.submitState.markDone(link);
} else if (response.status === "error") {
this.submitState.markFailed(response.error);
} else {
this.submitState.markFailed(JSON.stringify(response));
}
// return status so we can assert it in tests
return response.status;
};
componentDidMount() {
const { members } = this.props;
this.submitState.membersToTry = [...members];
@@ -18,16 +18,19 @@ beforeEach(() => {
readonly: false,
headers: { foo: "bar" },
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "mockAlertmanager",
clusterMembers: ["mockAlertmanager"]
}
]
};
fetch.resetMocks();
});
afterEach(() => {
jest.restoreAllMocks();
fetch.resetMocks();
});
const MountedSilenceSubmitProgress = () => {
@@ -54,19 +57,11 @@ describe("<SilenceSubmitProgress />", () => {
expect(fetch.mock.calls).toHaveLength(1);
});
it("[v1] appends /api/v1/silences to the passed URI", async () => {
it("appends /api/v2/silences to the passed URI", async () => {
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
const uri = fetch.mock.calls[0][0];
expect(uri).toBe("http://localhost/api/v1/silences");
});
it("[v2] appends /api/v2/silences to the passed URI", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
const uri = fetch.mock.calls[0][0];
expect(uri).toBe("http://localhost/api/v1/silences");
expect(uri).toBe("http://localhost/api/v2/silences");
});
it("sends correct JSON payload", () => {
@@ -86,12 +81,9 @@ describe("<SilenceSubmitProgress />", () => {
});
it("will retry on another cluster member after fetch failure", async () => {
fetch.resetMocks();
fetch
.mockRejectOnce(new Error("mock error message"))
.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
.mockResponseOnce(JSON.stringify({ silenceID: "123456789" }));
alertStore.data.upstreams = {
clusters: { ha: ["am1", "am2"] },
instances: [
@@ -102,7 +94,7 @@ describe("<SilenceSubmitProgress />", () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
},
@@ -113,7 +105,7 @@ describe("<SilenceSubmitProgress />", () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
}
@@ -136,11 +128,11 @@ describe("<SilenceSubmitProgress />", () => {
);
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
expect(fetch.mock.calls[0][0]).toBe(
"http://am2.example.com/api/v1/silences"
"http://am2.example.com/api/v2/silences"
);
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
await expect(tree.instance().submitState.fetch).resolves.toBe("123456789");
expect(fetch.mock.calls[1][0]).toBe(
"http://am1.example.com/api/v1/silences"
"http://am1.example.com/api/v2/silences"
);
});
@@ -160,7 +152,7 @@ describe("<SilenceSubmitProgress />", () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
}
@@ -183,7 +175,7 @@ describe("<SilenceSubmitProgress />", () => {
);
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
expect(fetch.mock.calls[0][0]).toBe(
"http://am1.example.com/api/v1/silences"
"http://am1.example.com/api/v2/silences"
);
expect(consoleSpy).toHaveBeenCalledTimes(1);
});
@@ -207,7 +199,7 @@ describe("<SilenceSubmitProgress />", () => {
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
},
@@ -218,7 +210,7 @@ describe("<SilenceSubmitProgress />", () => {
readonly: true,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "ha",
clusterMembers: ["am1", "am2"]
}
@@ -242,18 +234,16 @@ describe("<SilenceSubmitProgress />", () => {
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
expect(fetch.mock.calls).toHaveLength(1);
expect(fetch.mock.calls[0][0]).toBe(
"http://am1.example.com/api/v1/silences"
"http://am1.example.com/api/v2/silences"
);
expect(logs).toEqual(['Alertmanager instance "am2" is read-only']);
expect(consoleSpy).toHaveBeenCalledTimes(1);
});
it("renders returned silence ID on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123456789" } })
);
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123456789" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
await expect(tree.instance().submitState.fetch).resolves.toBe("123456789");
// force re-render
tree.update();
const silenceLink = tree.find("a");
@@ -262,40 +252,15 @@ describe("<SilenceSubmitProgress />", () => {
});
it("renders returned error message on failed fetch", async () => {
fetch.mockRejectOnce(new Error("mock error message"));
fetch.mockResponseOnce("mock error message", { status: 500 });
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
await expect(tree.instance().submitState.fetch).resolves.toBe(
"mock error message"
);
expect(tree.text()).toBe("mockAlertmanagermock error message");
});
it("[v1] renders success icon on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123" } })
);
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(1);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
});
it("[v1] renders silence link on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123" } })
);
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
tree.update();
expect(
tree
.find("a")
.getDOMNode()
.getAttribute("href")
).toBe("http://example.com/#/silences/123");
});
it("[v2] renders success icon on successful fetch", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
it("renders success icon on successful fetch", async () => {
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("123");
@@ -304,8 +269,7 @@ describe("<SilenceSubmitProgress />", () => {
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
});
it("[v2] renders silence link on successful fetch", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
it("renders silence link on successful fetch", async () => {
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("123");
@@ -318,17 +282,7 @@ describe("<SilenceSubmitProgress />", () => {
).toBe("http://example.com/#/silences/123");
});
it("[v1] renders error icon on failed fetch", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "error" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("error");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
});
it("[v2] renders error icon on failed fetch", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
it("renders error icon on failed fetch", async () => {
fetch.mockResponseOnce("error message", { status: 500 });
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe(
@@ -338,14 +292,4 @@ describe("<SilenceSubmitProgress />", () => {
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
});
it("renders unhandled 'status' values in the response as error", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "unhandled" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("unhandled");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
expect(tree.text()).toBe('mockAlertmanager{"status":"unhandled"}');
});
});
@@ -50,7 +50,7 @@ storiesOf("SilenceModal", module)
readonly: false,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default"]
}
@@ -124,7 +124,7 @@ storiesOf("SilenceModal", module)
readonly: true,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["readonly"]
}
@@ -189,7 +189,7 @@ storiesOf("SilenceModal", module)
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
@@ -252,7 +252,7 @@ storiesOf("SilenceModal", module)
publicURI: "http://example.com",
readonly: false,
error: "",
version: "0.15.3",
version: "0.17.0",
headers: {}
}
],
+4 -4
View File
@@ -33,7 +33,7 @@ describe("AlertStore.data", () => {
readonly: false,
headers: { foo: "bar" },
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "readonly"]
},
@@ -44,7 +44,7 @@ describe("AlertStore.data", () => {
readonly: true,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["default", "readonly"]
}
@@ -69,7 +69,7 @@ describe("AlertStore.data", () => {
readonly: true,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["ro1", "ro2"]
},
@@ -80,7 +80,7 @@ describe("AlertStore.data", () => {
readonly: true,
headers: {},
error: "",
version: "0.15.0",
version: "0.17.0",
cluster: "default",
clusterMembers: ["ro1", "ro2"]
}
+1 -1
View File
@@ -75,7 +75,7 @@ const MockAlertmanager = () => ({
Authorization: "Basic foo bar"
},
error: "",
version: "0.15.0",
version: "0.17.0",
clusterMembers: ["default"]
});