add test env
This commit is contained in:
1 parent
75acf7b38f
commit
b4da045770
7 files changed
+6809
-2
No files matched your search
+2
-1
@@ -8,8 +8,9 @@ yarn-error.log*
|
||||
.env
|
||||
.DS_Store
|
||||
/logs/
|
||||
coverage/
|
||||
|
||||
# TypeScript
|
||||
/dist/
|
||||
/build/
|
||||
*.tsbuildinfo
|
||||
*.tsbuildinfo
|
||||
@@ -0,0 +1,24 @@
|
||||
/** @type {import('jest').Config} */
|
||||
const config = {
|
||||
preset: "ts-jest/presets/default-esm",
|
||||
testEnvironment: "node",
|
||||
rootDir: "./",
|
||||
roots: ["<rootDir>/tests"],
|
||||
testMatch: ["**/*.test.ts"],
|
||||
verbose: true,
|
||||
moduleNameMapper: {
|
||||
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
"^(\\.\\.?/.*)\\.js$": "$1"
|
||||
},
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
transform: {
|
||||
"^.+\\.(t|j)s$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }]
|
||||
},
|
||||
collectCoverageFrom: ["src/**/*.{ts,js}", "!src/**/*.d.ts"],
|
||||
coverageDirectory: "coverage",
|
||||
coverageReporters: ["text", "lcov"],
|
||||
coveragePathIgnorePatterns: ["/node_modules/", "<rootDir>/src/index.ts"]
|
||||
};
|
||||
|
||||
export default config;
|
||||
Generated
+6427
File diff suppressed because it is too large.
Load diff
+6
-1
@@ -6,7 +6,9 @@
|
||||
"scripts": {
|
||||
"dev": "nodemon --exec tsx src/index.js",
|
||||
"build": "tsc && tsc-alias",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
"test": "jest",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
@@ -15,10 +17,13 @@
|
||||
"devDependencies": {
|
||||
"@types/cookie-parser": "1.4.9",
|
||||
"@types/cors": "2.8.19",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jsonwebtoken": "9.0.10",
|
||||
"@types/node": "24.7.1",
|
||||
"@types/nodemailer": "7.0.2",
|
||||
"jest": "^29.7.0",
|
||||
"nodemon": "3.1.10",
|
||||
"ts-jest": "^29.2.5",
|
||||
"tsc-alias": "1.8.16",
|
||||
"tsx": "4.20.6",
|
||||
"typescript": "5.9.3"
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
import StatusService from "@/services/infrastructure/StatusService.js";
|
||||
import { MonitorStats } from "@/db/models/index.js";
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
import { Types } from "mongoose";
|
||||
import type { IMonitorStats } from "@/db/models/index.js";
|
||||
import type { StatusResponse } from "@/services/infrastructure/NetworkService.js";
|
||||
import type { IMonitor } from "@/db/models/monitors/Monitor.js";
|
||||
import type { MonitorStatus } from "@/db/models/monitors/Monitor.js";
|
||||
|
||||
const buildStats = (
|
||||
overrides: Partial<IMonitorStats & { save: jest.Mock<any, any> }> = {}
|
||||
): IMonitorStats => {
|
||||
const saveMock =
|
||||
overrides.save ||
|
||||
jest.fn().mockImplementation(async function (this: unknown) {
|
||||
return this;
|
||||
});
|
||||
return {
|
||||
monitorId: {} as any,
|
||||
avgResponseTime: 0,
|
||||
maxResponseTime: 0,
|
||||
totalChecks: 1,
|
||||
totalUpChecks: 0,
|
||||
totalDownChecks: 0,
|
||||
uptimePercentage: 0,
|
||||
lastCheckTimestamp: 0,
|
||||
lastResponseTime: 0,
|
||||
timeOfLastFailure: 0,
|
||||
currentStreak: 0,
|
||||
currentStreakStatus: "up",
|
||||
currentStreakStartedAt: 0,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
save: saveMock,
|
||||
...overrides,
|
||||
} as unknown as IMonitorStats;
|
||||
};
|
||||
|
||||
const buildStatusResponse = (
|
||||
overrides: Partial<StatusResponse> = {}
|
||||
): StatusResponse => ({
|
||||
monitorId: "monitor-id",
|
||||
teamId: "team-id",
|
||||
type: "http" as any,
|
||||
status: "up",
|
||||
message: "OK",
|
||||
responseTime: 0,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
const buildMonitor = (
|
||||
overrides: Partial<IMonitor> & { status?: MonitorStatus }
|
||||
) => {
|
||||
const monitor: any = {
|
||||
_id: new Types.ObjectId(),
|
||||
teamId: "team-id",
|
||||
status: "up" as MonitorStatus,
|
||||
latestChecks: [],
|
||||
n: 1,
|
||||
save: jest.fn(),
|
||||
...overrides,
|
||||
};
|
||||
if (!monitor.latestChecks) {
|
||||
monitor.latestChecks = [];
|
||||
}
|
||||
monitor.save.mockImplementation(async () => monitor);
|
||||
return {
|
||||
monitor: monitor as IMonitor,
|
||||
saveMock: monitor.save as jest.Mock,
|
||||
};
|
||||
};
|
||||
|
||||
describe("StatusService.calculateAvgResponseTime", () => {
|
||||
const service = new StatusService();
|
||||
|
||||
it("returns latest response time when average is unset", () => {
|
||||
const stats = buildStats({ avgResponseTime: 0, totalChecks: 1 });
|
||||
const statusResponse = buildStatusResponse({ responseTime: 250 });
|
||||
|
||||
const result = service.calculateAvgResponseTime(stats, statusResponse);
|
||||
|
||||
expect(result).toBe(250);
|
||||
});
|
||||
|
||||
it("computes rolling average when checks exist", () => {
|
||||
const stats = buildStats({ avgResponseTime: 120, totalChecks: 5 });
|
||||
const statusResponse = buildStatusResponse({ responseTime: 300 });
|
||||
|
||||
const result = service.calculateAvgResponseTime(stats, statusResponse);
|
||||
|
||||
expect(result).toBeCloseTo(156);
|
||||
});
|
||||
});
|
||||
|
||||
describe("StatusService.updateMonitorStatus", () => {
|
||||
const service = new StatusService();
|
||||
|
||||
it("initializing monitor adopts first status", async () => {
|
||||
const { monitor, saveMock } = buildMonitor({
|
||||
status: "initializing" as MonitorStatus,
|
||||
latestChecks: [],
|
||||
});
|
||||
const statusResponse = buildStatusResponse({ status: "up", responseTime: 120 });
|
||||
|
||||
const [savedMonitor, statusChanged] = await service.updateMonitorStatus(
|
||||
monitor,
|
||||
statusResponse
|
||||
);
|
||||
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(monitor.status).toBe("up");
|
||||
expect(monitor.latestChecks).toHaveLength(1);
|
||||
expect(monitor.latestChecks?.[0]?.status).toBe("up");
|
||||
expect(monitor.lastCheckedAt).toBeInstanceOf(Date);
|
||||
expect(savedMonitor).toBe(monitor);
|
||||
expect(statusChanged).toBe(false);
|
||||
});
|
||||
|
||||
it("returns early when not enough checks accumulated", async () => {
|
||||
const { monitor, saveMock } = buildMonitor({
|
||||
status: "up" as MonitorStatus,
|
||||
n: 3,
|
||||
latestChecks: [],
|
||||
});
|
||||
const statusResponse = buildStatusResponse({ status: "down", responseTime: 90 });
|
||||
|
||||
const [, statusChanged] = await service.updateMonitorStatus(
|
||||
monitor,
|
||||
statusResponse
|
||||
);
|
||||
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(monitor.status).toBe("up");
|
||||
expect(monitor.latestChecks).toHaveLength(1);
|
||||
expect(statusChanged).toBe(false);
|
||||
});
|
||||
|
||||
it("updates status when last n checks differ", async () => {
|
||||
const { monitor, saveMock } = buildMonitor({
|
||||
status: "up" as MonitorStatus,
|
||||
n: 2,
|
||||
latestChecks: [
|
||||
{
|
||||
status: "down" as MonitorStatus,
|
||||
responseTime: 80,
|
||||
checkedAt: new Date("2024-01-01"),
|
||||
},
|
||||
],
|
||||
});
|
||||
const statusResponse = buildStatusResponse({ status: "down", responseTime: 70 });
|
||||
|
||||
const [, statusChanged] = await service.updateMonitorStatus(
|
||||
monitor,
|
||||
statusResponse
|
||||
);
|
||||
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(monitor.status).toBe("down");
|
||||
expect(statusChanged).toBe(true);
|
||||
expect(monitor.latestChecks.slice(-2).every((c) => c.status === "down")).toBe(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps only latest 25 checks", async () => {
|
||||
const existingChecks = Array.from({ length: 25 }, (_, index) => ({
|
||||
status: "up" as MonitorStatus,
|
||||
responseTime: index,
|
||||
checkedAt: new Date(`2024-01-${index + 1}`),
|
||||
}));
|
||||
const { monitor, saveMock } = buildMonitor({
|
||||
status: "up" as MonitorStatus,
|
||||
n: 1,
|
||||
latestChecks: [...existingChecks],
|
||||
});
|
||||
const statusResponse = buildStatusResponse({ status: "up", responseTime: 200 });
|
||||
|
||||
await service.updateMonitorStatus(monitor, statusResponse);
|
||||
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(monitor.latestChecks).toHaveLength(25);
|
||||
expect(monitor.latestChecks?.[0]?.responseTime).toBe(1);
|
||||
expect(
|
||||
monitor.latestChecks?.some((check) => check.responseTime === 0)
|
||||
).toBe(false);
|
||||
const lastCheck =
|
||||
monitor.latestChecks?.[monitor.latestChecks.length - 1] ?? null;
|
||||
expect(lastCheck?.responseTime).toBe(200);
|
||||
});
|
||||
|
||||
it("initializes latestChecks when missing", async () => {
|
||||
const { monitor, saveMock } = buildMonitor({});
|
||||
delete (monitor as any).latestChecks;
|
||||
const statusResponse = buildStatusResponse({ status: "up", responseTime: 75 });
|
||||
|
||||
const [, statusChanged] = await service.updateMonitorStatus(
|
||||
monitor,
|
||||
statusResponse
|
||||
);
|
||||
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(Array.isArray(monitor.latestChecks)).toBe(true);
|
||||
expect(monitor.latestChecks).toHaveLength(1);
|
||||
expect(statusChanged).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("StatusService.updateMonitorStats", () => {
|
||||
const service = new StatusService();
|
||||
let findOneSpy: jest.SpiedFunction<typeof MonitorStats.findOne>;
|
||||
|
||||
beforeEach(() => {
|
||||
findOneSpy = jest.spyOn(MonitorStats, "findOne");
|
||||
jest.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("throws ApiError when monitor stats are missing", async () => {
|
||||
findOneSpy.mockResolvedValue(null);
|
||||
const { monitor } = buildMonitor({});
|
||||
const statusResponse = buildStatusResponse({ status: "up" });
|
||||
|
||||
await expect(
|
||||
service.updateMonitorStats(monitor, statusResponse, false)
|
||||
).rejects.toBeInstanceOf(ApiError);
|
||||
await expect(
|
||||
service.updateMonitorStats(monitor, statusResponse, false)
|
||||
).rejects.toMatchObject({ message: "MonitorStats not found", status: 500 });
|
||||
expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id });
|
||||
});
|
||||
|
||||
it("increments counters and preserves streak when status unchanged", async () => {
|
||||
const now = new Date("2024-02-01T12:00:00Z");
|
||||
jest.setSystemTime(now);
|
||||
const stats = buildStats({
|
||||
totalChecks: 4,
|
||||
totalUpChecks: 3,
|
||||
totalDownChecks: 1,
|
||||
avgResponseTime: 100,
|
||||
maxResponseTime: 250,
|
||||
lastResponseTime: 90,
|
||||
currentStreak: 2,
|
||||
currentStreakStatus: "up",
|
||||
currentStreakStartedAt: 111,
|
||||
lastCheckTimestamp: 1,
|
||||
timeOfLastFailure: 555,
|
||||
});
|
||||
const saveMock = stats.save as jest.Mock;
|
||||
findOneSpy.mockResolvedValue(stats);
|
||||
const { monitor } = buildMonitor({});
|
||||
const statusResponse = buildStatusResponse({ status: "up", responseTime: 150 });
|
||||
|
||||
const result = await service.updateMonitorStats(
|
||||
monitor,
|
||||
statusResponse,
|
||||
false
|
||||
);
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id });
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe(stats);
|
||||
expect(stats.totalChecks).toBe(5);
|
||||
expect(stats.totalUpChecks).toBe(4);
|
||||
expect(stats.totalDownChecks).toBe(1);
|
||||
expect(stats.currentStreak).toBe(3);
|
||||
expect(stats.currentStreakStatus).toBe("up");
|
||||
expect(stats.currentStreakStartedAt).toBe(111);
|
||||
expect(stats.lastCheckTimestamp).toBe(now.getTime());
|
||||
expect(stats.timeOfLastFailure).toBe(555);
|
||||
expect(stats.avgResponseTime).toBeCloseTo(110);
|
||||
expect(stats.uptimePercentage).toBeCloseTo(4 / 5);
|
||||
expect(stats.lastResponseTime).toBe(150);
|
||||
expect(stats.maxResponseTime).toBe(250);
|
||||
});
|
||||
|
||||
it("resets streak and records failure on status change to down", async () => {
|
||||
const now = new Date("2024-02-01T12:05:00Z");
|
||||
jest.setSystemTime(now);
|
||||
const stats = buildStats({
|
||||
totalChecks: 10,
|
||||
totalUpChecks: 7,
|
||||
totalDownChecks: 3,
|
||||
avgResponseTime: 200,
|
||||
maxResponseTime: 300,
|
||||
currentStreak: 5,
|
||||
currentStreakStatus: "up",
|
||||
currentStreakStartedAt: 999,
|
||||
timeOfLastFailure: 444,
|
||||
});
|
||||
const saveMock = stats.save as jest.Mock;
|
||||
findOneSpy.mockResolvedValue(stats);
|
||||
const { monitor } = buildMonitor({});
|
||||
const statusResponse = buildStatusResponse({ status: "down", responseTime: 400 });
|
||||
|
||||
const result = await service.updateMonitorStats(
|
||||
monitor,
|
||||
statusResponse,
|
||||
true
|
||||
);
|
||||
|
||||
expect(findOneSpy).toHaveBeenCalledWith({ monitorId: monitor._id });
|
||||
expect(saveMock).toHaveBeenCalledTimes(1);
|
||||
expect(result).toBe(stats);
|
||||
expect(stats.totalChecks).toBe(11);
|
||||
expect(stats.totalUpChecks).toBe(7);
|
||||
expect(stats.totalDownChecks).toBe(4);
|
||||
expect(stats.currentStreak).toBe(1);
|
||||
expect(stats.currentStreakStatus).toBe("down");
|
||||
expect(stats.currentStreakStartedAt).toBe(now.getTime());
|
||||
expect(stats.lastCheckTimestamp).toBe(now.getTime());
|
||||
expect(stats.timeOfLastFailure).toBe(now.getTime());
|
||||
expect(stats.avgResponseTime).toBeCloseTo((200 * 10 + 400) / 11);
|
||||
expect(stats.uptimePercentage).toBeCloseTo(7 / 11);
|
||||
expect(stats.lastResponseTime).toBe(400);
|
||||
expect(stats.maxResponseTime).toBe(400);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import ApiError from "@/utils/ApiError.js";
|
||||
|
||||
describe("ApiError", () => {
|
||||
it("captures message and status", () => {
|
||||
const error = new ApiError("boom", 418);
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(error.message).toBe("boom");
|
||||
expect(error.status).toBe(418);
|
||||
expect(error.name).toBe("ApiError");
|
||||
});
|
||||
|
||||
it("defaults status to 500", () => {
|
||||
const error = new ApiError("failure");
|
||||
expect(error.status).toBe(500);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./",
|
||||
"outDir": "./dist",
|
||||
"module": "esnext",
|
||||
"moduleResolution": "nodenext",
|
||||
"types": ["jest", "node"]
|
||||
},
|
||||
"include": ["tests/**/*.ts", "src/**/*.ts"],
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
Reference in new issue
Block a user