Merge branch 'develop' into 901-create-confirmation-dialogs-for-clear-all-stats-and-remove-all-monitors

This commit is contained in:
Shemy Gan
2024-10-15 09:01:56 -04:00
29 changed files with 2930 additions and 207 deletions

View File

@@ -74,6 +74,7 @@ const Field = forwardRef(
"&:has(.input-error) .MuiOutlinedInput-root fieldset": {
borderColor: theme.palette.error.text,
},
display: hidden ? "none" : "",
}}
>
{label && (
@@ -142,7 +143,7 @@ const Field = forwardRef(
},
}
: {
display: hidden ? "none" : "",
}
}
InputProps={{

View File

@@ -41,6 +41,9 @@ import ArrowUp from "../../assets/icons/up-arrow.svg?react";
import ArrowRight from "../../assets/icons/right-arrow.svg?react";
import ArrowLeft from "../../assets/icons/left-arrow.svg?react";
import DotsVertical from "../../assets/icons/dots-vertical.svg?react";
import ChangeLog from "../../assets/icons/changeLog.svg?react";
import Docs from "../../assets/icons/docs.svg?react";
import Folder from "../../assets/icons/folder.svg?react";
import "./index.css";
@@ -66,12 +69,30 @@ const menu = [
{ name: "Team", path: "account/team", icon: <TeamSvg /> },
],
},
{
name: "Other",
icon: <Folder />,
nested: [
{ name: "Settings", path: "settings", icon: <Settings /> },
{ name: "Support", path: "support", icon: <Support /> },
{ name: "Docs", path: "docs", icon: <Docs /> },
{ name: "Changelog", path: "changelog", icon: <ChangeLog /> },
],
},
];
const other = [
{ name: "Support", path: "support", icon: <Support /> },
{ name: "Settings", path: "settings", icon: <Settings /> },
];
const URL_MAP = {
support: "https://github.com/bluewave-labs/bluewave-uptime/issues",
docs: "https://bluewavelabs.gitbook.io/uptime-manager",
changelog: "https://github.com/bluewave-labs/bluewave-uptime/releases",
};
const PATH_MAP = {
monitors: "Dashboard",
pagespeed: "Dashboard",
account: "Account",
settings: "Other",
};
/**
* @component
@@ -87,7 +108,7 @@ function Sidebar() {
const dispatch = useDispatch();
const authState = useSelector((state) => state.auth);
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
const [open, setOpen] = useState({ Dashboard: false, Account: false });
const [open, setOpen] = useState({ Dashboard: false, Account: false, Other: false });
const [anchorEl, setAnchorEl] = useState(null);
const [popup, setPopup] = useState();
const { user } = useSelector((state) => state.auth);
@@ -120,13 +141,13 @@ function Sidebar() {
};
useEffect(() => {
if (
location.pathname.includes("monitors") ||
location.pathname.includes("pagespeed")
)
setOpen((prev) => ({ ...prev, Dashboard: true }));
else if (location.pathname.includes("/account"))
setOpen((prev) => ({ ...prev, Account: true }));
const matchedKey = Object.keys(PATH_MAP).find((key) =>
location.pathname.includes(key)
);
if (matchedKey) {
setOpen((prev) => ({ ...prev, [PATH_MAP[matchedKey]]: true }));
}
}, []);
return (
@@ -202,7 +223,9 @@ function Sidebar() {
},
}}
onClick={() => {
setOpen({ Dashboard: false, Account: false });
setOpen(prev =>
Object.fromEntries(Object.keys(prev).map(key => [key, false]))
)
dispatch(toggleSidebar());
}}
>
@@ -346,7 +369,12 @@ function Sidebar() {
}
key={child.path}
onClick={() => {
navigate(`/${child.path}`);
const url = URL_MAP[child.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${child.path}`);
}
closePopup();
}}
sx={{
@@ -372,8 +400,8 @@ function Sidebar() {
<ListItemButton
onClick={() =>
setOpen((prev) => ({
...prev,
[`${item.name}`]: !prev[`${item.name}`],
...Object.fromEntries(Object.keys(prev).map(key => [key, false])),
[item.name]: !prev[item.name]
}))
}
sx={{
@@ -409,7 +437,14 @@ function Sidebar() {
: ""
}
key={child.path}
onClick={() => navigate(`/${child.path}`)}
onClick={() => {
const url = URL_MAP[child.path];
if (url) {
window.open(url, "_blank", "noreferrer");
} else {
navigate(`/${child.path}`);
}
}}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
@@ -455,70 +490,6 @@ function Sidebar() {
)
)}
</List>
<Divider sx={{ my: theme.spacing(4) }} />
{/* other */}
<List
component="nav"
aria-labelledby="nested-other-subheader"
subheader={
<ListSubheader
component="div"
id="nested-other-subheader"
sx={{
pt: theme.spacing(4),
px: collapsed ? 0 : theme.spacing(4),
backgroundColor: "transparent",
}}
>
Other
</ListSubheader>
}
sx={{ px: theme.spacing(6) }}
>
{other.map((item) => (
<Tooltip
key={item.path}
placement="right"
title={collapsed ? item.name : ""}
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: {
offset: [0, -16],
},
},
],
},
}}
disableInteractive
>
<ListItemButton
className={
location.pathname.includes(item.path) ? "selected-path" : ""
}
onClick={() =>
item.path === "support"
? window.open(
"https://github.com/bluewave-labs/bluewave-uptime/issues",
"_blank",
"noreferrer"
)
: navigate(`/${item.path}`)
}
sx={{
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
px: theme.spacing(4),
}}
>
<ListItemIcon sx={{ minWidth: 0 }}>{item.icon}</ListItemIcon>
<ListItemText>{item.name}</ListItemText>
</ListItemButton>
</Tooltip>
))}
</List>
<Divider sx={{ mt: "auto" }} />
<Stack
@@ -632,7 +603,7 @@ function Sidebar() {
</MenuItem>
)}
{collapsed && <Divider />}
<MenuItem
{/* <MenuItem
onClick={() => {
dispatch(setMode("light"));
closePopup();
@@ -647,7 +618,7 @@ function Sidebar() {
}}
>
Dark
</MenuItem>
</MenuItem> */}
<Divider />
<MenuItem
onClick={logout}

View File

