Merge branch 'develop' into 913-fe-advanced-settings-page-validation-and-error-handling
This commit is contained in:
commit
82d44b2b1b
19 files changed
+1673
-422
No files matched your search
@@ -1,9 +1,14 @@
|
||||
## Describe your changes
|
||||
|
||||
## Issue Number if applicable
|
||||
Provide a brief description of the changes you’ve made and their purpose.
|
||||
|
||||
## Checklist before requesting a review
|
||||
## Issue number
|
||||
|
||||
- [ ] I have performed a self-review of my code
|
||||
- [ ] I have inlcuded the issue # in the PR if applicable
|
||||
- [ ] I have labelled the PR correctly
|
||||
Mention the issue number(s) this PR addresses (e.g., #123).
|
||||
|
||||
## Please ensure all items are checked off before requesting a review:
|
||||
|
||||
- [ ] I have performed a self-review of my code.
|
||||
- [ ] I have included the issue # in the PR.
|
||||
- [ ] I have labelled the PR correctly.
|
||||
- [ ] My PR is granular and targeted to one specific feature only.
|
||||
@@ -31,6 +31,30 @@ export const createPageSpeed = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const checkEndpointResolution = createAsyncThunk(
|
||||
"monitors/checkEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorURL } = data;
|
||||
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
})
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const getPagespeedMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
@@ -222,7 +246,24 @@ const pageSpeedMonitorSlice = createSlice({
|
||||
? action.payload.msg
|
||||
: "Failed to create page speed monitor";
|
||||
})
|
||||
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Update Monitor
|
||||
// *****************************************************
|
||||
|
||||
@@ -31,6 +31,30 @@ export const createUptimeMonitor = createAsyncThunk(
|
||||
}
|
||||
);
|
||||
|
||||
export const checkEndpointResolution = createAsyncThunk(
|
||||
"monitors/checkEndpoint",
|
||||
async (data, thunkApi) => {
|
||||
try {
|
||||
const { authToken, monitorURL } = data;
|
||||
|
||||
const res = await networkService.checkEndpointResolution({
|
||||
authToken: authToken,
|
||||
monitorURL: monitorURL,
|
||||
})
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (error.response && error.response.data) {
|
||||
return thunkApi.rejectWithValue(error.response.data);
|
||||
}
|
||||
const payload = {
|
||||
status: false,
|
||||
msg: error.message ? error.message : "Unknown error",
|
||||
};
|
||||
return thunkApi.rejectWithValue(payload);
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
export const getUptimeMonitorById = createAsyncThunk(
|
||||
"monitors/getMonitorById",
|
||||
async (data, thunkApi) => {
|
||||
@@ -271,6 +295,24 @@ const uptimeMonitorsSlice = createSlice({
|
||||
: "Failed to create uptime monitor";
|
||||
})
|
||||
// *****************************************************
|
||||
// Resolve Endpoint
|
||||
// *****************************************************
|
||||
.addCase(checkEndpointResolution.pending, (state) => {
|
||||
state.isLoading = true;
|
||||
})
|
||||
.addCase(checkEndpointResolution.fulfilled, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = action.payload.success;
|
||||
state.msg = action.payload.msg;
|
||||
})
|
||||
.addCase(checkEndpointResolution.rejected, (state, action) => {
|
||||
state.isLoading = false;
|
||||
state.success = false;
|
||||
state.msg = action.payload
|
||||
? action.payload.msg
|
||||
: "Failed to check endpoint resolution";
|
||||
})
|
||||
// *****************************************************
|
||||
// Get Monitor By Id
|
||||
// *****************************************************
|
||||
.addCase(getUptimeMonitorById.pending, (state) => {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { createUptimeMonitor } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { checkEndpointResolution } from "../../../Features/UptimeMonitors/uptimeMonitorsSlice"
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
@@ -17,12 +19,12 @@ import { getUptimeMonitorById } from "../../../Features/UptimeMonitors/uptimeMon
|
||||
import "./index.css";
|
||||
|
||||
const CreateMonitor = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { monitors } = useSelector((state) => state.uptimeMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { monitors, isLoading } = useSelector((state) => state.uptimeMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
@@ -146,6 +148,17 @@ const CreateMonitor = () => {
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
if (monitor.type === "http") {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkEndpointResolution({ authToken, monitorURL: form.url })
|
||||
)
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." });
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
@@ -373,18 +386,19 @@ const CreateMonitor = () => {
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateMonitor;
|
||||
export default CreateMonitor;
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { Box, Button, ButtonGroup, Stack, Typography } from "@mui/material";
|
||||
import LoadingButton from '@mui/lab/LoadingButton';
|
||||
import { useSelector, useDispatch } from "react-redux";
|
||||
import { monitorValidation } from "../../../Validation/validation";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTheme } from "@emotion/react";
|
||||
import { createPageSpeed } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { createPageSpeed, checkEndpointResolution } from "../../../Features/PageSpeedMonitor/pageSpeedMonitorSlice";
|
||||
import { createToast } from "../../../Utils/toastUtils";
|
||||
import { logger } from "../../../Utils/Logger";
|
||||
import { ConfigBox } from "../../Monitors/styled";
|
||||
@@ -16,11 +17,12 @@ import Breadcrumbs from "../../../Components/Breadcrumbs";
|
||||
import "./index.css";
|
||||
|
||||
const CreatePageSpeed = () => {
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
const MS_PER_MINUTE = 60000;
|
||||
const { user, authToken } = useSelector((state) => state.auth);
|
||||
const { isLoading } = useSelector((state) => state.pageSpeedMonitors);
|
||||
const dispatch = useDispatch();
|
||||
const navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
const idMap = {
|
||||
"monitor-url": "url",
|
||||
@@ -103,248 +105,252 @@ const CreatePageSpeed = () => {
|
||||
abortEarly: false,
|
||||
});
|
||||
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(createPageSpeed({ authToken, monitor: form }));
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
if (error) {
|
||||
const newErrors = {};
|
||||
error.details.forEach((err) => {
|
||||
newErrors[err.path[0]] = err.message;
|
||||
});
|
||||
setErrors(newErrors);
|
||||
createToast({ body: "Error validation data." });
|
||||
} else {
|
||||
const checkEndpointAction = await dispatch(
|
||||
checkEndpointResolution({ authToken, monitorURL: form.url })
|
||||
)
|
||||
if (checkEndpointAction.meta.requestStatus === "rejected") {
|
||||
createToast({ body: "The endpoint you entered doesn't resolve. Check the URL again." });
|
||||
setErrors({ url: "The entered URL is not reachable." });
|
||||
return;
|
||||
}
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
return (
|
||||
<Box
|
||||
className="create-monitor"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "create", path: `/pagespeed/create` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography
|
||||
component="h1"
|
||||
variant="h1"
|
||||
>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
>
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
pagespeed monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the type of monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Field
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p">
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="Website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={monitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack
|
||||
direction="row"
|
||||
justifyContent="flex-end"
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
>
|
||||
Create monitor
|
||||
</Button>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
form = {
|
||||
...form,
|
||||
description: form.name,
|
||||
teamId: user.teamId,
|
||||
userId: user._id,
|
||||
notifications: monitor.notifications,
|
||||
};
|
||||
const action = await dispatch(
|
||||
createPageSpeed({ authToken, monitor: form })
|
||||
);
|
||||
if (action.meta.requestStatus === "fulfilled") {
|
||||
createToast({ body: "Monitor created successfully!" });
|
||||
navigate("/pagespeed");
|
||||
} else {
|
||||
createToast({ body: "Failed to create monitor." });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
//select values
|
||||
const frequencies = [
|
||||
{ _id: 3, name: "3 minutes" },
|
||||
{ _id: 5, name: "5 minutes" },
|
||||
{ _id: 10, name: "10 minutes" },
|
||||
{ _id: 20, name: "20 minutes" },
|
||||
{ _id: 60, name: "1 hour" },
|
||||
{ _id: 1440, name: "1 day" },
|
||||
{ _id: 10080, name: "1 week" },
|
||||
];
|
||||
return (
|
||||
<Box
|
||||
className="create-monitor"
|
||||
sx={{
|
||||
"& h1": {
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Breadcrumbs
|
||||
list={[
|
||||
{ name: "pagespeed", path: "/pagespeed" },
|
||||
{ name: "create", path: `/pagespeed/create` },
|
||||
]}
|
||||
/>
|
||||
<Stack
|
||||
component="form"
|
||||
className="create-monitor-form"
|
||||
onSubmit={handleCreateMonitor}
|
||||
noValidate
|
||||
spellCheck="false"
|
||||
gap={theme.spacing(12)}
|
||||
mt={theme.spacing(6)}
|
||||
>
|
||||
<Typography component="h1" variant="h1">
|
||||
<Typography component="span" fontSize="inherit">
|
||||
Create your{" "}
|
||||
</Typography>
|
||||
<Typography
|
||||
component="span"
|
||||
fontSize="inherit"
|
||||
fontWeight="inherit"
|
||||
color={theme.palette.text.secondary}
|
||||
>
|
||||
pagespeed monitor
|
||||
</Typography>
|
||||
</Typography>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">General settings</Typography>
|
||||
<Typography component="p">
|
||||
Here you can select the URL of the host, together with the type of
|
||||
monitor.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(15)}>
|
||||
<Field
|
||||
type={"url"}
|
||||
id="monitor-url"
|
||||
label="URL to monitor"
|
||||
https={https}
|
||||
placeholder="google.com"
|
||||
value={monitor.url}
|
||||
onChange={handleChange}
|
||||
error={errors["url"]}
|
||||
/>
|
||||
<Field
|
||||
type="text"
|
||||
id="monitor-name"
|
||||
label="Display name"
|
||||
isOptional={true}
|
||||
placeholder="Google"
|
||||
value={monitor.name}
|
||||
onChange={handleChange}
|
||||
error={errors["name"]}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Checks to perform</Typography>
|
||||
<Typography component="p">
|
||||
You can always add or remove checks after adding your site.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Radio
|
||||
id="monitor-checks-http"
|
||||
title="Website monitoring"
|
||||
desc="Use HTTP(s) to monitor your website or API endpoint."
|
||||
size="small"
|
||||
value="http"
|
||||
checked={monitor.type === "pagespeed"}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<ButtonGroup sx={{ ml: "32px" }}>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={https.toString()}
|
||||
onClick={() => setHttps(true)}
|
||||
>
|
||||
HTTPS
|
||||
</Button>
|
||||
<Button
|
||||
variant="group"
|
||||
filled={(!https).toString()}
|
||||
onClick={() => setHttps(false)}
|
||||
>
|
||||
HTTP
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Stack>
|
||||
{errors["type"] ? (
|
||||
<Box className="error-container">
|
||||
<Typography
|
||||
component="p"
|
||||
className="input-error"
|
||||
color={theme.palette.error.text}
|
||||
>
|
||||
{errors["type"]}
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Incident notifications</Typography>
|
||||
<Typography component="p">
|
||||
When there is an incident, notify users.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(6)}>
|
||||
<Typography component="p">When there is a new incident,</Typography>
|
||||
<Checkbox
|
||||
id="notify-sms"
|
||||
label="Notify via SMS (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email-default"
|
||||
label={`Notify via email (to ${user.email})`}
|
||||
isChecked={monitor.notifications.some(
|
||||
(notification) => notification.type === "email"
|
||||
)}
|
||||
value={user?.email}
|
||||
onChange={(event) => handleChange(event)}
|
||||
/>
|
||||
<Checkbox
|
||||
id="notify-email"
|
||||
label="Also notify via email to multiple addresses (coming soon)"
|
||||
isChecked={false}
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
isDisabled={true}
|
||||
/>
|
||||
{monitor.notifications.some(
|
||||
(notification) => notification.type === "emails"
|
||||
) ? (
|
||||
<Box mx={theme.spacing(16)}>
|
||||
<Field
|
||||
id="notify-email-list"
|
||||
type="text"
|
||||
placeholder="name@gmail.com"
|
||||
value=""
|
||||
onChange={() => logger.warn("disabled")}
|
||||
/>
|
||||
<Typography mt={theme.spacing(4)}>
|
||||
You can separate multiple emails with a comma
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<ConfigBox>
|
||||
<Box>
|
||||
<Typography component="h2">Advanced settings</Typography>
|
||||
</Box>
|
||||
<Stack gap={theme.spacing(12)}>
|
||||
<Select
|
||||
id="monitor-interval"
|
||||
label="Check frequency"
|
||||
value={monitor.interval || 3}
|
||||
onChange={(event) => handleChange(event, "interval")}
|
||||
items={frequencies}
|
||||
/>
|
||||
</Stack>
|
||||
</ConfigBox>
|
||||
<Stack direction="row" justifyContent="flex-end">
|
||||
<LoadingButton
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleCreateMonitor}
|
||||
disabled={Object.keys(errors).length !== 0 && true}
|
||||
loading={isLoading}
|
||||
>
|
||||
Create monitor
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePageSpeed;
|
||||
@@ -3,7 +3,6 @@ const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL;
|
||||
const FALLBACK_BASE_URL = "http://localhost:5000/api/v1";
|
||||
import { clearAuthState } from "../Features/Auth/authSlice";
|
||||
import { clearUptimeMonitorState } from "../Features/UptimeMonitors/uptimeMonitorsSlice";
|
||||
import { logger } from "./Logger";
|
||||
class NetworkService {
|
||||
constructor(store, dispatch, navigate) {
|
||||
this.store = store;
|
||||
@@ -88,22 +87,48 @@ class NetworkService {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Check the endpoint resolution
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {Object} config.monitorURL - The monitor url to be sent in the request body.
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async checkEndpointResolution(config) {
|
||||
const { authToken, monitorURL } = config;
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (monitorURL) params.append("monitorURL", monitorURL);
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Gets monitors and summary of stats by TeamID
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} config.teamId - Team ID
|
||||
* @param {Array<string>} config.types - Array of monitor types
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async getMonitorsAndSummaryByTeamId(config) {
|
||||
const params = new URLSearchParams();
|
||||
return this.axiosInstance.get(`/monitors/resolution/url?${params.toString()}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${authToken}`,
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* ************************************
|
||||
* Gets monitors and summary of stats by TeamID
|
||||
* ************************************
|
||||
*
|
||||
* @async
|
||||
* @param {Object} config - The configuration object.
|
||||
* @param {string} config.authToken - The authorization token to be used in the request header.
|
||||
* @param {string} config.teamId - Team ID
|
||||
* @param {Array<string>} config.types - Array of monitor types
|
||||
* @returns {Promise<AxiosResponse>} The response from the axios POST request.
|
||||
*/
|
||||
async getMonitorsAndSummaryByTeamId(config) {
|
||||
const params = new URLSearchParams();
|
||||
|
||||
if (config.types) {
|
||||
config.types.forEach((type) => {
|
||||
|
||||
+19
-17
@@ -1,24 +1,26 @@
|
||||
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../..
|
||||
|
||||
#Client
|
||||
client="./Docker/dev/client.Dockerfile"
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["uptime_client"]="./Docker/dev/client.Dockerfile"
|
||||
["uptime_database_mongo"]="./Docker/dev/mongoDB.Dockerfile"
|
||||
["uptime_redis"]="./Docker/dev/redis.Dockerfile"
|
||||
["uptime_server"]="./Docker/dev/server.Dockerfile"
|
||||
)
|
||||
|
||||
# MongoDB
|
||||
mongoDB="./Docker/dev/mongoDB.Dockerfile"
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
|
||||
## Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Redis
|
||||
redis="./Docker/dev/redis.Dockerfile"
|
||||
|
||||
# Server
|
||||
server="./Docker/dev/server.Dockerfile"
|
||||
|
||||
docker build -f $client -t uptime_client .
|
||||
docker build -f $mongoDB -t uptime_database_mongo .
|
||||
docker build -f $redis -t uptime_redis .
|
||||
docker build -f $server -t uptime_server .
|
||||
|
||||
echo "All images built"
|
||||
echo "All images built successfully"
|
||||
@@ -44,14 +44,13 @@ We've just launched our [Discussions](https://github.com/bluewave-labs/bluewave-
|
||||
|
||||
**Roadmap (short term):**
|
||||
|
||||
- [ ] Memory, disk and CPU monitoring
|
||||
- [ ] 3rd party integrations
|
||||
- [ ] DNS monitoring
|
||||
- [ ] SSL monitoring
|
||||
We are actively developing **infrastructure monitoring** features for Uptime Manager, which will include comprehensive monitoring of memory, disk usage, and CPU performance. Our goal is to build a lightweight agent that runs on Linux servers, continuously collecting and transmitting health metrics to Uptime Manager, where the data will be visualized for real-time insights.
|
||||
|
||||
**Roadmap (long term):**
|
||||
|
||||
- [ ] Status pages
|
||||
- DNS monitoring
|
||||
- SSL monitoring
|
||||
- Status pages
|
||||
|
||||
## Tech stack
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
getMonitorByIdQueryValidation,
|
||||
getMonitorsByTeamIdValidation,
|
||||
createMonitorBodyValidation,
|
||||
getMonitorURLByQueryValidation,
|
||||
editMonitorBodyValidation,
|
||||
getMonitorsAndSummaryByTeamIdParamValidation,
|
||||
getMonitorsAndSummaryByTeamIdQueryValidation,
|
||||
@@ -13,13 +14,14 @@ import {
|
||||
getCertificateParamValidation,
|
||||
} from "../validation/joi.js";
|
||||
import sslChecker from "ssl-checker";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
import jwt from "jsonwebtoken";
|
||||
import { getTokenFromHeaders } from "../utils/utils.js";
|
||||
import logger from "../utils/logger.js";
|
||||
import { handleError, handleValidationError } from "./controllerUtils.js";
|
||||
import dns from "dns";
|
||||
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
/**
|
||||
* Returns all monitors
|
||||
@@ -257,6 +259,44 @@ const createMonitor = async (req, res, next) => {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if the endpoint can be resolved
|
||||
* @async
|
||||
* @param {Object} req - The Express request object.
|
||||
* @property {Object} req.query - The query parameters of the request.
|
||||
* @param {Object} res - The Express response object.
|
||||
* @param {function} next - The next middleware function.
|
||||
* @returns {Object} The response object with a success status, a message, and the resolution result.
|
||||
* @throws {Error} If there is an error during the process, especially if there is a validation error (422).
|
||||
*/
|
||||
const checkEndpointResolution = async (req, res, next) => {
|
||||
try {
|
||||
await getMonitorURLByQueryValidation.validateAsync(req.query);
|
||||
} catch (error) {
|
||||
next(handleValidationError(error, SERVICE_NAME));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
let { monitorURL } = req.query;
|
||||
monitorURL = new URL(monitorURL);
|
||||
await new Promise((resolve, reject) => {
|
||||
dns.resolve(monitorURL.hostname, (error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
msg: `URL resolved successfully`,
|
||||
});
|
||||
} catch (error) {
|
||||
next(handleError(error, SERVICE_NAME, "checkEndpointResolution"));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a monitor by its ID and also deletes associated checks, alerts, and notifications.
|
||||
* @async
|
||||
@@ -477,6 +517,7 @@ export {
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
checkEndpointResolution,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
|
||||
@@ -157,7 +157,7 @@ const getTeamChecks = async (req) => {
|
||||
// Default sort order is newest -> oldest
|
||||
sortOrder = sortOrder === "asc" ? 1 : -1;
|
||||
|
||||
checksQuery = { monitorId: { $in: userMonitors } };
|
||||
const checksQuery = { monitorId: { $in: userMonitors } };
|
||||
|
||||
if (filter !== undefined) {
|
||||
checksQuery.status = false;
|
||||
|
||||
+6
-1
@@ -20,7 +20,12 @@ import { fileURLToPath } from "url";
|
||||
import { connectDbAndRunServer } from "./configs/db.js";
|
||||
import queueRouter from "./routes/queueRoute.js";
|
||||
import JobQueue from "./service/jobQueue.js";
|
||||
|
||||
//Network service and dependencies
|
||||
import NetworkService from "./service/networkService.js";
|
||||
import axios from "axios";
|
||||
import ping from "ping";
|
||||
import http from "http";
|
||||
|
||||
// Email service and dependencies
|
||||
import EmailService from "./service/emailService.js";
|
||||
@@ -149,7 +154,7 @@ const startApp = async () => {
|
||||
nodemailer,
|
||||
logger
|
||||
);
|
||||
const networkService = new NetworkService(db, emailService);
|
||||
const networkService = new NetworkService(db, emailService, axios, ping, logger, http);
|
||||
const jobQueue = await JobQueue.createJobQueue(db, networkService, settingsService);
|
||||
|
||||
const cleanup = async () => {
|
||||
|
||||
+303
-63
@@ -22,12 +22,16 @@
|
||||
"variables": {
|
||||
"PORT": {
|
||||
"description": "API Port",
|
||||
"enum": ["5000"],
|
||||
"enum": [
|
||||
"5000"
|
||||
],
|
||||
"default": "5000"
|
||||
},
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": ["api/v1"],
|
||||
"enum": [
|
||||
"api/v1"
|
||||
],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
@@ -38,7 +42,9 @@
|
||||
"variables": {
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": ["api/v1"],
|
||||
"enum": [
|
||||
"api/v1"
|
||||
],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
@@ -49,12 +55,16 @@
|
||||
"variables": {
|
||||
"PORT": {
|
||||
"description": "API Port",
|
||||
"enum": ["5000"],
|
||||
"enum": [
|
||||
"5000"
|
||||
],
|
||||
"default": "5000"
|
||||
},
|
||||
"API_PATH": {
|
||||
"description": "API Base Path",
|
||||
"enum": ["api/v1"],
|
||||
"enum": [
|
||||
"api/v1"
|
||||
],
|
||||
"default": "api/v1"
|
||||
}
|
||||
}
|
||||
@@ -89,7 +99,9 @@
|
||||
"paths": {
|
||||
"/auth/register": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Register a new user",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -125,8 +137,23 @@
|
||||
},
|
||||
"role": {
|
||||
"type": "array",
|
||||
"enum": [["user"], ["admin"], ["superadmin"], ["Demo"]],
|
||||
"default": ["superadmin"]
|
||||
"enum": [
|
||||
[
|
||||
"user"
|
||||
],
|
||||
[
|
||||
"admin"
|
||||
],
|
||||
[
|
||||
"superadmin"
|
||||
],
|
||||
[
|
||||
"Demo"
|
||||
]
|
||||
],
|
||||
"default": [
|
||||
"superadmin"
|
||||
]
|
||||
},
|
||||
"teamId": {
|
||||
"type": "string",
|
||||
@@ -173,14 +200,19 @@
|
||||
},
|
||||
"/auth/login": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Login with credentials",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "password"],
|
||||
"required": [
|
||||
"email",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
@@ -231,7 +263,9 @@
|
||||
},
|
||||
"/auth/user/{userId}": {
|
||||
"put": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Change user information",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -292,7 +326,9 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Delete user",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -345,7 +381,9 @@
|
||||
},
|
||||
"/auth/users/superadmin": {
|
||||
"get": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Checks to see if an admin account exists",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -388,7 +426,9 @@
|
||||
},
|
||||
"/auth/users": {
|
||||
"get": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Get all users",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -431,14 +471,18 @@
|
||||
},
|
||||
"/auth/recovery/request": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Request a recovery token",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email"],
|
||||
"required": [
|
||||
"email"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string",
|
||||
@@ -485,14 +529,18 @@
|
||||
},
|
||||
"/auth/recovery/validate": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Validate recovery token",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["recoveryToken"],
|
||||
"required": [
|
||||
"recoveryToken"
|
||||
],
|
||||
"properties": {
|
||||
"recoveryToken": {
|
||||
"type": "string"
|
||||
@@ -538,14 +586,19 @@
|
||||
},
|
||||
"/auth/recovery/reset": {
|
||||
"post": {
|
||||
"tags": ["auth"],
|
||||
"tags": [
|
||||
"auth"
|
||||
],
|
||||
"description": "Password reset",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["recoveryToken", "password"],
|
||||
"required": [
|
||||
"recoveryToken",
|
||||
"password"
|
||||
],
|
||||
"properties": {
|
||||
"recoveryToken": {
|
||||
"type": "string"
|
||||
@@ -594,14 +647,19 @@
|
||||
},
|
||||
"/invite": {
|
||||
"post": {
|
||||
"tags": ["invite"],
|
||||
"tags": [
|
||||
"invite"
|
||||
],
|
||||
"description": "Request an invitation",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["email", "role"],
|
||||
"required": [
|
||||
"email",
|
||||
"role"
|
||||
],
|
||||
"properties": {
|
||||
"email": {
|
||||
"type": "string"
|
||||
@@ -655,14 +713,18 @@
|
||||
},
|
||||
"/invite/verify": {
|
||||
"post": {
|
||||
"tags": ["invite"],
|
||||
"tags": [
|
||||
"invite"
|
||||
],
|
||||
"description": "Request an invitation",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"required": ["token"],
|
||||
"required": [
|
||||
"token"
|
||||
],
|
||||
"properties": {
|
||||
"token": {
|
||||
"type": "string"
|
||||
@@ -713,7 +775,9 @@
|
||||
},
|
||||
"/monitors": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Get all monitors",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -754,7 +818,9 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Create a new monitor",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -804,7 +870,9 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Delete all monitors",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -845,9 +913,78 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/monitors/resolution/url": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Check DNS resolution for a given URL",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "monitorURL",
|
||||
"in": "query",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"example": "https://example.com"
|
||||
},
|
||||
"description": "The URL to check DNS resolution for"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "URL resolved successfully",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/SuccessResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "DNS resolution failed",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"422": {
|
||||
"description": "Unprocessable Content",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"security": [
|
||||
{
|
||||
"bearerAuth": []
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"/monitors/{monitorId}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Get monitor by id",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -898,7 +1035,9 @@
|
||||
]
|
||||
},
|
||||
"put": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Update monitor by id",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -958,7 +1097,9 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Delete monitor by id",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1011,7 +1152,9 @@
|
||||
},
|
||||
"/monitors/stats/{monitorId}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Get monitor stats",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1064,7 +1207,9 @@
|
||||
},
|
||||
"/monitors/certificate/{monitorId}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Get monitor certificate",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1117,7 +1262,9 @@
|
||||
},
|
||||
"/monitors/team/summary/{teamId}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Get monitors and summary by teamId",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1134,7 +1281,11 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1179,7 +1330,9 @@
|
||||
},
|
||||
"/monitors/team/{teamId}": {
|
||||
"get": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Get monitors by teamId",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1206,7 +1359,10 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["asc", "desc"]
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1225,7 +1381,11 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -1269,7 +1429,11 @@
|
||||
"required": false,
|
||||
"schema": {
|
||||
"type": "string",
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -1314,7 +1478,9 @@
|
||||
},
|
||||
"/monitors/pause/{monitorId}": {
|
||||
"post": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Pause monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1367,7 +1533,9 @@
|
||||
},
|
||||
"/monitors/demo": {
|
||||
"post": {
|
||||
"tags": ["monitors"],
|
||||
"tags": [
|
||||
"monitors"
|
||||
],
|
||||
"description": "Create a demo monitor",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1419,7 +1587,9 @@
|
||||
},
|
||||
"/checks/{monitorId}": {
|
||||
"get": {
|
||||
"tags": ["checks"],
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"description": "Get all checks for a monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1470,7 +1640,9 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["checks"],
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"description": "Create a new check",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1530,7 +1702,9 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["checks"],
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"description": "Delete all checks for a monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1583,7 +1757,9 @@
|
||||
},
|
||||
"/checks/team/{teamId}": {
|
||||
"get": {
|
||||
"tags": ["checks"],
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"description": "Get all checks for a team",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1634,7 +1810,9 @@
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"tags": ["checks"],
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"description": "Delete all checks for a team",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1687,7 +1865,9 @@
|
||||
},
|
||||
"/checks/team/ttl": {
|
||||
"put": {
|
||||
"tags": ["checks"],
|
||||
"tags": [
|
||||
"checks"
|
||||
],
|
||||
"description": "Update check TTL",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
@@ -1739,7 +1919,9 @@
|
||||
},
|
||||
"/maintenance-window/monitor/{monitorId}": {
|
||||
"get": {
|
||||
"tags": ["maintenance-window"],
|
||||
"tags": [
|
||||
"maintenance-window"
|
||||
],
|
||||
"description": "Get maintenance window for monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1790,7 +1972,9 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["maintenance-window"],
|
||||
"tags": [
|
||||
"maintenance-window"
|
||||
],
|
||||
"description": "Create maintenance window for monitor",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1852,7 +2036,9 @@
|
||||
},
|
||||
"/maintenance-window/user/{userId}": {
|
||||
"get": {
|
||||
"tags": ["maintenance-window"],
|
||||
"tags": [
|
||||
"maintenance-window"
|
||||
],
|
||||
"description": "Get maintenance window for user",
|
||||
"parameters": [
|
||||
{
|
||||
@@ -1905,7 +2091,9 @@
|
||||
},
|
||||
"/queue/jobs": {
|
||||
"get": {
|
||||
"tags": ["queue"],
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"description": "Get all jobs in queue",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -1946,7 +2134,9 @@
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"tags": ["queue"],
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"description": "Create a new job. Useful for testing scaling workers",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -1989,7 +2179,9 @@
|
||||
},
|
||||
"/queue/metrics": {
|
||||
"get": {
|
||||
"tags": ["queue"],
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"description": "Get queue metrics",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -2032,7 +2224,9 @@
|
||||
},
|
||||
"/queue/obliterate": {
|
||||
"post": {
|
||||
"tags": ["queue"],
|
||||
"tags": [
|
||||
"queue"
|
||||
],
|
||||
"description": "Obliterate job queue",
|
||||
"responses": {
|
||||
"200": {
|
||||
@@ -2074,7 +2268,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"components": {
|
||||
"securitySchemes": {
|
||||
"bearerAuth": {
|
||||
@@ -2113,7 +2306,14 @@
|
||||
},
|
||||
"UserUpdateRequest": {
|
||||
"type": "object",
|
||||
"required": ["firstName", "lastName", "email", "password", "role", "teamId"],
|
||||
"required": [
|
||||
"firstName",
|
||||
"lastName",
|
||||
"email",
|
||||
"password",
|
||||
"role",
|
||||
"teamId"
|
||||
],
|
||||
"properties": {
|
||||
"firstName": {
|
||||
"type": "string"
|
||||
@@ -2135,8 +2335,23 @@
|
||||
},
|
||||
"role": {
|
||||
"type": "array",
|
||||
"enum": [["user"], ["admin"], ["superadmin"], ["Demo"]],
|
||||
"default": ["superadmin"]
|
||||
"enum": [
|
||||
[
|
||||
"user"
|
||||
],
|
||||
[
|
||||
"admin"
|
||||
],
|
||||
[
|
||||
"superadmin"
|
||||
],
|
||||
[
|
||||
"Demo"
|
||||
]
|
||||
],
|
||||
"default": [
|
||||
"superadmin"
|
||||
]
|
||||
},
|
||||
"deleteProfileImage": {
|
||||
"type": "boolean"
|
||||
@@ -2145,7 +2360,14 @@
|
||||
},
|
||||
"CreateMonitorBody": {
|
||||
"type": "object",
|
||||
"required": ["userId", "teamId", "name", "description", "type", "url"],
|
||||
"required": [
|
||||
"userId",
|
||||
"teamId",
|
||||
"name",
|
||||
"description",
|
||||
"type",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"_id": {
|
||||
"type": "string"
|
||||
@@ -2164,7 +2386,11 @@
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["http", "ping", "pagespeed"]
|
||||
"enum": [
|
||||
"http",
|
||||
"ping",
|
||||
"pagespeed"
|
||||
]
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
@@ -2205,7 +2431,13 @@
|
||||
},
|
||||
"CreateCheckBody": {
|
||||
"type": "object",
|
||||
"required": ["monitorId", "status", "responseTime", "statusCode", "message"],
|
||||
"required": [
|
||||
"monitorId",
|
||||
"status",
|
||||
"responseTime",
|
||||
"statusCode",
|
||||
"message"
|
||||
],
|
||||
"properties": {
|
||||
"monitorId": {
|
||||
"type": "string"
|
||||
@@ -2226,7 +2458,9 @@
|
||||
},
|
||||
"UpdateCheckTTLBody": {
|
||||
"type": "object",
|
||||
"required": ["ttl"],
|
||||
"required": [
|
||||
"ttl"
|
||||
],
|
||||
"properties": {
|
||||
"ttl": {
|
||||
"type": "integer"
|
||||
@@ -2235,7 +2469,13 @@
|
||||
},
|
||||
"CreateMaintenanceWindowBody": {
|
||||
"type": "object",
|
||||
"required": ["userId", "active", "oneTime", "start", "end"],
|
||||
"required": [
|
||||
"userId",
|
||||
"active",
|
||||
"oneTime",
|
||||
"start",
|
||||
"end"
|
||||
],
|
||||
"properties": {
|
||||
"userId": {
|
||||
"type": "string"
|
||||
@@ -2262,4 +2502,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,18 @@
|
||||
import { Router } from "express";
|
||||
import {
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
pauseMonitor,
|
||||
addDemoMonitors,
|
||||
getAllMonitors,
|
||||
getMonitorStatsById,
|
||||
getMonitorCertificate,
|
||||
getMonitorById,
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
checkEndpointResolution,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
pauseMonitor,
|
||||
addDemoMonitors,
|
||||
} from "../controllers/monitorController.js";
|
||||
import { isAllowed } from "../middleware/isAllowed.js";
|
||||
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
|
||||
@@ -21,15 +22,25 @@ const router = Router();
|
||||
router.get("/", getAllMonitors);
|
||||
router.get("/stats/:monitorId", getMonitorStatsById);
|
||||
router.get("/certificate/:monitorId", (req, res, next) => {
|
||||
getMonitorCertificate(req, res, next, fetchMonitorCertificate);
|
||||
getMonitorCertificate(req, res, next, fetchMonitorCertificate);
|
||||
});
|
||||
router.get("/:monitorId", getMonitorById);
|
||||
router.get("/team/summary/:teamId", getMonitorsAndSummaryByTeamId);
|
||||
router.get("/team/:teamId", getMonitorsByTeamId);
|
||||
|
||||
router.post("/", isAllowed(["admin", "superadmin"]), createMonitor);
|
||||
router.get(
|
||||
"/resolution/url",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
checkEndpointResolution
|
||||
)
|
||||
|
||||
router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), deleteMonitor);
|
||||
router.delete(
|
||||
"/:monitorId",
|
||||
isAllowed(["admin", "superadmin"]),
|
||||
deleteMonitor
|
||||
);
|
||||
|
||||
router.post("/", isAllowed(["admin", "superadmin"]), createMonitor);
|
||||
|
||||
router.put("/:monitorId", isAllowed(["admin", "superadmin"]), editMonitor);
|
||||
|
||||
|
||||
@@ -98,7 +98,7 @@ class JobQueue {
|
||||
}, false);
|
||||
|
||||
if (!maintenanceWindowActive) {
|
||||
const res = await this.networkService.getStatus(job);
|
||||
await this.networkService.getStatus(job);
|
||||
} else {
|
||||
logger.info(`Monitor ${monitorId} is in maintenance window`, {
|
||||
service: SERVICE_NAME,
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
import axios from "axios";
|
||||
import ping from "ping";
|
||||
import logger from "../utils/logger.js";
|
||||
import http from "http";
|
||||
import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
|
||||
/**
|
||||
@@ -12,7 +8,17 @@ import { errorMessages, successMessages } from "../utils/messages.js";
|
||||
*/
|
||||
|
||||
class NetworkService {
|
||||
constructor(db, emailService) {
|
||||
/**
|
||||
* Creates an instance of NetworkService.
|
||||
*
|
||||
* @param {Object} db - The database service.
|
||||
* @param {Object} emailService - The email service.
|
||||
* @param {Object} axios - The axios HTTP client.
|
||||
* @param {Object} ping - The ping service.
|
||||
* @param {Object} logger - The logging service.
|
||||
* @param {Object} http - The HTTP service.
|
||||
*/
|
||||
constructor(db, emailService, axios, ping, logger, http) {
|
||||
this.db = db;
|
||||
this.emailService = emailService;
|
||||
this.TYPE_PING = "ping";
|
||||
@@ -20,9 +26,19 @@ class NetworkService {
|
||||
this.TYPE_PAGESPEED = "pagespeed";
|
||||
this.SERVICE_NAME = "NetworkService";
|
||||
this.NETWORK_ERROR = 5000;
|
||||
this.axios = axios;
|
||||
this.ping = ping;
|
||||
this.logger = logger;
|
||||
this.http = http;
|
||||
}
|
||||
|
||||
async handleNotification(monitor, isAlive) {
|
||||
/**
|
||||
* Handles the notification process for a monitor.
|
||||
*
|
||||
* @param {Object} monitor - The monitor object containing monitor details.
|
||||
* @param {boolean} isAlive - The status of the monitor (true if up, false if down).
|
||||
* @returns {Promise<void>}
|
||||
*/ async handleNotification(monitor, isAlive) {
|
||||
try {
|
||||
let template = isAlive === true ? "serverIsUpTemplate" : "serverIsDownTemplate";
|
||||
let status = isAlive === true ? "up" : "down";
|
||||
@@ -39,7 +55,7 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error.message, {
|
||||
this.logger.error(error.message, {
|
||||
method: "handleNotification",
|
||||
service: this.SERVICE_NAME,
|
||||
monitorId: monitor._id,
|
||||
@@ -47,11 +63,19 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the status update for a monitor job.
|
||||
*
|
||||
* @param {Object} job - The job object containing job details.
|
||||
* @param {boolean} isAlive - The status of the monitor (true if up, false if down).
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleStatusUpdate(job, isAlive) {
|
||||
let monitor;
|
||||
const { _id } = job.data;
|
||||
|
||||
// Look up the monitor, if it doesn't exist, it's probably been removed, return
|
||||
try {
|
||||
const { _id } = job.data;
|
||||
monitor = await this.db.getMonitorById(_id);
|
||||
} catch (error) {
|
||||
return;
|
||||
@@ -60,7 +84,7 @@ class NetworkService {
|
||||
// Otherwise, try to update monitor status
|
||||
try {
|
||||
if (monitor === null || monitor === undefined) {
|
||||
logger.error(`Null Monitor: ${_id}`, {
|
||||
this.logger.error(`Null Monitor: ${_id}`, {
|
||||
method: "handleStatusUpdate",
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
@@ -77,7 +101,7 @@ class NetworkService {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(error.message, {
|
||||
this.logger.error(error.message, {
|
||||
method: "handleStatusUpdate",
|
||||
service: this.SERVICE_NAME,
|
||||
jobId: job.id,
|
||||
@@ -100,7 +124,7 @@ class NetworkService {
|
||||
} catch (error) {
|
||||
const endTime = Date.now();
|
||||
error.responseTime = endTime - startTime;
|
||||
error.service === undefined ? (error.service = SERVICE_NAME) : null;
|
||||
error.service === undefined ? (error.service = this.SERVICE_NAME) : null;
|
||||
error.method === undefined ? (error.method = "measureResponseTime") : null;
|
||||
throw error;
|
||||
}
|
||||
@@ -112,13 +136,13 @@ class NetworkService {
|
||||
* @returns {Promise<{boolean}} The result of logging and storing the check
|
||||
*/
|
||||
async handlePing(job) {
|
||||
let isAlive;
|
||||
|
||||
const operation = async () => {
|
||||
const response = await ping.promise.probe(job.data.url);
|
||||
const response = await this.ping.promise.probe(job.data.url);
|
||||
return response;
|
||||
};
|
||||
|
||||
let isAlive;
|
||||
|
||||
try {
|
||||
const { responseTime, response } = await this.measureResponseTime(operation);
|
||||
isAlive = response.alive;
|
||||
@@ -130,7 +154,7 @@ class NetworkService {
|
||||
? successMessages.PING_SUCCESS
|
||||
: errorMessages.PING_CANNOT_RESOLVE,
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
@@ -139,7 +163,7 @@ class NetworkService {
|
||||
message: errorMessages.PING_CANNOT_RESOLVE,
|
||||
responseTime: error.responseTime,
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
@@ -153,7 +177,7 @@ class NetworkService {
|
||||
async handleHttp(job) {
|
||||
// Define operation for timing
|
||||
const operation = async () => {
|
||||
const response = await axios.get(job.data.url);
|
||||
const response = await this.axios.get(job.data.url);
|
||||
return response;
|
||||
};
|
||||
|
||||
@@ -171,12 +195,12 @@ class NetworkService {
|
||||
status: isAlive,
|
||||
responseTime,
|
||||
statusCode: response.status,
|
||||
message: http.STATUS_CODES[response.status],
|
||||
message: this.http.STATUS_CODES[response.status],
|
||||
};
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} catch (error) {
|
||||
const statusCode = error.response?.status || this.NETWORK_ERROR;
|
||||
let message = http.STATUS_CODES[statusCode] || "Network Error";
|
||||
let message = this.http.STATUS_CODES[statusCode] || "Network Error";
|
||||
isAlive = false;
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
@@ -185,8 +209,7 @@ class NetworkService {
|
||||
responseTime: error.responseTime,
|
||||
message,
|
||||
};
|
||||
|
||||
return await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
await this.logAndStoreCheck(checkData, this.db.createCheck);
|
||||
} finally {
|
||||
this.handleStatusUpdate(job, isAlive);
|
||||
}
|
||||
@@ -210,7 +233,8 @@ class NetworkService {
|
||||
let isAlive;
|
||||
try {
|
||||
const url = job.data.url;
|
||||
const response = await axios.get(
|
||||
|
||||
const response = await this.axios.get(
|
||||
`https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`
|
||||
);
|
||||
const pageSpeedResults = response.data;
|
||||
@@ -223,7 +247,6 @@ class NetworkService {
|
||||
"largest-contentful-paint": lcp,
|
||||
"total-blocking-time": tbt,
|
||||
} = audits;
|
||||
|
||||
// Weights
|
||||
// First Contentful Paint 10%
|
||||
// Speed Index 10%
|
||||
@@ -236,7 +259,7 @@ class NetworkService {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
statusCode: response.status,
|
||||
message: http.STATUS_CODES[response.status],
|
||||
message: this.http.STATUS_CODES[response.status],
|
||||
accessibility: (categories.accessibility?.score || 0) * 100,
|
||||
bestPractices: (categories["best-practices"]?.score || 0) * 100,
|
||||
seo: (categories.seo?.score || 0) * 100,
|
||||
@@ -249,12 +272,11 @@ class NetworkService {
|
||||
tbt,
|
||||
},
|
||||
};
|
||||
|
||||
this.logAndStoreCheck(checkData, this.db.createPageSpeedCheck);
|
||||
} catch (error) {
|
||||
isAlive = false;
|
||||
const statusCode = error.response?.status || this.NETWORK_ERROR;
|
||||
const message = http.STATUS_CODES[statusCode] || "Network Error";
|
||||
const message = this.http.STATUS_CODES[statusCode] || "Network Error";
|
||||
const checkData = {
|
||||
monitorId: job.data._id,
|
||||
status: isAlive,
|
||||
@@ -287,7 +309,7 @@ class NetworkService {
|
||||
case this.TYPE_PAGESPEED:
|
||||
return await this.handlePagespeed(job);
|
||||
default:
|
||||
logger.error(`Unsupported type: ${job.data.type}`, {
|
||||
this.logger.error(`Unsupported type: ${job.data.type}`, {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "getStatus",
|
||||
jobId: job.id,
|
||||
@@ -311,8 +333,9 @@ class NetworkService {
|
||||
if (insertedCheck !== null && insertedCheck !== undefined) {
|
||||
return insertedCheck.status;
|
||||
}
|
||||
throw new Error();
|
||||
} catch (error) {
|
||||
logger.error(`Error wrtiting check for ${data.monitorId}`, {
|
||||
this.logger.error(`Error writing check for ${data.monitorId}`, {
|
||||
service: this.SERVICE_NAME,
|
||||
method: "logAndStoreCheck",
|
||||
monitorId: data.monitorId,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
getMonitorsAndSummaryByTeamId,
|
||||
getMonitorsByTeamId,
|
||||
createMonitor,
|
||||
checkEndpointResolution,
|
||||
deleteMonitor,
|
||||
deleteAllMonitors,
|
||||
editMonitor,
|
||||
@@ -16,9 +17,7 @@ import jwt from "jsonwebtoken";
|
||||
import sinon from "sinon";
|
||||
import { successMessages } from "../../utils/messages.js";
|
||||
import logger from "../../utils/logger.js";
|
||||
import * as monitorController from "../../controllers/monitorController.js";
|
||||
import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js";
|
||||
|
||||
import dns from "dns";
|
||||
const SERVICE_NAME = "monitorController";
|
||||
|
||||
describe("Monitor Controller - getAllMonitors", () => {
|
||||
@@ -460,6 +459,50 @@ describe("Monitor Controller - createMonitor", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Monitor Controllor - checkEndpointResolution", () => {
|
||||
let req, res, next, dnsResolveStub;
|
||||
beforeEach(() => {
|
||||
req = { query: { monitorURL: 'https://example.com' } };
|
||||
res = { status: sinon.stub().returnsThis(), json: sinon.stub() };
|
||||
next = sinon.stub();
|
||||
dnsResolveStub = sinon.stub(dns, 'resolve');
|
||||
});
|
||||
afterEach(() => {
|
||||
dnsResolveStub.restore();
|
||||
});
|
||||
it('should resolve the URL successfully', async () => {
|
||||
dnsResolveStub.callsFake((hostname, callback) => callback(null));
|
||||
await checkEndpointResolution(req, res, next);
|
||||
expect(res.status.calledWith(200)).to.be.true;
|
||||
expect(res.json.calledWith({
|
||||
success: true,
|
||||
msg: 'URL resolved successfully',
|
||||
})).to.be.true;
|
||||
expect(next.called).to.be.false;
|
||||
});
|
||||
it("should return an error if DNS resolution fails", async () => {
|
||||
const dnsError = new Error("DNS resolution failed");
|
||||
dnsError.code = 'ENOTFOUND';
|
||||
dnsResolveStub.callsFake((hostname, callback) => callback(dnsError));
|
||||
await checkEndpointResolution(req, res, next);
|
||||
expect(next.calledOnce).to.be.true;
|
||||
const errorPassedToNext = next.getCall(0).args[0];
|
||||
expect(errorPassedToNext).to.be.an.instanceOf(Error);
|
||||
expect(errorPassedToNext.message).to.include('DNS resolution failed');
|
||||
expect(errorPassedToNext.code).to.equal('ENOTFOUND');
|
||||
expect(errorPassedToNext.status).to.equal(500);
|
||||
});
|
||||
it('should reject with an error if query validation fails', async () => {
|
||||
req.query.monitorURL = 'invalid-url';
|
||||
await checkEndpointResolution(req, res, next);
|
||||
expect(next.calledOnce).to.be.true;
|
||||
const error = next.getCall(0).args[0];
|
||||
expect(next.firstCall.args[0]).to.be.an("error");
|
||||
expect(next.firstCall.args[0].status).to.equal(422);
|
||||
expect(error.message).to.equal('"monitorURL" must be a valid uri');
|
||||
});
|
||||
});
|
||||
|
||||
describe("Monitor Controller - deleteMonitor", () => {
|
||||
let req, res, next;
|
||||
beforeEach(() => {
|
||||
|
||||
@@ -0,0 +1,750 @@
|
||||
import sinon from "sinon";
|
||||
import NetworkService from "../../service/networkService.js";
|
||||
import { errorMessages, successMessages } from "../../utils/messages.js";
|
||||
import exp from "constants";
|
||||
describe("NetworkService - Constructor", function () {
|
||||
let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock;
|
||||
|
||||
beforeEach(function () {
|
||||
dbMock = sinon.stub();
|
||||
emailServiceMock = sinon.stub();
|
||||
axiosMock = sinon.stub();
|
||||
pingMock = sinon.stub();
|
||||
loggerMock = sinon.stub();
|
||||
httpMock = sinon.stub();
|
||||
});
|
||||
|
||||
it("should correctly initialize properties", function () {
|
||||
const networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
|
||||
expect(networkService.db).to.equal(dbMock);
|
||||
expect(networkService.emailService).to.equal(emailServiceMock);
|
||||
expect(networkService.TYPE_PING).to.equal("ping");
|
||||
expect(networkService.TYPE_HTTP).to.equal("http");
|
||||
expect(networkService.TYPE_PAGESPEED).to.equal("pagespeed");
|
||||
expect(networkService.SERVICE_NAME).to.equal("NetworkService");
|
||||
expect(networkService.NETWORK_ERROR).to.equal(5000);
|
||||
expect(networkService.axios).to.equal(axiosMock);
|
||||
expect(networkService.ping).to.equal(pingMock);
|
||||
expect(networkService.logger).to.equal(loggerMock);
|
||||
expect(networkService.http).to.equal(httpMock);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NetworkService - handleNotification", () => {
|
||||
let dbMock, emailServiceMock, loggerMock, networkService;
|
||||
|
||||
beforeEach(function () {
|
||||
dbMock = {
|
||||
getNotificationsByMonitorId: sinon.stub(),
|
||||
};
|
||||
emailServiceMock = {
|
||||
buildAndSendEmail: sinon.stub().resolves(),
|
||||
};
|
||||
loggerMock = {
|
||||
error: sinon.stub(),
|
||||
};
|
||||
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
null,
|
||||
null,
|
||||
loggerMock,
|
||||
null
|
||||
);
|
||||
});
|
||||
|
||||
it("should send email notifications when monitor is down", async function () {
|
||||
const monitor = { _id: "monitor1", name: "Test Monitor", url: "http://test.com" };
|
||||
const notifications = [{ type: "email", address: "test@example.com" }];
|
||||
dbMock.getNotificationsByMonitorId.resolves(notifications);
|
||||
|
||||
await networkService.handleNotification(monitor, false);
|
||||
|
||||
expect(emailServiceMock.buildAndSendEmail.calledOnce).to.be.true;
|
||||
expect(
|
||||
emailServiceMock.buildAndSendEmail.calledWith(
|
||||
"serverIsDownTemplate",
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
"test@example.com",
|
||||
"Monitor Test Monitor is down"
|
||||
)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should send email notifications when monitor is up", async function () {
|
||||
const monitor = { _id: "monitor1", name: "Test Monitor", url: "http://test.com" };
|
||||
const notifications = [{ type: "email", address: "test@example.com" }];
|
||||
dbMock.getNotificationsByMonitorId.resolves(notifications);
|
||||
|
||||
await networkService.handleNotification(monitor, true);
|
||||
|
||||
expect(emailServiceMock.buildAndSendEmail.calledOnce).to.be.true;
|
||||
expect(
|
||||
emailServiceMock.buildAndSendEmail.calledWith(
|
||||
"serverIsUpTemplate",
|
||||
{ monitorName: monitor.name, monitorUrl: monitor.url },
|
||||
"test@example.com",
|
||||
"Monitor Test Monitor is up"
|
||||
)
|
||||
).to.be.true;
|
||||
});
|
||||
|
||||
it("should log an error if an exception is thrown", async function () {
|
||||
const monitor = { _id: "monitor1", name: "Test Monitor", url: "http://test.com" };
|
||||
dbMock.getNotificationsByMonitorId.rejects(new Error("Database error"));
|
||||
|
||||
await networkService.handleNotification(monitor, true);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(
|
||||
loggerMock.error.calledWith("Database error", {
|
||||
method: "handleNotification",
|
||||
service: networkService.SERVICE_NAME,
|
||||
monitorId: monitor._id,
|
||||
})
|
||||
).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NetworkService - handleStatusUpdate", function () {
|
||||
let dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock,
|
||||
networkService,
|
||||
monitorMock;
|
||||
|
||||
beforeEach(function () {
|
||||
dbMock = { getMonitorById: sinon.stub() };
|
||||
emailServiceMock = sinon.stub();
|
||||
axiosMock = sinon.stub();
|
||||
pingMock = sinon.stub();
|
||||
loggerMock = { error: sinon.stub() };
|
||||
httpMock = sinon.stub();
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
monitorMock = {
|
||||
status: sinon.stub(),
|
||||
save: sinon.stub(),
|
||||
};
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return if getMonitorById throws an error", async function () {
|
||||
dbMock.getMonitorById.throws(new Error("Database error"));
|
||||
|
||||
const res = await networkService.handleStatusUpdate(
|
||||
{ data: { _id: "monitor1" } },
|
||||
true
|
||||
);
|
||||
expect(res).to.be.undefined;
|
||||
});
|
||||
|
||||
it("should log an error if monitor is null", async function () {
|
||||
dbMock.getMonitorById.resolves(null);
|
||||
|
||||
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith("Null Monitor: monitor1")).to.be.true;
|
||||
});
|
||||
|
||||
it("should update monitor status if different", async function () {
|
||||
monitorMock.status = false;
|
||||
dbMock.getMonitorById.resolves(monitorMock);
|
||||
|
||||
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
|
||||
|
||||
expect(monitorMock.save.calledOnce).to.be.true;
|
||||
expect(monitorMock.status).to.be.true;
|
||||
});
|
||||
|
||||
it("should not update monitor status if same", async function () {
|
||||
monitorMock.status = true;
|
||||
dbMock.getMonitorById.resolves(monitorMock);
|
||||
|
||||
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
|
||||
|
||||
expect(monitorMock.save.called).to.be.false;
|
||||
});
|
||||
|
||||
it("should log an error if exception is thrown during status update", async function () {
|
||||
monitorMock.status = false;
|
||||
dbMock.getMonitorById.resolves(monitorMock);
|
||||
monitorMock.save.rejects(new Error("Save error"));
|
||||
|
||||
await networkService.handleStatusUpdate({ data: { _id: "monitor1" } }, true);
|
||||
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith("Save error")).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NetworkService - measureResponseTime", () => {
|
||||
let mockOperation,
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock,
|
||||
networkService;
|
||||
|
||||
beforeEach(() => {
|
||||
mockOperation = sinon.stub();
|
||||
dbMock = { getMonitorById: sinon.stub() };
|
||||
emailServiceMock = sinon.stub();
|
||||
axiosMock = sinon.stub();
|
||||
pingMock = sinon.stub();
|
||||
loggerMock = { error: sinon.stub() };
|
||||
httpMock = sinon.stub();
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return response time and response on successful operation", async () => {
|
||||
const fakeResponse = { data: "test" };
|
||||
mockOperation.resolves(fakeResponse);
|
||||
|
||||
const result = await networkService.measureResponseTime(mockOperation);
|
||||
expect(result).to.have.property("responseTime").that.is.a("number");
|
||||
expect(result).to.have.property("response").that.deep.equals(fakeResponse);
|
||||
});
|
||||
|
||||
it("should throw an error with response time and additional properties on operation failure", async () => {
|
||||
const fakeError = new Error("Operation failed");
|
||||
mockOperation.rejects(fakeError);
|
||||
|
||||
try {
|
||||
await networkService.measureResponseTime(mockOperation);
|
||||
} catch (error) {
|
||||
expect(error).to.have.property("responseTime").that.is.a("number");
|
||||
expect(error).to.have.property("service", networkService.SERVICE_NAME);
|
||||
expect(error).to.have.property("method", "measureResponseTime");
|
||||
expect(error.message).to.equal("Operation failed");
|
||||
}
|
||||
});
|
||||
it("should throw an error with response time set properties on operation failure", async () => {
|
||||
const fakeError = new Error("Operation failed");
|
||||
fakeError.service = "test";
|
||||
fakeError.method = "testMethod";
|
||||
mockOperation.rejects(fakeError);
|
||||
|
||||
try {
|
||||
await networkService.measureResponseTime(mockOperation);
|
||||
} catch (error) {
|
||||
expect(error).to.have.property("responseTime").that.is.a("number");
|
||||
expect(error).to.have.property("service", "test");
|
||||
expect(error).to.have.property("method", "testMethod");
|
||||
expect(error.message).to.equal("Operation failed");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("networkService - handlePing", () => {
|
||||
let job,
|
||||
dbMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock,
|
||||
networkService,
|
||||
logAndStoreCheckStub,
|
||||
handleStatusUpdateStub;
|
||||
beforeEach(function () {
|
||||
job = {
|
||||
data: {
|
||||
url: "http://example.com",
|
||||
_id: "12345",
|
||||
},
|
||||
};
|
||||
|
||||
dbMock = { getMonitorById: sinon.stub() };
|
||||
pingMock = { promise: { probe: sinon.stub() } };
|
||||
loggerMock = { error: sinon.stub() };
|
||||
httpMock = sinon.stub();
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
null,
|
||||
null,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck");
|
||||
handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate");
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle a successful ping response", async function () {
|
||||
const response = { alive: true };
|
||||
const responseTime = 0;
|
||||
pingMock.promise.probe.resolves(response);
|
||||
logAndStoreCheckStub.resolves();
|
||||
await networkService.handlePing(job);
|
||||
expect(
|
||||
logAndStoreCheckStub.calledOnceWith(
|
||||
{
|
||||
monitorId: job.data._id,
|
||||
status: true,
|
||||
responseTime,
|
||||
message: successMessages.PING_SUCCESS,
|
||||
},
|
||||
networkService.db.createCheck
|
||||
)
|
||||
).to.be.true;
|
||||
expect(handleStatusUpdateStub.calledOnceWith(job, true)).to.be.true;
|
||||
});
|
||||
it("should handle a successful ping response and isAlive === false", async function () {
|
||||
const response = { alive: false };
|
||||
const responseTime = 0;
|
||||
pingMock.promise.probe.resolves(response);
|
||||
logAndStoreCheckStub.resolves();
|
||||
|
||||
await networkService.handlePing(job);
|
||||
console.log(logAndStoreCheckStub.getCall(0).args[0]);
|
||||
expect(
|
||||
logAndStoreCheckStub.calledOnceWith(
|
||||
{
|
||||
monitorId: job.data._id,
|
||||
status: false,
|
||||
responseTime,
|
||||
message: errorMessages.PING_CANNOT_RESOLVE,
|
||||
},
|
||||
networkService.db.createCheck
|
||||
)
|
||||
).to.be.true;
|
||||
expect(handleStatusUpdateStub.calledOnceWith(job, false)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle a failed ping response", async function () {
|
||||
const error = new Error("Ping failed");
|
||||
error.responseTime = 0;
|
||||
|
||||
pingMock.promise.probe.rejects(error);
|
||||
|
||||
await networkService.handlePing(job);
|
||||
|
||||
expect(handleStatusUpdateStub.calledOnceWith(job, false)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NetworkService - handleHttp", () => {
|
||||
let networkService,
|
||||
axiosMock,
|
||||
dbMock,
|
||||
httpMock,
|
||||
jobMock,
|
||||
loggerMock,
|
||||
logAndStoreCheckStub,
|
||||
handleStatusUpdateStub;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock = {
|
||||
get: sinon.stub(),
|
||||
};
|
||||
dbMock = {
|
||||
createCheck: sinon.stub().resolves(),
|
||||
};
|
||||
httpMock = {
|
||||
STATUS_CODES: {
|
||||
200: "OK",
|
||||
500: "Internal Server Error",
|
||||
},
|
||||
};
|
||||
loggerMock = {
|
||||
error: sinon.stub(),
|
||||
};
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
null,
|
||||
axiosMock,
|
||||
null,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
jobMock = {
|
||||
data: {
|
||||
url: "http://example.com",
|
||||
_id: "monitorId123",
|
||||
},
|
||||
};
|
||||
sinon.stub(networkService, "measureResponseTime").callsFake(async (operation) => {
|
||||
const response = await operation();
|
||||
return { responseTime: 100, response };
|
||||
});
|
||||
logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck").resolves();
|
||||
handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate").resolves();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle a successful HTTP response", async () => {
|
||||
const responseMock = { status: 200 };
|
||||
axiosMock.get.resolves(responseMock);
|
||||
|
||||
await networkService.handleHttp(jobMock);
|
||||
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: true,
|
||||
statusCode: 200,
|
||||
message: "OK",
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle an HTTP error response", async () => {
|
||||
const errorMock = { response: { status: 500 }, responseTime: 200 };
|
||||
axiosMock.get.rejects(errorMock);
|
||||
|
||||
await networkService.handleHttp(jobMock);
|
||||
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: false,
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true;
|
||||
});
|
||||
it("should handle an HTTP error response with undefined status", async () => {
|
||||
const errorMock = { response: { status: undefined }, responseTime: 200 };
|
||||
axiosMock.get.rejects(errorMock);
|
||||
|
||||
await networkService.handleHttp(jobMock);
|
||||
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: false,
|
||||
statusCode: 5000,
|
||||
message: "Network Error",
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("networkService - handlePagespeed", () => {
|
||||
let dbMock,
|
||||
axiosMock,
|
||||
jobMock,
|
||||
emailServiceMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock,
|
||||
networkService,
|
||||
logAndStoreCheckStub,
|
||||
handleStatusUpdateStub;
|
||||
beforeEach(() => {
|
||||
jobMock = {
|
||||
data: {
|
||||
_id: "12345",
|
||||
url: "http://example.com",
|
||||
},
|
||||
};
|
||||
dbMock = { getMonitorById: sinon.stub() };
|
||||
axiosMock = { get: sinon.stub() };
|
||||
|
||||
emailServiceMock = sinon.stub();
|
||||
pingMock = { promise: { probe: sinon.stub() } };
|
||||
loggerMock = { error: sinon.stub() };
|
||||
httpMock = {
|
||||
STATUS_CODES: {
|
||||
200: "OK",
|
||||
500: "Internal Server Error",
|
||||
},
|
||||
};
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
logAndStoreCheckStub = sinon.stub(networkService, "logAndStoreCheck").resolves();
|
||||
handleStatusUpdateStub = sinon.stub(networkService, "handleStatusUpdate").resolves();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should handle a successful PageSpeed response", async () => {
|
||||
const responseMock = {
|
||||
status: 200,
|
||||
data: {
|
||||
lighthouseResult: {
|
||||
categories: {
|
||||
accessibility: { score: 0.9 },
|
||||
"best-practices": { score: 0.8 },
|
||||
seo: { score: 0.7 },
|
||||
performance: { score: 0.6 },
|
||||
},
|
||||
audits: {
|
||||
"cumulative-layout-shift": { score: 0.1 },
|
||||
"speed-index": { score: 0.2 },
|
||||
"first-contentful-paint": { score: 0.3 },
|
||||
"largest-contentful-paint": { score: 0.4 },
|
||||
"total-blocking-time": { score: 0.5 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
axiosMock.get.resolves(responseMock);
|
||||
|
||||
await networkService.handlePagespeed(jobMock);
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: true,
|
||||
statusCode: 200,
|
||||
message: "OK",
|
||||
accessibility: 90,
|
||||
bestPractices: 80,
|
||||
seo: 70,
|
||||
performance: 60,
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle a successful PageSpeed response with missing data", async () => {
|
||||
const responseMock = {
|
||||
status: 200,
|
||||
data: {
|
||||
lighthouseResult: {
|
||||
categories: {},
|
||||
audits: {
|
||||
"cumulative-layout-shift": { score: 0.1 },
|
||||
"speed-index": { score: 0.2 },
|
||||
"first-contentful-paint": { score: 0.3 },
|
||||
"largest-contentful-paint": { score: 0.4 },
|
||||
"total-blocking-time": { score: 0.5 },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
axiosMock.get.resolves(responseMock);
|
||||
|
||||
await networkService.handlePagespeed(jobMock);
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: true,
|
||||
statusCode: 200,
|
||||
message: "OK",
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performance: 0,
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, true)).to.be.true;
|
||||
});
|
||||
|
||||
it("should handle an error PageSpeed response", async () => {
|
||||
const errorMock = { response: { status: 500 } };
|
||||
axiosMock.get.rejects(errorMock);
|
||||
|
||||
await networkService.handlePagespeed(jobMock);
|
||||
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: false,
|
||||
statusCode: 500,
|
||||
message: "Internal Server Error",
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performance: 0,
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true;
|
||||
});
|
||||
it("should handle an error PageSpeed response with unknown status", async () => {
|
||||
const errorMock = { response: { status: undefined } };
|
||||
axiosMock.get.rejects(errorMock);
|
||||
|
||||
await networkService.handlePagespeed(jobMock);
|
||||
|
||||
expect(networkService.logAndStoreCheck.calledOnce).to.be.true;
|
||||
const checkData = networkService.logAndStoreCheck.getCall(0).args[0];
|
||||
expect(checkData).to.include({
|
||||
monitorId: jobMock.data._id,
|
||||
status: false,
|
||||
statusCode: 5000,
|
||||
message: "Network Error",
|
||||
accessibility: 0,
|
||||
bestPractices: 0,
|
||||
seo: 0,
|
||||
performance: 0,
|
||||
});
|
||||
expect(networkService.handleStatusUpdate.calledOnceWith(jobMock, false)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NetworkService - getStatus", () => {
|
||||
let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock, networkService;
|
||||
|
||||
beforeEach(() => {
|
||||
dbMock = { getMonitorById: sinon.stub() };
|
||||
emailServiceMock = sinon.stub();
|
||||
axiosMock = sinon.stub();
|
||||
pingMock = sinon.stub();
|
||||
loggerMock = { error: sinon.stub() };
|
||||
httpMock = sinon.stub();
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should return true if the job type is ping and handlePing is successful", async () => {
|
||||
const job = { data: { type: networkService.TYPE_PING } };
|
||||
sinon.stub(networkService, "handlePing").resolves(true);
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
it("should return false if the job type is ping and handlePing is not successful", async () => {
|
||||
const job = { data: { type: networkService.TYPE_PING } };
|
||||
sinon.stub(networkService, "handlePing").resolves(false);
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
it("should return true if the job type is http and handleHttp is successful", async () => {
|
||||
const job = { data: { type: networkService.TYPE_HTTP } };
|
||||
sinon.stub(networkService, "handleHttp").resolves(true);
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
it("should return false if the job type is http and handleHttp is not successful", async () => {
|
||||
const job = { data: { type: networkService.TYPE_HTTP } };
|
||||
sinon.stub(networkService, "handleHttp").resolves(false);
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
it("should return true if the job type is pagespeed and handlePagespeed is successful", async () => {
|
||||
const job = { data: { type: networkService.TYPE_PAGESPEED } };
|
||||
sinon.stub(networkService, "handlePagespeed").resolves(true);
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
it("should return false if the job type is pagespeed and handlePagespeed is not successful", async () => {
|
||||
const job = { data: { type: networkService.TYPE_PAGESPEED } };
|
||||
sinon.stub(networkService, "handlePagespeed").resolves(false);
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.false;
|
||||
});
|
||||
it("should log an error and return false if the job type is unknown", async () => {
|
||||
const job = { data: { type: "unknown" } };
|
||||
const result = await networkService.getStatus(job);
|
||||
expect(result).to.be.false;
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Unsupported type: unknown`)).to.be.true;
|
||||
});
|
||||
});
|
||||
|
||||
describe("NetworkService - logAndStoreCheck", async () => {
|
||||
let dbMock, emailServiceMock, axiosMock, pingMock, loggerMock, httpMock, networkService;
|
||||
|
||||
beforeEach(() => {
|
||||
dbMock = { getMonitorById: sinon.stub() };
|
||||
emailServiceMock = sinon.stub();
|
||||
axiosMock = sinon.stub();
|
||||
pingMock = sinon.stub();
|
||||
loggerMock = { error: sinon.stub() };
|
||||
httpMock = sinon.stub();
|
||||
networkService = new NetworkService(
|
||||
dbMock,
|
||||
emailServiceMock,
|
||||
axiosMock,
|
||||
pingMock,
|
||||
loggerMock,
|
||||
httpMock
|
||||
);
|
||||
});
|
||||
const data = { monitorId: "12345" };
|
||||
const writeToDB = sinon.stub();
|
||||
afterEach(() => {
|
||||
sinon.restore();
|
||||
});
|
||||
|
||||
it("should log an error if `writeToDb` throws an error", async () => {
|
||||
writeToDB.rejects(new Error("Database error"));
|
||||
await networkService.logAndStoreCheck(data, writeToDB);
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
|
||||
.true;
|
||||
});
|
||||
|
||||
it("should return the status of the inserted check if successful", async () => {
|
||||
writeToDB.resolves({ status: true });
|
||||
const result = await networkService.logAndStoreCheck(data, writeToDB);
|
||||
expect(result).to.be.true;
|
||||
});
|
||||
|
||||
it("should thrown an error if the check is not inserted (null)", async () => {
|
||||
writeToDB.resolves(null);
|
||||
await networkService.logAndStoreCheck(data, writeToDB);
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
|
||||
.true;
|
||||
});
|
||||
it("should thrown an error if the check is not inserted (undefined)", async () => {
|
||||
writeToDB.resolves(undefined);
|
||||
await networkService.logAndStoreCheck(data, writeToDB);
|
||||
expect(loggerMock.error.calledOnce).to.be.true;
|
||||
expect(loggerMock.error.calledWith(`Error writing check for ${data.monitorId}`)).to.be
|
||||
.true;
|
||||
});
|
||||
});
|
||||
@@ -62,7 +62,6 @@ describe("calculatePercentile", () => {
|
||||
const percentile = 100;
|
||||
const result = calculatePercentile(checks, percentile);
|
||||
const expected = 50;
|
||||
console.log(result);
|
||||
expect(result).to.equal(expected);
|
||||
});
|
||||
});
|
||||
@@ -236,6 +236,10 @@ const pauseMonitorParamValidation = joi.object({
|
||||
monitorId: joi.string().required(),
|
||||
});
|
||||
|
||||
const getMonitorURLByQueryValidation = joi.object({
|
||||
monitorURL: joi.string().uri().required(),
|
||||
});
|
||||
|
||||
//****************************************
|
||||
// Alerts
|
||||
//****************************************
|
||||
@@ -446,6 +450,7 @@ export {
|
||||
getCertificateParamValidation,
|
||||
editMonitorBodyValidation,
|
||||
pauseMonitorParamValidation,
|
||||
getMonitorURLByQueryValidation,
|
||||
editUserParamValidation,
|
||||
editUserBodyValidation,
|
||||
createAlertParamValidation,
|
||||
|
||||
Reference in new issue
Block a user