@@ -469,14 +469,6 @@ const ProfilePanel = () => {
gap={theme.spacing(5)}
justifyContent="flex-end"
>
<Button
variant="outlined"
color="secondary"
disabled
sx={{ mr: "auto" }}
>
Edit
</Button>
<Button variant="text" color="info" onClick={removePicture}>
Remove
</Button>

View File

@@ -49,6 +49,7 @@ const TeamPanel = () => {
const [tableData, setTableData] = useState({});
const [members, setMembers] = useState([]);
const [filter, setFilter] = useState("all");
const [isDisabled, setIsDisabled] = useState(true);
const [errors, setErrors] = useState({});
const [isSendingInvite, setIsSendingInvite] = useState(false);
@@ -137,6 +138,9 @@ const TeamPanel = () => {
setTableData(data);
}, [members, filter]);
useEffect(() => {
setIsDisabled(Object.keys(errors).length !== 0 || toInvite.email === "");
}, [errors, toInvite.email]);
// RENAME ORGANIZATION
const toggleEdit = () => {
@@ -172,6 +176,10 @@ const TeamPanel = () => {
};
const handleInviteMember = async () => {
if (!toInvite.email) {
setErrors((prev) => ({ ...prev, email: "Email is required." }));
return;
}
setIsSendingInvite(true);
if (!toInvite.role.includes("user") || !toInvite.role.includes("admin"))
setToInvite((prev) => ({ ...prev, role: ["user"] }));
@@ -415,7 +423,7 @@ const TeamPanel = () => {
color="primary"
onClick={handleInviteMember}
loading={isSendingInvite}
disabled={Object.keys(errors).length !== 0}
disabled={isDisabled}
>
Send invite
</LoadingButton>

View File

@@ -3,6 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils";
import { logger } from "../../../Utils/Logger";
import {
Button,
IconButton,
@@ -14,6 +15,7 @@ import {
} from "@mui/material";
import {
deleteUptimeMonitor,
pauseUptimeMonitor,
getUptimeMonitorsByTeamId,
} from "../../../Features/UptimeMonitors/uptimeMonitorsSlice";
import Settings from "../../../assets/icons/settings-bold.svg?react";
@@ -26,6 +28,8 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
const dispatch = useDispatch();
const theme = useTheme();
const authState = useSelector((state) => state.auth);
const authToken = authState.authToken;
const handleRemove = async (event) => {
event.preventDefault();
@@ -44,6 +48,24 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
}
};
const handlePause = async () => {
try {
const action = await dispatch(
pauseUptimeMonitor({ authToken, monitorId: monitor._id })
);
if (pauseUptimeMonitor.fulfilled.match(action)) {
updateCallback();
const state = action?.payload?.data.isActive === false ? "paused" : "resumed";
createToast({ body: `Monitor ${state} successfully.` });
} else {
throw new Error(action?.error?.message ?? "Failed to pause monitor.");
}
} catch (error) {
logger.error("Error pausing monitor:", monitor._id, error);
createToast({ body: "Failed to pause monitor." });
}
};
const openMenu = (event, id, url) => {
event.preventDefault();
event.stopPropagation();
@@ -155,6 +177,17 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
Clone
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
e.stopPropagation();
handlePause(e);
}}
>
{monitor?.isActive === true ? "Pause" : "Resume"}
</MenuItem>
)}
{isAdmin && (
<MenuItem
onClick={(e) => {
@@ -164,7 +197,7 @@ const ActionsMenu = ({ monitor, isAdmin, updateCallback }) => {
>
Remove
</MenuItem>
)}
)}
</Menu>
<Modal
aria-labelledby="modal-delete-monitor"
@@ -238,6 +271,7 @@ ActionsMenu.propTypes = {
_id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
isActive: PropTypes.bool,
}).isRequired,
isAdmin: PropTypes.bool,
updateCallback: PropTypes.func,

View File

@@ -15,7 +15,7 @@ import {
import { update } from "../../Features/Auth/authSlice";
import PropTypes from "prop-types";
import LoadingButton from "@mui/lab/LoadingButton";
import { setTimezone } from "../../Features/UI/uiSlice";
import { setTimezone, setMode } from "../../Features/UI/uiSlice";
import timezones from "../../Utils/timezones.json";
import { useState } from "react";
import { ConfigBox } from "./styled";
@@ -33,6 +33,7 @@ const Settings = ({ isAdmin }) => {
const { isLoading } = useSelector((state) => state.uptimeMonitors);
const { isLoading: authIsLoading } = useSelector((state) => state.auth);
const { timezone } = useSelector((state) => state.ui);
const {mode} = useSelector((state) => state.ui);
const [checksIsLoading, setChecksIsLoading] = useState(false);
const [form, setForm] = useState({
ttl: checkTTL ? (checkTTL / SECONDS_PER_DAY).toString() : 0,
@@ -188,6 +189,26 @@ const Settings = ({ isAdmin }) => {
/>
</Stack>
</ConfigBox>
<ConfigBox>
<Box>
<Typography component="h1">Appearance</Typography>
<Typography sx={{ mt: theme.spacing(2), mb: theme.spacing(2) }}>
Switch between light and dark mode.
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<Select
id="theme-mode"
label="Theme Mode"
value={mode}
onChange={(e) => {
dispatch(setMode(e.target.value));
}}
items={[{_id:'light', name:'Light'},{_id:'dark', name:'Dark'}]}
>
</Select>
</Stack>
</ConfigBox>
{isAdmin && (
<ConfigBox>
<Box>

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M17 17L22 12L17 7M7 7L2 12L7 17M14 3L10 21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 237 B

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 11H8M10 15H8M16 7H8M20 6.8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V6.8C4 5.11984 4 4.27976 4.32698 3.63803C4.6146 3.07354 5.07354 2.6146 5.63803 2.32698C6.27976 2 7.11984 2 8.8 2H15.2C16.8802 2 17.7202 2 18.362 2.32698C18.9265 2.6146 19.3854 3.07354 19.673 3.63803C20 4.27976 20 5.11984 20 6.8Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 684 B

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M3 8.2C3 7.07989 3 6.51984 3.21799 6.09202C3.40973 5.71569 3.71569 5.40973 4.09202 5.21799C4.51984 5 5.0799 5 6.2 5H9.67452C10.1637 5 10.4083 5 10.6385 5.05526C10.8425 5.10425 11.0376 5.18506 11.2166 5.29472C11.4184 5.4184 11.5914 5.59135 11.9373 5.93726L12.0627 6.06274C12.4086 6.40865 12.5816 6.5816 12.7834 6.70528C12.9624 6.81494 13.1575 6.89575 13.3615 6.94474C13.5917 7 13.8363 7 14.3255 7H17.8C18.9201 7 19.4802 7 19.908 7.21799C20.2843 7.40973 20.5903 7.71569 20.782 8.09202C21 8.51984 21 9.0799 21 10.2V15.8C21 16.9201 21 17.4802 20.782 17.908C20.5903 18.2843 20.2843 18.5903 19.908 18.782C19.4802 19 18.9201 19 17.8 19H6.2C5.07989 19 4.51984 19 4.09202 18.782C3.71569 18.5903 3.40973 18.2843 3.21799 17.908C3 17.4802 3 16.9201 3 15.8V8.2Z" stroke="#000000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,6 +1,6 @@
{
"all": true,
"include": ["**/*.js"],
"include": ["controllers/*.js"],
"exclude": ["**/*.test.js"],
"reporter": ["html", "text", "lcov"],
"sourceMap": false,

View File

@@ -68,6 +68,7 @@ const registerUser = async (req, res, next) => {
}
const newUser = await req.db.insertUser({ ...req.body }, req.file);
logger.info(successMessages.AUTH_CREATE_USER, {
service: SERVICE_NAME,
userId: newUser._id,
@@ -78,7 +79,9 @@ const registerUser = async (req, res, next) => {
delete userForToken.avatarImage;
const appSettings = await req.settingsService.getSettings();
const token = issueToken(userForToken, appSettings);
req.emailService
.buildAndSendEmail(
"welcomeEmailTemplate",
@@ -99,6 +102,7 @@ const registerUser = async (req, res, next) => {
data: { user: newUser, token: token },
});
} catch (error) {
console.log("ERROR", error);
next(handleError(error, SERVICE_NAME, "registerController"));
}
};
@@ -293,6 +297,8 @@ const requestRecovery = async (req, res, next) => {
msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN,
data: msgId,
});
} else {
throw new Error(errorMessages.FRIENDLY_ERROR);
}
} catch (error) {
next(handleError(error, SERVICE_NAME, "recoveryRequestController"));
@@ -435,6 +441,7 @@ const getAllUsers = async (req, res) => {
};
module.exports = {
issueToken,
registerUser,
loginUser,
editUser,

View File

@@ -6,8 +6,8 @@ const handleValidationError = (error, serviceName) => {
return error;
};
const handleError = (error, serviceName, method, code = 500) => {
error.code === undefined ? (error.code = code) : null;
const handleError = (error, serviceName, method, status = 500) => {
error.status === undefined ? (error.status = status) : null;
error.service === undefined ? (error.service = serviceName) : null;
error.method === undefined ? (error.method = method) : null;
return error;

View File

@@ -8,17 +8,7 @@ require("dotenv").config();
const jwt = require("jsonwebtoken");
const { handleError, handleValidationError } = require("./controllerUtils");
const SERVICE_NAME = "inviteController";
const getTokenFromHeaders = (headers) => {
const authorizationHeader = headers.authorization;
if (!authorizationHeader) throw new Error("No auth headers");
const parts = authorizationHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer")
throw new Error("Invalid auth headers");
return parts[1];
};
const { getTokenFromHeaders } = require("../utils/utils");
/**
* Issues an invitation to a new user. Only admins can invite new users. An invitation token is created and sent via email.
@@ -93,6 +83,6 @@ const inviteVerifyController = async (req, res, next) => {
};
module.exports = {
inviteController: issueInvitation,
issueInvitation,
inviteVerifyController,
};

View File

@@ -84,7 +84,7 @@ const getMaintenanceWindowsByTeamId = async (req, res, next) => {
req.query
);
return res.status(201).json({
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
data: maintenanceWindows,
@@ -109,7 +109,7 @@ const getMaintenanceWindowsByMonitorId = async (req, res, next) => {
req.params.monitorId
);
return res.status(201).json({
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_USER,
data: maintenanceWindows,
@@ -128,7 +128,7 @@ const deleteMaintenanceWindow = async (req, res, next) => {
}
try {
await req.db.deleteMaintenanceWindowById(req.params.id);
return res.status(201).json({
return res.status(200).json({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
});

View File

@@ -33,7 +33,7 @@ const { handleError, handleValidationError } = require("./controllerUtils");
const getAllMonitors = async (req, res, next) => {
try {
const monitors = await req.db.getAllMonitors();
return res.json({
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_ALL,
data: monitors,
@@ -63,7 +63,7 @@ const getMonitorStatsById = async (req, res, next) => {
try {
const monitorStats = await req.db.getMonitorStatsById(req);
return res.json({
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_STATS_BY_ID,
data: monitorStats,
@@ -86,7 +86,7 @@ const getMonitorCertificate = async (req, res, next) => {
const monitorUrl = new URL(monitor.url);
const certificate = await sslChecker(monitorUrl.hostname);
if (certificate && certificate.validTo) {
return res.json({
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_CERTIFICATE,
data: {
@@ -94,7 +94,7 @@ const getMonitorCertificate = async (req, res, next) => {
},
});
} else {
return res.json({
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_CERTIFICATE,
data: { certificateDate: "N/A" },
@@ -130,9 +130,10 @@ const getMonitorById = async (req, res, next) => {
if (!monitor) {
const error = new Error(errorMessages.MONITOR_GET_BY_ID);
error.status = 404;
throw error;
}
return res.json({
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_ID,
data: monitor,
@@ -207,7 +208,7 @@ const getMonitorsByTeamId = async (req, res, next) => {
try {
const teamId = req.params.teamId;
const monitors = await req.db.getMonitorsByTeamId(req, res);
return res.json({
return res.status(200).json({
success: true,
msg: successMessages.MONITOR_GET_BY_USER_ID(teamId),
data: monitors,
@@ -324,14 +325,23 @@ const deleteAllMonitors = async (req, res, next) => {
const { jwtSecret } = req.settingsService.getSettings();
const { teamId } = jwt.verify(token, jwtSecret);
const { monitors, deletedCount } = await req.db.deleteAllMonitors(teamId);
await monitors.forEach(async (monitor) => {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deleteAlertByMonitorId(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
});
await Promise.all(
monitors.map(async (monitor) => {
try {
await req.jobQueue.deleteJob(monitor);
await req.db.deleteChecks(monitor._id);
await req.db.deletePageSpeedChecksByMonitorId(monitor._id);
await req.db.deleteNotificationsByMonitorId(monitor._id);
} catch (error) {
logger.error(
`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`,
{
method: "deleteAllMonitors",
}
);
}
})
);
return res
.status(200)
.json({ success: true, msg: `Deleted ${deletedCount} monitors` });
@@ -459,7 +469,7 @@ const addDemoMonitors = async (req, res, next) => {
return res.status(200).json({
success: true,
message: successMessages.MONITOR_DEMO_ADDED,
msg: successMessages.MONITOR_DEMO_ADDED,
data: demoMonitors.length,
});
} catch (error) {

View File

@@ -1,13 +1,15 @@
const { handleError } = require("./controllerUtils");
const { errorMessages, successMessages } = require("../utils/messages");
const SERVICE_NAME = "JobQueueController";
const getMetrics = async (req, res, next) => {
try {
const metrics = await req.jobQueue.getMetrics();
res
.status(200)
.json({ success: true, msg: "Metrics retrieved", data: metrics });
res.status(200).json({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data: metrics,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getMetrics"));
return;
@@ -17,7 +19,11 @@ const getMetrics = async (req, res, next) => {
const getJobs = async (req, res, next) => {
try {
const jobs = await req.jobQueue.getJobStats();
return res.status(200).json({ jobs });
return res.status(200).json({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data: jobs,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getJobs"));
return;
@@ -27,7 +33,10 @@ const getJobs = async (req, res, next) => {
const addJob = async (req, res, next) => {
try {
await req.jobQueue.addJob(Math.random().toString(36).substring(7));
return res.send("Added job");
return res.status(200).json({
success: true,
msg: successMessages.QUEUE_ADD_JOB,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "addJob"));
return;
@@ -37,7 +46,9 @@ const addJob = async (req, res, next) => {
const obliterateQueue = async (req, res, next) => {
try {
await req.jobQueue.obliterate();
return res.status(200).send("Obliterated queue");
return res
.status(200)
.json({ success: true, msg: successMessages.QUEUE_OBLITERATE });
} catch (error) {
next(handleError(error, SERVICE_NAME, "obliterateQueue"));
return;

View File

@@ -36,6 +36,7 @@
"devDependencies": {
"nodemon": "3.1.0",
"nyc": "17.1.0",
"proxyquire": "2.1.3",
"sinon": "19.0.2"
}
},
@@ -2802,6 +2803,20 @@
"resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz",
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="
},
"node_modules/fill-keys": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/fill-keys/-/fill-keys-1.0.2.tgz",
"integrity": "sha512-tcgI872xXjwFF4xgQmLxi76GnwJG3g/3isB1l4/G5Z4zrbddGpBjqZCO9oEAcB5wX0Hj/5iQB3toxfO7in1hHA==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-object": "~1.0.1",
"merge-descriptors": "~1.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
@@ -3535,6 +3550,22 @@
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.15.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
"integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -3575,6 +3606,16 @@
"node": ">=0.12.0"
}
},
"node_modules/is-object": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz",
"integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-plain-obj": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz",
@@ -4968,6 +5009,13 @@
"node": ">=10"
}
},
"node_modules/module-not-found-error": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz",
"integrity": "sha512-pEk4ECWQXV6z2zjhRZUongnLJNUeGQJ3w6OQ5ctGwD+i5o93qjRQUk2Rt6VdNeu3sEP0AB4LcfvdebpxBRVr4g==",
"dev": true,
"license": "MIT"
},
"node_modules/mongodb": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.6.2.tgz",
@@ -5833,6 +5881,13 @@
"node": ">=8"
}
},
"node_modules/path-parse": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true,
"license": "MIT"
},
"node_modules/path-scurry": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
@@ -6545,6 +6600,18 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
"node_modules/proxyquire": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz",
"integrity": "sha512-BQWfCqYM+QINd+yawJz23tbBM40VIGXOdDw3X344KcclI/gtBbdWF6SlQ4nK/bYhF9d27KYug9WzljHC6B9Ysg==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-keys": "^1.0.2",
"module-not-found-error": "^1.0.1",
"resolve": "^1.11.1"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
@@ -6683,6 +6750,24 @@
"dev": true,
"license": "ISC"
},
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
"integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-core-module": "^2.13.0",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
"bin": {
"resolve": "bin/resolve"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
@@ -7242,6 +7327,19 @@
"node": ">=4"
}
},
"node_modules/supports-preserve-symlinks-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/svgo": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz",

View File

@@ -38,6 +38,7 @@
"devDependencies": {
"nodemon": "3.1.0",
"nyc": "17.1.0",
"proxyquire": "2.1.3",
"sinon": "19.0.2"
}
}

View File

@@ -3,7 +3,7 @@ const { verifyJWT } = require("../middleware/verifyJWT");
const { isAllowed } = require("../middleware/isAllowed");
const {
inviteController,
issueInvitation,
inviteVerifyController,
} = require("../controllers/inviteController");
@@ -11,8 +11,8 @@ router.post(
"/",
isAllowed(["admin", "superadmin"]),
verifyJWT,
inviteController
issueInvitation
);
router.post("/verify", inviteVerifyController);
router.post("/verify", issueInvitation);
module.exports = router;

View File

@@ -1,4 +1,5 @@
const {
issueToken,
registerUser,
loginUser,
editUser,
@@ -12,11 +13,45 @@ const {
const jwt = require("jsonwebtoken");
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
const logger = require("../../utils/logger");
describe("Auth Controller - issueToken", () => {
it("should reject with an error if jwt.sign fails", () => {
const error = new Error("jwt.sign error");
stub = sinon.stub(jwt, "sign").throws(error);
const payload = { id: "123" };
const appSettings = { jwtSecret: "my_secret" };
expect(() => issueToken(payload, appSettings)).to.throw(error);
stub.restore();
});
it("should return a token if jwt.sign is successful and appSettings.jtwTTL is not defined", () => {
const payload = { id: "123" };
const appSettings = { jwtSecret: "my_secret" };
const token = issueToken(payload, appSettings);
expect(token).to.be.a("string");
});
it("should return a token if jwt.sign is successful and appSettings.jwtTTL is defined", () => {
const payload = { id: "123" };
const appSettings = { jwtSecret: "my_secret", jwtTTL: "1s" };
const token = issueToken(payload, appSettings);
expect(token).to.be.a("string");
});
});
describe("Auth Controller - registerUser", () => {
// Set up test
beforeEach(() => {
req = {
body: {
firstName: "firstname",
lastName: "lastname",
email: "test@test.com",
password: "Uptime1!",
role: ["admin"],
teamId: "123",
inviteToken: "invite",
},
db: {
checkSuperadmin: sinon.stub(),
getInviteTokenAndDelete: sinon.stub(),
@@ -29,7 +64,7 @@ describe("Auth Controller - registerUser", () => {
}),
},
emailService: {
buildAndSendEmail: sinon.stub().returns(Promise.resolve()),
buildAndSendEmail: sinon.stub(),
},
file: {},
};
@@ -38,72 +73,97 @@ describe("Auth Controller - registerUser", () => {
json: sinon.stub(),
};
next = sinon.stub();
sinon.stub(logger, "error");
});
afterEach(() => {
sinon.restore();
});
it("should register a valid user", async () => {
req.body = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
password: "Uptime1!",
inviteToken: "someToken",
role: ["user"],
teamId: "123",
};
req.db.checkSuperadmin.resolves(false);
req.db.insertUser.resolves({
_id: "123",
_doc: {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
},
});
it("should reject with an error if body validation fails", async () => {
req.body = {};
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if checkSuperadmin fails", async () => {
req.db.checkSuperadmin.rejects(new Error("checkSuperadmin error"));
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error");
});
it("should reject with an error if getInviteTokenAndDelete fails", async () => {
req.db.checkSuperadmin.resolves(true);
req.db.getInviteTokenAndDelete.rejects(
new Error("getInviteTokenAndDelete error")
);
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(
"getInviteTokenAndDelete error"
);
});
it("should reject with an error if updateAppSettings fails", async () => {
req.db.checkSuperadmin.resolves(false);
req.db.updateAppSettings.rejects(new Error("updateAppSettings error"));
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("updateAppSettings error");
});
it("should reject with an error if insertUser fails", async () => {
req.db.checkSuperadmin.resolves(false);
req.db.updateAppSettings.resolves();
req.db.insertUser.rejects(new Error("insertUser error"));
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("insertUser error");
});
it("should reject with an error if settingsService.getSettings fails", async () => {
req.db.checkSuperadmin.resolves(false);
req.db.updateAppSettings.resolves();
req.db.insertUser.resolves({ _id: "123" });
req.settingsService.getSettings.rejects(
new Error("settingsService.getSettings error")
);
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(
"settingsService.getSettings error"
);
});
it("should log an error if emailService.buildAndSendEmail fails", async () => {
req.db.checkSuperadmin.resolves(false);
req.db.updateAppSettings.resolves();
req.db.insertUser.returns({ _id: "123" });
req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
req.emailService.buildAndSendEmail.rejects(new Error("emailService error"));
await registerUser(req, res, next);
expect(logger.error.calledOnce).to.be.true;
expect(logger.error.firstCall.args[1].error).to.equal("emailService error");
});
it("should return a success message and data if all operations are successful", async () => {
const user = { _id: "123" };
req.db.checkSuperadmin.resolves(false);
req.db.updateAppSettings.resolves();
req.db.insertUser.returns(user);
req.settingsService.getSettings.returns({ jwtSecret: "my_secret" });
req.emailService.buildAndSendEmail.resolves("message-id");
await registerUser(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith(
sinon.match({
success: true,
msg: sinon.match.string,
data: {
user: sinon.match.object,
token: sinon.match.string,
},
})
)
res.json.calledWith({
success: true,
msg: successMessages.AUTH_CREATE_USER,
data: { user, token: sinon.match.string },
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should reject a user with an invalid password", async () => {
req.body = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
password: "bad_password",
inviteToken: "someToken",
role: ["user"],
teamId: "123",
};
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject a user with an invalid role", async () => {
req.body = {
firstName: "John",
lastName: "Doe",
email: "john.doe@example.com",
password: "Uptime1!",
inviteToken: "someToken",
role: ["superman"],
teamId: "123",
};
await registerUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
});
describe("Auth Controller - loginUser", () => {
@@ -131,6 +191,20 @@ describe("Auth Controller - loginUser", () => {
comparePassword: sinon.stub(),
};
});
it("should reject with an error if validation fails", async () => {
req.body = {};
await loginUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if getUserByEmail fails", async () => {
req.db.getUserByEmail.rejects(new Error("getUserByEmail error"));
await loginUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("getUserByEmail error");
});
it("should login user successfully", async () => {
req.db.getUserByEmail.resolves(user);
user.comparePassword.resolves(true);
@@ -186,10 +260,63 @@ describe("Auth Controller - editUser", async () => {
json: sinon.stub(),
};
next = sinon.stub();
stub = sinon.stub(jwt, "verify").returns({ email: "test@example.com" });
});
afterEach(() => {
sinon.restore();
stub.restore();
});
it("should reject with an error if param validation fails", async () => {
req.params = {};
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if body validation fails", async () => {
req.body = { invalid: 1 };
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if param.userId !== req.user._id", async () => {
req.params = { userId: "456" };
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(401);
});
it("should reject with an error if !req.body.password and getUserByEmail fails", async () => {
req.db.getUserByEmail.rejects(new Error("getUserByEmail error"));
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("getUserByEmail error");
});
it("should reject with an error if user.comparePassword fails", async () => {
req.db.getUserByEmail.returns({
comparePassword: sinon.stub().rejects(new Error("Bad Password Match")),
});
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("Bad Password Match");
});
it("should reject with an error if user.comparePassword returns false", async () => {
req.db.getUserByEmail.returns({
comparePassword: sinon.stub().returns(false),
});
await editUser(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(403);
expect(next.firstCall.args[0].message).to.equal(
errorMessages.AUTH_INCORRECT_PASSWORD
);
});
it("should edit a user if it receives a proper request", async () => {
sinon.stub(jwt, "verify").returns({ email: "test@example.com" });
const user = {
comparePassword: sinon.stub().resolves(true),
};
@@ -211,6 +338,24 @@ describe("Auth Controller - editUser", async () => {
expect(next.notCalled).to.be.true;
});
it("should edit a user if it receives a proper request and both password fields are undefined", async () => {
req.body.password = undefined;
req.body.newPassword = undefined;
req.db.getUserByEmail.resolves(user);
req.db.updateUser.resolves({ email: "test@example.com" });
await editUser(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.AUTH_UPDATE_USER,
data: { email: "test@example.com" },
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should reject an edit request if password format is incorrect", async () => {
req.body = { password: "bad_password", newPassword: "bad_password" };
const user = {
@@ -239,6 +384,13 @@ describe("Auth Controller - checkSuperadminExists", async () => {
next = sinon.stub();
});
it("should reject with an error if checkSuperadmin fails", async () => {
req.db.checkSuperadmin.rejects(new Error("checkSuperadmin error"));
await checkSuperadminExists(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error");
});
it("should return true if a superadmin exists", async () => {
req.db.checkSuperadmin.resolves(true);
await checkSuperadminExists(req, res, next);
@@ -289,6 +441,30 @@ describe("Auth Controller - requestRecovery", async () => {
};
next = sinon.stub();
});
it("should reject with an error if validation fails", async () => {
req.body = {};
await requestRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if getUserByEmail fails", async () => {
req.db.getUserByEmail.rejects(new Error("getUserByEmail error"));
await requestRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("getUserByEmail error");
});
it("should throw an error if the user is not found", async () => {
req.db.getUserByEmail.resolves(null);
await requestRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
// expect(next.firstCall.args[0].message).to.equal(
// errorMessages.FRIENDLY_ERROR
// );
});
it("should throw an error if the email is not provided", async () => {
req.body = {};
await requestRecovery(req, res, next);
@@ -344,15 +520,24 @@ describe("Auth Controller - validateRecovery", async () => {
next = sinon.stub();
});
it("should call next with a validation error if the token is invalid", async () => {
req = {
body: {},
};
it("should reject with an error if validation fails", async () => {
req.body = {};
await validateRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if validateRecoveryToken fails", async () => {
req.db.validateRecoveryToken.rejects(
new Error("validateRecoveryToken error")
);
await validateRecovery(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal(
"validateRecoveryToken error"
);
});
it("should return a success message if the token is valid", async () => {
req.db.validateRecoveryToken.resolves();
await validateRecovery(req, res, next);
@@ -391,14 +576,23 @@ describe("Auth Controller - resetPassword", async () => {
};
handleValidationError = sinon.stub();
handleError = sinon.stub();
issueToken = sinon.stub();
});
it("should call next with a validation error if the password is invalid", async () => {
it("should reject with an error if validation fails", async () => {
req.body = { password: "bad_password" };
await resetPassword(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if resetPassword fails", async () => {
const error = new Error("resetPassword error");
newPasswordValidation.validateAsync.resolves();
req.db.resetPassword.rejects(error);
await resetPassword(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("resetPassword error");
});
it("should reset password successfully", async () => {
const user = { _doc: {} };
const appSettings = { jwtSecret: "my_secret" };
@@ -407,7 +601,6 @@ describe("Auth Controller - resetPassword", async () => {
newPasswordValidation.validateAsync.resolves();
req.db.resetPassword.resolves(user);
req.settingsService.getSettings.resolves(appSettings);
issueToken.returns(token);
await resetPassword(req, res, next);

View File

@@ -0,0 +1,370 @@
const {
createCheck,
getChecks,
getTeamChecks,
deleteChecks,
deleteChecksByTeamId,
updateChecksTTL,
} = require("../../controllers/checkController");
const jwt = require("jsonwebtoken");
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
describe("Check Controller - createCheck", () => {
beforeEach(() => {
req = {
params: {},
body: {},
db: {
createCheck: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore(); // Restore the original methods after each test
});
it("should reject with a validation if params are invalid", async () => {
await createCheck(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with a validation error if body is invalid", async () => {
req.params = {
monitorId: "monitorId",
};
await createCheck(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should call next with error if data retrieval fails", async () => {
req.params = {
monitorId: "monitorId",
};
req.body = {
monitorId: "monitorId",
status: true,
responseTime: 100,
statusCode: 200,
message: "message",
};
req.db.createCheck.rejects(new Error("error"));
await createCheck(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
});
it("should return a success message if check is created", async () => {
req.params = {
monitorId: "monitorId",
};
req.db.createCheck.resolves({ id: "123" });
req.body = {
monitorId: "monitorId",
status: true,
responseTime: 100,
statusCode: 200,
message: "message",
};
await createCheck(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.CHECK_CREATE,
data: { id: "123" },
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
});
describe("Check Controller - getChecks", () => {
beforeEach(() => {
req = {
params: {},
query: {},
db: {
getChecks: sinon.stub(),
getChecksCount: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with a validation error if params are invalid", async () => {
await getChecks(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should return a success message if checks are found", async () => {
req.params = {
monitorId: "monitorId",
};
req.db.getChecks.resolves([{ id: "123" }]);
req.db.getChecksCount.resolves(1);
await getChecks(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: successMessages.CHECK_GET,
data: { checksCount: 1, checks: [{ id: "123" }] },
})
).to.be.true;
expect(next.notCalled).to.be.true;
});
it("should call next with error if data retrieval fails", async () => {
req.params = {
monitorId: "monitorId",
};
req.db.getChecks.rejects(new Error("error"));
await getChecks(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
});
});
describe("Check Controller - getTeamChecks", () => {
beforeEach(() => {
req = {
params: {},
query: {},
db: {
getTeamChecks: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with a validation error if params are invalid", async () => {
await getTeamChecks(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should return 200 and check data on successful validation and data retrieval", async () => {
req.params = { teamId: "1" };
const checkData = [{ id: 1, name: "Check 1" }];
req.db.getTeamChecks.resolves(checkData);
await getTeamChecks(req, res, next);
expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.CHECK_GET,
data: checkData,
})
).to.be.true;
});
it("should call next with error if data retrieval fails", async () => {
req.params = { teamId: "1" };
req.db.getTeamChecks.rejects(new Error("Retrieval Error"));
await getTeamChecks(req, res, next);
expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true;
expect(next.firstCall.args[0]).to.be.an("error");
expect(res.status.notCalled).to.be.true;
expect(res.json.notCalled).to.be.true;
});
});
describe("Check Controller - deleteChecks", () => {
beforeEach(() => {
req = {
params: {},
db: {
deleteChecks: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if param validation fails", async () => {
await deleteChecks(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should call next with error if data retrieval fails", async () => {
req.params = { monitorId: "1" };
req.db.deleteChecks.rejects(new Error("Deletion Error"));
await deleteChecks(req, res, next);
expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true;
expect(next.firstCall.args[0]).to.be.an("error");
expect(res.status.notCalled).to.be.true;
expect(res.json.notCalled).to.be.true;
});
it("should delete checks successfully", async () => {
req.params = { monitorId: "123" };
req.db.deleteChecks.resolves(1);
await deleteChecks(req, res, next);
expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount: 1 },
})
).to.be.true;
});
});
describe("Check Controller - deleteChecksByTeamId", () => {
beforeEach(() => {
req = {
params: {},
db: {
deleteChecksByTeamId: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if param validation fails", async () => {
await deleteChecksByTeamId(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should call next with error if data retrieval fails", async () => {
req.params = { teamId: "1" };
req.db.deleteChecksByTeamId.rejects(new Error("Deletion Error"));
await deleteChecksByTeamId(req, res, next);
expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be
.true;
expect(next.firstCall.args[0]).to.be.an("error");
expect(res.status.notCalled).to.be.true;
expect(res.json.notCalled).to.be.true;
});
it("should delete checks successfully", async () => {
req.params = { teamId: "123" };
req.db.deleteChecksByTeamId.resolves(1);
await deleteChecksByTeamId(req, res, next);
expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be
.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.CHECK_DELETE,
data: { deletedCount: 1 },
})
).to.be.true;
});
});
describe("Check Controller - updateCheckTTL", () => {
beforeEach(() => {
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
req = {
body: {},
headers: { authorization: "Bearer token" },
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }),
},
db: {
updateChecksTTL: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
stub.restore();
});
it("should reject if body validation fails", async () => {
await updateChecksTTL(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should throw a JwtError if verification fails", async () => {
stub.restore();
req.body = {
ttl: 1,
};
await updateChecksTTL(req, res, next);
expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
});
it("should call next with error if data retrieval fails", async () => {
req.body = {
ttl: 1,
};
req.db.updateChecksTTL.rejects(new Error("Update Error"));
await updateChecksTTL(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
});
it("should update TTL successfully", async () => {
req.body = {
ttl: 1,
};
req.db.updateChecksTTL.resolves();
await updateChecksTTL(req, res, next);
expect(req.db.updateChecksTTL.calledOnceWith("123", 1 * 86400)).to.be.true;
expect(res.status.calledOnceWith(200)).to.be.true;
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.CHECK_UPDATE_TTL,
})
).to.be.true;
});
});

View File

@@ -0,0 +1,105 @@
const {
handleValidationError,
handleError,
} = require("../../controllers/controllerUtils");
describe("controllerUtils - handleValidationError", () => {
it("should set status to 422", () => {
const error = {};
const serviceName = "TestService";
const result = handleValidationError(error, serviceName);
expect(result.status).to.equal(422);
});
it("should set service to the provided serviceName", () => {
const error = {};
const serviceName = "TestService";
const result = handleValidationError(error, serviceName);
expect(result.service).to.equal(serviceName);
});
it("should set message to error.details[0].message if present", () => {
const error = {
details: [{ message: "Detail message" }],
};
const serviceName = "TestService";
const result = handleValidationError(error, serviceName);
expect(result.message).to.equal("Detail message");
});
it("should set message to error.message if error.details is not present", () => {
const error = {
message: "Error message",
};
const serviceName = "TestService";
const result = handleValidationError(error, serviceName);
expect(result.message).to.equal("Error message");
});
it('should set message to "Validation Error" if neither error.details nor error.message is present', () => {
const error = {};
const serviceName = "TestService";
const result = handleValidationError(error, serviceName);
expect(result.message).to.equal("Validation Error");
});
});
describe("handleError", () => {
it("should set stats to the provided status if error.code is undefined", () => {
const error = {};
const serviceName = "TestService";
const method = "testMethod";
const status = 400;
const result = handleError(error, serviceName, method, status);
expect(result.status).to.equal(status);
});
it("should not overwrite error.code if it is already defined", () => {
const error = { status: 404 };
const serviceName = "TestService";
const method = "testMethod";
const status = 400;
const result = handleError(error, serviceName, method, status);
expect(result.status).to.equal(404);
});
it("should set service to the provided serviceName if error.service is undefined", () => {
const error = {};
const serviceName = "TestService";
const method = "testMethod";
const result = handleError(error, serviceName, method);
expect(result.service).to.equal(serviceName);
});
it("should not overwrite error.service if it is already defined", () => {
const error = { service: "ExistingService" };
const serviceName = "TestService";
const method = "testMethod";
const result = handleError(error, serviceName, method);
expect(result.service).to.equal("ExistingService");
});
it("should set method to the provided method if error.method is undefined", () => {
const error = {};
const serviceName = "TestService";
const method = "testMethod";
const result = handleError(error, serviceName, method);
expect(result.method).to.equal(method);
});
it("should not overwrite error.method if it is already defined", () => {
const error = { method: "existingMethod" };
const serviceName = "TestService";
const method = "testMethod";
const result = handleError(error, serviceName, method);
expect(result.method).to.equal("existingMethod");
});
it("should set code to 500 if error.code is undefined and no code is provided", () => {
const error = {};
const serviceName = "TestService";
const method = "testMethod";
const result = handleError(error, serviceName, method);
expect(result.status).to.equal(500);
});
});

View File

@@ -0,0 +1,202 @@
const {
issueInvitation,
inviteVerifyController,
} = require("../../controllers/inviteController");
const jwt = require("jsonwebtoken");
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
const joi = require("joi");
describe("inviteController - issueInvitation", () => {
beforeEach(() => {
req = {
headers: { authorization: "Bearer token" },
body: {
email: "test@test.com",
role: ["admin"],
teamId: "123",
},
db: { requestInviteToken: sinon.stub() },
settingsService: { getSettings: sinon.stub() },
emailService: { buildAndSendEmail: sinon.stub() },
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if role validation fails", async () => {
stub = sinon.stub(jwt, "decode").callsFake(() => {
return { role: ["bad_role"], firstname: "first_name", teamId: "1" };
});
await issueInvitation(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0]).to.be.instanceOf(joi.ValidationError);
expect(next.firstCall.args[0].status).to.equal(422);
stub.restore();
});
it("should reject with an error if body validation fails", async () => {
stub = sinon.stub(jwt, "decode").callsFake(() => {
return { role: ["admin"], firstname: "first_name", teamId: "1" };
});
req.body = {};
await issueInvitation(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
stub.restore();
});
it("should reject with an error if DB operations fail", async () => {
stub = sinon.stub(jwt, "decode").callsFake(() => {
return { role: ["admin"], firstname: "first_name", teamId: "1" };
});
req.db.requestInviteToken.throws(new Error("DB error"));
await issueInvitation(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
stub.restore();
});
it("should send an invite successfully", async () => {
const token = "token";
const decodedToken = {
role: "admin",
firstname: "John",
teamId: "team123",
};
const inviteToken = { token: "inviteToken" };
const clientHost = "http://localhost";
stub = sinon.stub(jwt, "decode").callsFake(() => {
return decodedToken;
});
req.db.requestInviteToken.resolves(inviteToken);
req.settingsService.getSettings.returns({ clientHost });
req.emailService.buildAndSendEmail.resolves();
await issueInvitation(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: "Invite sent",
data: inviteToken,
})
).to.be.true;
stub.restore();
});
it("should send an email successfully", async () => {
const token = "token";
const decodedToken = {
role: "admin",
firstname: "John",
teamId: "team123",
};
const inviteToken = { token: "inviteToken" };
const clientHost = "http://localhost";
stub = sinon.stub(jwt, "decode").callsFake(() => {
return decodedToken;
});
req.db.requestInviteToken.resolves(inviteToken);
req.settingsService.getSettings.returns({ clientHost });
req.emailService.buildAndSendEmail.resolves();
await issueInvitation(req, res, next);
expect(req.emailService.buildAndSendEmail.calledOnce).to.be.true;
expect(
req.emailService.buildAndSendEmail.calledWith(
"employeeActivationTemplate",
{
name: "John",
link: "http://localhost/register/inviteToken",
},
"test@test.com",
"Welcome to Uptime Monitor"
)
).to.be.true;
stub.restore();
});
it("should continue executing if sending an email fails", async () => {
const token = "token";
req.emailService.buildAndSendEmail.rejects(new Error("Email error"));
const decodedToken = {
role: "admin",
firstname: "John",
teamId: "team123",
};
const inviteToken = { token: "inviteToken" };
const clientHost = "http://localhost";
stub = sinon.stub(jwt, "decode").callsFake(() => {
return decodedToken;
});
req.db.requestInviteToken.resolves(inviteToken);
req.settingsService.getSettings.returns({ clientHost });
await issueInvitation(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
success: true,
msg: "Invite sent",
data: inviteToken,
})
).to.be.true;
stub.restore();
});
});
describe("inviteController - inviteVerifyController", () => {
beforeEach(() => {
req = {
body: { token: "token" },
db: {
getInviteToken: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if body validation fails", async () => {
req.body = {};
await inviteVerifyController(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if DB operations fail", async () => {
req.db.getInviteToken.throws(new Error("DB error"));
await inviteVerifyController(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
});
it("should return 200 and invite data when validation and invite retrieval are successful", async () => {
req.db.getInviteToken.resolves({ invite: "data" });
await inviteVerifyController(req, res, next);
expect(res.status.calledWith(200)).to.be.true;
expect(
res.json.calledWith({
status: "success",
msg: "Invite verified",
data: { invite: "data" },
})
).to.be.true;
expect(next.called).to.be.false;
});
});

View File

@@ -0,0 +1,410 @@
const {
createMaintenanceWindows,
getMaintenanceWindowById,
getMaintenanceWindowsByTeamId,
getMaintenanceWindowsByMonitorId,
deleteMaintenanceWindow,
editMaintenanceWindow,
} = require("../../controllers/maintenanceWindowController");
const jwt = require("jsonwebtoken");
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
describe("maintenanceWindowController - createMaintenanceWindows", () => {
beforeEach(() => {
req = {
body: {
monitors: ["66ff52e7c5911c61698ac724"],
name: "window",
active: true,
start: "2024-10-11T05:27:13.747Z",
end: "2024-10-11T05:27:14.747Z",
repeat: "123",
},
headers: {
authorization: "Bearer token",
},
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
},
db: {
createMaintenanceWindow: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if body validation fails", async () => {
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
req.body = {};
await createMaintenanceWindows(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
stub.restore();
});
it("should reject with an error if jwt.verify fails", async () => {
stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError());
await createMaintenanceWindows(req, res, next);
expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
stub.restore();
});
it("should reject with an error DB operations fail", async () => {
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
req.db.createMaintenanceWindow.throws(new Error("DB error"));
await createMaintenanceWindows(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
stub.restore();
});
it("should return success message if all operations are successful", async () => {
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
await createMaintenanceWindows(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(201);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
})
).to.be.true;
stub.restore();
});
it("should return success message if all operations are successful with active set to undefined", async () => {
req.body.active = undefined;
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
await createMaintenanceWindows(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(201);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_CREATE,
})
).to.be.true;
stub.restore();
});
});
describe("maintenanceWindowController - getMaintenanceWindowById", () => {
beforeEach(() => {
req = {
body: {},
params: {
id: "123",
},
headers: {
authorization: "Bearer token",
},
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
},
db: {
getMaintenanceWindowById: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should reject if param validation fails", async () => {
req.params = {};
await getMaintenanceWindowById(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject if DB operations fail", async () => {
req.db.getMaintenanceWindowById.throws(new Error("DB error"));
await getMaintenanceWindowById(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
});
it("should return success message with data if all operations are successful", async () => {
req.db.getMaintenanceWindowById.returns({ id: "123" });
await getMaintenanceWindowById(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID,
data: { id: "123" },
})
).to.be.true;
});
});
describe("maintenanceWindowController - getMaintenanceWindowsByTeamId", () => {
beforeEach(() => {
req = {
body: {},
params: {},
query: {},
headers: {
authorization: "Bearer token",
},
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
},
db: {
getMaintenanceWindowsByTeamId: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
it("should reject if query validation fails", async () => {
req.query = {
invalid: 1,
};
await getMaintenanceWindowsByTeamId(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject if jwt.verify fails", async () => {
stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError());
await getMaintenanceWindowsByTeamId(req, res, next);
expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError);
stub.restore();
});
it("should reject with an error if DB operations fail", async () => {
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
req.db.getMaintenanceWindowsByTeamId.throws(new Error("DB error"));
await getMaintenanceWindowsByTeamId(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
stub.restore();
});
it("should return success message with data if all operations are successful", async () => {
stub = sinon.stub(jwt, "verify").callsFake(() => {
return { teamId: "123" };
});
req.db.getMaintenanceWindowsByTeamId.returns([{ id: "123" }]);
await getMaintenanceWindowsByTeamId(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM,
data: [{ id: jwt.verify().teamId }],
})
).to.be.true;
stub.restore();
});
});
describe("maintenanceWindowController - getMaintenanceWindowsByMonitorId", () => {
beforeEach(() => {
req = {
body: {},
params: {
monitorId: "123",
},
query: {},
headers: {
authorization: "Bearer token",
},
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
},
db: {
getMaintenanceWindowsByMonitorId: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject if param validation fails", async () => {
req.params = {};
await getMaintenanceWindowsByMonitorId(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if DB operations fail", async () => {
req.db.getMaintenanceWindowsByMonitorId.throws(new Error("DB error"));
await getMaintenanceWindowsByMonitorId(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
});
it("should return success message with data if all operations are successful", async () => {
const data = [{ monitorId: "123" }];
req.db.getMaintenanceWindowsByMonitorId.returns(data);
await getMaintenanceWindowsByMonitorId(req, res, next);
expect(
req.db.getMaintenanceWindowsByMonitorId.calledOnceWith(
req.params.monitorId
)
);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_GET_BY_MONITOR,
data: data,
})
).to.be.true;
});
});
describe("maintenanceWindowController - deleteMaintenanceWindow", () => {
beforeEach(() => {
req = {
body: {},
params: {
id: "123",
},
query: {},
headers: {
authorization: "Bearer token",
},
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
},
db: {
deleteMaintenanceWindowById: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject if param validation fails", async () => {
req.params = {};
await deleteMaintenanceWindow(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if DB operations fail", async () => {
req.db.deleteMaintenanceWindowById.throws(new Error("DB error"));
await deleteMaintenanceWindow(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
});
it("should return success message if all operations are successful", async () => {
await deleteMaintenanceWindow(req, res, next);
expect(req.db.deleteMaintenanceWindowById.calledOnceWith(req.params.id));
expect(res.status.firstCall.args[0]).to.equal(200);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_DELETE,
})
).to.be.true;
});
});
describe("maintenanceWindowController - editMaintenanceWindow", () => {
beforeEach(() => {
req = {
body: {
active: true,
name: "test",
},
params: {
id: "123",
},
query: {},
headers: {
authorization: "Bearer token",
},
settingsService: {
getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }),
},
db: {
editMaintenanceWindowById: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject if param validation fails", async () => {
req.params = {};
await editMaintenanceWindow(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject if body validation fails", async () => {
req.body = { invalid: 1 };
await editMaintenanceWindow(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if DB operations fail", async () => {
req.db.editMaintenanceWindowById.throws(new Error("DB error"));
await editMaintenanceWindow(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("DB error");
});
it("should return success message with data if all operations are successful", async () => {
const data = { id: "123" };
req.db.editMaintenanceWindowById.returns(data);
await editMaintenanceWindow(req, res, next);
expect(
req.db.editMaintenanceWindowById.calledOnceWith(req.params.id, req.body)
);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(
res.json.calledOnceWith({
success: true,
msg: successMessages.MAINTENANCE_WINDOW_EDIT,
data: data,
})
).to.be.true;
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,168 @@
const { afterEach } = require("node:test");
const {
getMetrics,
getJobs,
addJob,
obliterateQueue,
} = require("../../controllers/queueController");
const SERVICE_NAME = "JobQueueController";
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
describe("Queue Controller - getMetrics", () => {
beforeEach(() => {
req = {
headers: {},
params: {},
body: {},
db: {},
jobQueue: {
getMetrics: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should throw an error if getMetrics throws an error", async () => {
req.jobQueue.getMetrics.throws(new Error("getMetrics error"));
await getMetrics(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("getMetrics error");
});
it("should return a success message and data if getMetrics is successful", async () => {
const data = { data: "metrics" };
req.jobQueue.getMetrics.returns(data);
await getMetrics(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(res.json.firstCall.args[0]).to.deep.equal({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data,
});
});
});
describe("Queue Controller - getJobs", () => {
beforeEach(() => {
req = {
headers: {},
params: {},
body: {},
db: {},
jobQueue: {
getJobStats: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if getJobs throws an error", async () => {
req.jobQueue.getJobStats.throws(new Error("getJobs error"));
await getJobs(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("getJobs error");
});
it("should return a success message and data if getJobs is successful", async () => {
const data = { data: "jobs" };
req.jobQueue.getJobStats.returns(data);
await getJobs(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(res.json.firstCall.args[0]).to.deep.equal({
success: true,
msg: successMessages.QUEUE_GET_METRICS,
data,
});
});
});
describe("Queue Controller - addJob", () => {
beforeEach(() => {
req = {
headers: {},
params: {},
body: {},
db: {},
jobQueue: {
addJob: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if addJob throws an error", async () => {
req.jobQueue.addJob.throws(new Error("addJob error"));
await addJob(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("addJob error");
});
it("should return a success message if addJob is successful", async () => {
req.jobQueue.addJob.resolves();
await addJob(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(res.json.firstCall.args[0]).to.deep.equal({
success: true,
msg: successMessages.QUEUE_ADD_JOB,
});
});
});
describe("Queue Controller - obliterateQueue", () => {
beforeEach(() => {
req = {
headers: {},
params: {},
body: {},
db: {},
jobQueue: {
obliterate: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if obliterateQueue throws an error", async () => {
req.jobQueue.obliterate.throws(new Error("obliterateQueue error"));
await obliterateQueue(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("obliterateQueue error");
});
it("should return a success message if obliterateQueue is successful", async () => {
req.jobQueue.obliterate.resolves();
await obliterateQueue(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(res.json.firstCall.args[0]).to.deep.equal({
success: true,
msg: successMessages.QUEUE_OBLITERATE,
});
});
});

View File

@@ -0,0 +1,105 @@
const { afterEach } = require("node:test");
const {
getAppSettings,
updateAppSettings,
} = require("../../controllers/settingsController");
const { errorMessages, successMessages } = require("../../utils/messages");
const sinon = require("sinon");
describe("Settings Controller - getAppSettings", () => {
beforeEach(() => {
req = {
headers: {},
params: {},
body: {},
db: {},
settingsService: {
getSettings: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should throw an error if getSettings throws an error", async () => {
req.settingsService.getSettings.throws(new Error("getSettings error"));
await getAppSettings(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("getSettings error");
});
it("should return a success message and data if getSettings is successful", async () => {
const data = { data: "settings" };
req.settingsService.getSettings.returns(data);
await getAppSettings(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(res.json.firstCall.args[0]).to.deep.equal({
success: true,
msg: successMessages.GET_APP_SETTINGS,
data,
});
});
});
describe("Settings Controller - updateAppSettings", () => {
beforeEach(() => {
req = {
headers: {},
params: {},
body: {},
db: {
updateAppSettings: sinon.stub(),
},
settingsService: {
reloadSettings: sinon.stub(),
},
};
res = {
status: sinon.stub().returnsThis(),
json: sinon.stub(),
};
next = sinon.stub();
handleError = sinon.stub();
});
afterEach(() => {
sinon.restore();
});
it("should reject with an error if body validation fails", async () => {
req.body = { invalid: 1 };
await updateAppSettings(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].status).to.equal(422);
});
it("should reject with an error if updateAppSettings throws an error", async () => {
req.db.updateAppSettings.throws(new Error("updateAppSettings error"));
await updateAppSettings(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("updateAppSettings error");
});
it("should reject with an error if reloadSettings throws an error", async () => {
req.settingsService.reloadSettings.throws(
new Error("reloadSettings error")
);
await updateAppSettings(req, res, next);
expect(next.firstCall.args[0]).to.be.an("error");
expect(next.firstCall.args[0].message).to.equal("reloadSettings error");
});
it("should return a success message and data if updateAppSettings is successful", async () => {
const data = { data: "settings" };
req.settingsService.reloadSettings.returns(data);
await updateAppSettings(req, res, next);
expect(res.status.firstCall.args[0]).to.equal(200);
expect(res.json.firstCall.args[0]).to.deep.equal({
success: true,
msg: successMessages.UPDATE_APP_SETTINGS,
data,
});
});
});

View File

@@ -86,6 +86,12 @@ const successMessages = {
MONITOR_CERTIFICATE: "Got monitor certificate successfully",
MONITOR_DEMO_ADDED: "Successfully added demo monitors",
// Queue Controller
QUEUE_GET_METRICS: "Got metrics successfully",
QUEUE_GET_METRICS: "Got job stats successfully",
QUEUE_ADD_JOB: "Job added successfully",
QUEUE_OBLITERATE: "Queue obliterated",
//Job Queue
JOB_QUEUE_DELETE_JOB: "Job removed successfully",
JOB_QUEUE_OBLITERATE: "Queue OBLITERATED!!!",

View File

@@ -10,8 +10,8 @@ const { start } = require("repl");
const roleValidatior = (role) => (value, helpers) => {
const hasRole = role.some((role) => value.includes(role));
if (!hasRole) {
throw new Joi.ValidationError(
`You do not have the required authorization. Required roles: ${roles.join(", ")}`
throw new joi.ValidationError(
`You do not have the required authorization. Required roles: ${role.join(", ")}`
);
}
return value;