add validation middleware and improve REST API structure
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { describe, it, expect } from "vitest";
|
||||
import request from "supertest";
|
||||
import express from "express";
|
||||
import { authLimiter, spotifyLimiter } from "../../../src/rest/middleware/rateLimit";
|
||||
|
||||
function createTestApp() {
|
||||
const app = express();
|
||||
app.set("trust proxy", 1);
|
||||
app.get("/auth-test", authLimiter, (_req, res) => res.status(200).send({ ok: true }));
|
||||
app.get("/spotify-test", spotifyLimiter, (_req, res) => res.status(200).send({ ok: true }));
|
||||
return app;
|
||||
}
|
||||
|
||||
async function hit(app: express.Express, path: string, times: number) {
|
||||
for (let i = 0; i < times; i++) {
|
||||
await request(app).get(path);
|
||||
}
|
||||
}
|
||||
|
||||
describe("RateLimit", () => {
|
||||
it("limits /auth-test after 30 Requests, returns http 429", async () => {
|
||||
const app = createTestApp();
|
||||
|
||||
// 30 are allowed
|
||||
await hit(app, "/auth-test", 30);
|
||||
|
||||
// afterwards, any request returns 429
|
||||
const res = await request(app).get("/auth-test");
|
||||
expect(res.status).toBe(429);
|
||||
|
||||
expect(res.headers["ratelimit-policy"]).toBeTruthy();
|
||||
});
|
||||
|
||||
it("limits /spotify-test after 60 requests, returns http 429", async () => {
|
||||
const app = createTestApp();
|
||||
|
||||
await hit(app, "/spotify-test", 60);
|
||||
|
||||
const res = await request(app).get("/spotify-test");
|
||||
expect(res.status).toBe(429);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,206 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { v, validateBody, validateParams, validateQuery } from "../../../src/rest/middleware/validate";
|
||||
|
||||
describe("v.isString", () => {
|
||||
it("accepts a simple string", () => {
|
||||
const res = v.isString()("abc");
|
||||
expect(res).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects non strings", () => {
|
||||
const res = v.isString()(123 as any);
|
||||
expect(res).toBe("must be a string");
|
||||
});
|
||||
|
||||
it("forces nonEmpty", () => {
|
||||
const res = v.isString({ nonEmpty: true })(" ");
|
||||
expect(res).toBe("must be a non-empty string");
|
||||
});
|
||||
|
||||
it("checks min/max length", () => {
|
||||
expect(v.isString({ min: 3 })("ab")).toBe("must be at least 3 chars");
|
||||
expect(v.isString({ max: 3 })("abcd")).toBe("must be at most 3 chars");
|
||||
expect(v.isString({ min: 2, max: 4 })("abc")).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v.isNumber", () => {
|
||||
it("accepts numbers, rejects NaN", () => {
|
||||
expect(v.isNumber()(10)).toBe(true);
|
||||
expect(v.isNumber()(Number.NaN)).toBe("must be a number");
|
||||
expect(v.isNumber()("10")).toBe("must be a number");
|
||||
});
|
||||
|
||||
it("checks integer min/max", () => {
|
||||
expect(v.isNumber({ integer: true })(1.2)).toBe("must be an integer");
|
||||
expect(v.isNumber({ min: 5 })(4)).toBe("must be >= 5");
|
||||
expect(v.isNumber({ max: 5 })(6)).toBe("must be <= 5");
|
||||
expect(v.isNumber({ integer: true, min: 0, max: 10 })(10)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("v.isBoolean", () => {
|
||||
it("accepts true/false", () => {
|
||||
expect(v.isBoolean()(true)).toBe(true);
|
||||
expect(v.isBoolean()(false)).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects other types", () => {
|
||||
expect(v.isBoolean()("true" )).toBe("must be a boolean");
|
||||
expect(v.isBoolean()(0)).toBe("must be a boolean");
|
||||
});
|
||||
});
|
||||
|
||||
describe("v.isEnum", () => {
|
||||
it("accepts only correct values", () => {
|
||||
const validator = v.isEnum(["A", "B", "C"] as const);
|
||||
expect(validator("A")).toBe(true);
|
||||
expect(validator("D")).toBe("must be one of: A, B, C");
|
||||
});
|
||||
});
|
||||
|
||||
describe("v.isArrayLength", () => {
|
||||
it("checks the exact length", () => {
|
||||
expect(v.isArrayLength(2)([1, 2])).toBe(true);
|
||||
expect(v.isArrayLength(2)([1])).toBe("must be an array of length 2");
|
||||
expect(v.isArrayLength(2)("not array")).toBe("must be an array of length 2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("v.isUrl", () => {
|
||||
it("accepts valid urls", () => {
|
||||
expect(v.isUrl()("https://example.com")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects invalid URLs and nom-strings", () => {
|
||||
expect(v.isUrl()("notaurl")).toBe("must be a valid URL");
|
||||
expect(v.isUrl()(123 as any)).toBe("must be a string URL");
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateBody Middleware", () => {
|
||||
const schema = {
|
||||
name: { required: true, validator: v.isString({ nonEmpty: true }) },
|
||||
age: { required: false, validator: v.isNumber({ min: 0, integer: true }) },
|
||||
};
|
||||
|
||||
function makeRes() {
|
||||
const res: any = {};
|
||||
res.status = vi.fn().mockReturnValue(res);
|
||||
res.send = vi.fn().mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
it("calls next(), when valid", async () => {
|
||||
const req: any = { body: { name: "Alice", age: 30 } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
const mw = validateBody(schema);
|
||||
mw(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
expect(res.status).not.toHaveBeenCalled();
|
||||
expect(res.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("sends 400 with missing/incorrect fields", async () => {
|
||||
const req: any = { body: { name: " ", age: -1 } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
const mw = validateBody(schema);
|
||||
mw(req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error: "Validation failed",
|
||||
details: expect.arrayContaining([
|
||||
"name must be a non-empty string",
|
||||
"age must be >= 0",
|
||||
]),
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("correctly checks missing fields", async () => {
|
||||
const req: any = { body: { /* no name */ } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
const mw = validateBody(schema);
|
||||
mw(req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error: "Validation failed",
|
||||
details: expect.arrayContaining(["name is required"]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateParams and validateQuery middleware", () => {
|
||||
const paramSchema = {
|
||||
id: { required: true, validator: v.isString({ nonEmpty: true }) },
|
||||
};
|
||||
const querySchema = {
|
||||
limit: { required: false, validator: v.isNumber({ integer: true, min: 1, max: 100 }) },
|
||||
};
|
||||
|
||||
function makeRes() {
|
||||
const res: any = {};
|
||||
res.status = vi.fn().mockReturnValue(res);
|
||||
res.send = vi.fn().mockReturnValue(res);
|
||||
return res;
|
||||
}
|
||||
|
||||
it("validateParams: ok -> next()", () => {
|
||||
const req: any = { params: { id: "abc" } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
validateParams(paramSchema)(req, res, next);
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("validateParams: error -> 400", () => {
|
||||
const req: any = { params: { id: " " } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
validateParams(paramSchema)(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("validateQuery: ok without limit", () => {
|
||||
const req: any = { query: { } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
validateQuery(querySchema)(req, res, next);
|
||||
expect(next).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("validateQuery: error on limit outside range", () => {
|
||||
const req: any = { query: { limit: 101 } };
|
||||
const res = makeRes();
|
||||
const next = vi.fn();
|
||||
|
||||
validateQuery(querySchema)(req, res, next);
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.send).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error: "Validation failed",
|
||||
details: expect.arrayContaining(["limit must be <= 100"]),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("jsonwebtoken", () => {
|
||||
return {
|
||||
default: {
|
||||
verify: vi.fn(),
|
||||
sign: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import jwt from "jsonwebtoken";
|
||||
import { JwtAuthenticator } from "../../src/utils/jwtAuthenticator";
|
||||
|
||||
describe("JwtAuthenticator", () => {
|
||||
const secret = "test-secret";
|
||||
let auth: JwtAuthenticator;
|
||||
|
||||
beforeEach(() => {
|
||||
auth = new JwtAuthenticator(secret);
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("verifyToken returns null when no token is passed", () => {
|
||||
expect(auth.verifyToken(undefined)).toBeNull();
|
||||
expect(jwt.verify).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("verifyToken returns DecodedToken when verify was successful ", () => {
|
||||
const payload = { username: "alice", id: "1", uuid: "u-1" };
|
||||
(jwt.verify as any).mockReturnValue(payload);
|
||||
|
||||
const res = auth.verifyToken("valid.jwt.token");
|
||||
expect(jwt.verify).toHaveBeenCalledWith("valid.jwt.token", secret);
|
||||
expect(res).toEqual(payload);
|
||||
});
|
||||
|
||||
it("verifyToken returns null when verify throws error", () => {
|
||||
const spy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
(jwt.verify as any).mockImplementation(() => {
|
||||
throw new Error("invalid");
|
||||
});
|
||||
|
||||
const res = auth.verifyToken("broken.token");
|
||||
expect(res).toBeNull();
|
||||
expect(spy).toHaveBeenCalled();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
it("generateToken signs payload with secret", () => {
|
||||
(jwt.sign as any).mockReturnValue("signed.jwt");
|
||||
const payload = { username: "bob" } as any;
|
||||
|
||||
const token = auth.generateToken(payload);
|
||||
expect(jwt.sign).toHaveBeenCalledWith(payload, secret);
|
||||
expect(token).toBe("signed.jwt");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const { hashMock, compareMock } = vi.hoisted(() => ({
|
||||
hashMock: vi.fn(),
|
||||
compareMock: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
vi.mock("bcrypt", () => {
|
||||
return {
|
||||
hash: hashMock,
|
||||
compare: compareMock,
|
||||
default: {
|
||||
hash: hashMock,
|
||||
compare: compareMock,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
import { PasswordUtils } from "../../src/utils/passwordUtils";
|
||||
|
||||
describe("PasswordUtils", () => {
|
||||
beforeEach(() => {
|
||||
hashMock.mockReset();
|
||||
compareMock.mockReset();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("hashPassword uses bcrypt.hash with 10 saltrounds", async () => {
|
||||
hashMock.mockResolvedValue("hashed");
|
||||
const res = await PasswordUtils.hashPassword("secret");
|
||||
expect(hashMock).toHaveBeenCalledWith("secret", 10);
|
||||
expect(res).toBe("hashed");
|
||||
});
|
||||
|
||||
it("comparePassword uses bcrypt.compare", async () => {
|
||||
compareMock.mockResolvedValue(true);
|
||||
const ok = await PasswordUtils.comparePassword("secret", "hashed");
|
||||
expect(compareMock).toHaveBeenCalledWith("secret", "hashed");
|
||||
expect(ok).toBe(true);
|
||||
});
|
||||
|
||||
describe("validatePassword", () => {
|
||||
it("fails when password too short", () => {
|
||||
const res = PasswordUtils.validatePassword("A1!");
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toMatch(/mindestens 8 Zeichen/);
|
||||
});
|
||||
|
||||
it("fails without capital letter", () => {
|
||||
const res = PasswordUtils.validatePassword("password1!");
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toMatch(/Großbuchstaben/);
|
||||
});
|
||||
|
||||
it("fails without uncapitalized letter", () => {
|
||||
const res = PasswordUtils.validatePassword("PASSWORD1!");
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toMatch(/Kleinbuchstaben/);
|
||||
});
|
||||
|
||||
it("fails without number", () => {
|
||||
const res = PasswordUtils.validatePassword("Password!");
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toMatch(/Zahl/);
|
||||
});
|
||||
|
||||
it("fails without special characters", () => {
|
||||
const res = PasswordUtils.validatePassword("Password1");
|
||||
expect(res.valid).toBe(false);
|
||||
expect(res.message).toMatch(/Sonderzeichen/);
|
||||
});
|
||||
|
||||
it("accepts valid password", () => {
|
||||
const res = PasswordUtils.validatePassword("ValidPassword1!");
|
||||
expect(res.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("../../src/utils/jwtAuthenticator", () => {
|
||||
return {
|
||||
JwtAuthenticator: vi.fn().mockImplementation(() => ({
|
||||
verifyToken: mockVerifyToken,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const mockVerifyToken = vi.fn();
|
||||
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { verifyClient } from "../../src/utils/verifyClient";
|
||||
|
||||
describe("verifyClient", () => {
|
||||
const cb = vi.fn();
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
function makeReq(authHeader?: string) {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authHeader) headers["authorization"] = authHeader;
|
||||
|
||||
// socket infos just for log
|
||||
const socket: any = { remoteAddress: "127.0.0.1", remotePort: 12345 };
|
||||
return { headers, socket } as unknown as IncomingMessage & { [k: string]: any };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cb.mockReset();
|
||||
mockVerifyToken.mockReset();
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("accepts connections with valid token and sets payload", () => {
|
||||
const req = makeReq("Bearer valid.jwt");
|
||||
mockVerifyToken.mockReturnValue({ sub: "user-1" });
|
||||
|
||||
verifyClient(req, cb);
|
||||
|
||||
expect(mockVerifyToken).toHaveBeenCalledWith("valid.jwt");
|
||||
expect(cb).toHaveBeenCalledWith(true);
|
||||
expect((req as any).payload).toEqual({ sub: "user-1" });
|
||||
});
|
||||
|
||||
it("Rejects connection if no Authorization header is set", () => {
|
||||
const req = makeReq(undefined);
|
||||
mockVerifyToken.mockReturnValue(null);
|
||||
|
||||
verifyClient(req, cb);
|
||||
|
||||
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects connection, if token is invalid", () => {
|
||||
const req = makeReq("Bearer bad.jwt");
|
||||
mockVerifyToken.mockReturnValue(null);
|
||||
|
||||
verifyClient(req, cb);
|
||||
|
||||
expect(mockVerifyToken).toHaveBeenCalledWith("bad.jwt");
|
||||
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
||||
});
|
||||
|
||||
it("extracts token correctly after 'Bearer ' prefix", () => {
|
||||
const expectedToken = " fancy.token.with.spaces ";
|
||||
const req = makeReq(`Bearer ${expectedToken}`);
|
||||
mockVerifyToken.mockReturnValue({ ok: true });
|
||||
|
||||
verifyClient(req, cb);
|
||||
|
||||
expect(mockVerifyToken).toHaveBeenCalledWith(expectedToken);
|
||||
expect(cb).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
import { describe, it, expect, vi } from "vitest";
|
||||
import { getEventListeners } from "../../../../src/utils/websocket/websocketCustomEvents/websocketEventUtils";
|
||||
import { WebsocketEventType } from "../../../../src/utils/websocket/websocketCustomEvents/websocketEventType";
|
||||
|
||||
describe("websocketEventUtils.getEventListeners", () => {
|
||||
function makeWs() {
|
||||
return {
|
||||
user: {
|
||||
timezone: "Europe/Berlin",
|
||||
lastState: { global: { mode: "idle", brightness: 42 } },
|
||||
},
|
||||
send: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
it("returns a list of event-handlers incl. GET_STATE/GET_SETTINGS", async () => {
|
||||
const ws: any = makeWs();
|
||||
const listeners = getEventListeners(ws);
|
||||
|
||||
expect(Array.isArray(listeners)).toBe(true);
|
||||
expect(listeners.length).toBeGreaterThan(0);
|
||||
|
||||
const byType = Object.fromEntries(listeners.map(l => [l.event, l]));
|
||||
|
||||
expect(byType[WebsocketEventType.GET_STATE]).toBeTruthy();
|
||||
byType[WebsocketEventType.GET_STATE].handler({});
|
||||
expect(ws.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: "STATE", payload: ws.user.lastState }),
|
||||
{ binary: false },
|
||||
);
|
||||
|
||||
ws.send.mockClear();
|
||||
expect(byType[WebsocketEventType.GET_SETTINGS]).toBeTruthy();
|
||||
byType[WebsocketEventType.GET_SETTINGS].handler({});
|
||||
expect(ws.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: "SETTINGS", payload: { timezone: ws.user.timezone } }),
|
||||
{ binary: false },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,112 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
const { heartbeatSpy, getUserByUUID } = vi.hoisted(() => ({
|
||||
heartbeatSpy: vi.fn(),
|
||||
getUserByUUID: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../../../src/utils/websocket/websocketServerHeartbeatInterval", () => {
|
||||
return {
|
||||
heartbeat: () => heartbeatSpy,
|
||||
};
|
||||
});
|
||||
|
||||
const userObj = {
|
||||
name: "tester",
|
||||
uuid: "uuid-1",
|
||||
timezone: "Europe/Berlin",
|
||||
location: "Berlin",
|
||||
lastState: { global: { mode: "idle", brightness: 50 } },
|
||||
};
|
||||
|
||||
vi.mock("../../../src/db/services/db/UserService", () => {
|
||||
return {
|
||||
UserService: {
|
||||
create: vi.fn().mockResolvedValue({
|
||||
getUserByUUID,
|
||||
}),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
class FakeWSS {
|
||||
clients = new Set<any>();
|
||||
handlers = new Map<string, Function>();
|
||||
on(event: string, handler: Function) {
|
||||
this.handlers.set(event, handler);
|
||||
}
|
||||
emit(event: string, ...args: any[]) {
|
||||
const h = this.handlers.get(event);
|
||||
if (h) h(...args);
|
||||
}
|
||||
}
|
||||
|
||||
import { WebsocketServerEventHandler } from "../../../src/utils/websocket/websocketServerEventHandler";
|
||||
|
||||
describe("WebsocketServerEventHandler", () => {
|
||||
let wss: FakeWSS;
|
||||
|
||||
beforeEach(() => {
|
||||
wss = new FakeWSS();
|
||||
heartbeatSpy.mockReset();
|
||||
getUserByUUID.mockReset();
|
||||
getUserByUUID.mockResolvedValue(userObj);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("enableConnectionEvent sets user/payload/isAlive/asyncUpdates and calls callback", async () => {
|
||||
const handler = new WebsocketServerEventHandler(wss as any);
|
||||
const cb = vi.fn();
|
||||
|
||||
const done = new Promise<void>((resolve) => {
|
||||
cb.mockImplementation(() => resolve());
|
||||
});
|
||||
|
||||
handler.enableConnectionEvent(cb);
|
||||
|
||||
const req = { payload: { uuid: "uuid-1" } };
|
||||
const ws: any = {};
|
||||
|
||||
wss.emit("connection", ws, req);
|
||||
|
||||
await done;
|
||||
|
||||
expect(getUserByUUID).toHaveBeenCalledWith("uuid-1");
|
||||
expect(ws.user).toEqual(userObj);
|
||||
expect(ws.payload).toEqual(req.payload);
|
||||
expect(ws.isAlive).toBe(true);
|
||||
expect(ws.asyncUpdates).toBeInstanceOf(Map);
|
||||
expect(cb).toHaveBeenCalledWith(ws, req);
|
||||
});
|
||||
|
||||
it("enableHeartbeat starts interval and calls heartbeat()", () => {
|
||||
vi.useFakeTimers();
|
||||
const handler = new WebsocketServerEventHandler(wss as any);
|
||||
|
||||
const id = handler.enableHeartbeat(1000);
|
||||
expect(["number", "object"]).toContain(typeof id);
|
||||
|
||||
vi.advanceTimersByTime(3000);
|
||||
expect(heartbeatSpy).toHaveBeenCalledTimes(3);
|
||||
|
||||
clearInterval(id);
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("enableCloseEvent registers Listener and calls callback on close", () => {
|
||||
const handler = new WebsocketServerEventHandler(wss as any);
|
||||
const cb = vi.fn();
|
||||
|
||||
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
handler.enableCloseEvent(cb);
|
||||
|
||||
wss.emit("close");
|
||||
|
||||
expect(cb).toHaveBeenCalledTimes(1);
|
||||
expect(logSpy).toHaveBeenCalledWith("WebSocket server closed");
|
||||
logSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,46 @@
|
||||
import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
|
||||
import {heartbeat} from "../../../src/utils/websocket/websocketServerHeartbeatInterval";
|
||||
|
||||
describe("heartbeat(wss)", () => {
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
beforeEach(() => {
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
function makeClient({
|
||||
isAlive,
|
||||
username,
|
||||
}: {
|
||||
isAlive: boolean;
|
||||
username: string;
|
||||
}) {
|
||||
return {
|
||||
isAlive,
|
||||
payload: {username},
|
||||
ping: vi.fn(),
|
||||
terminate: vi.fn(),
|
||||
} as any;
|
||||
}
|
||||
|
||||
it("terminated dead clients and pings alive ones, sets isAlive to false", () => {
|
||||
const alive = makeClient({isAlive: true, username: "alive-user"});
|
||||
const dead = makeClient({isAlive: false, username: "dead-user"});
|
||||
|
||||
const wss = {
|
||||
clients: new Set<any>([alive, dead]),
|
||||
} as any;
|
||||
|
||||
const hb = heartbeat(wss);
|
||||
hb();
|
||||
|
||||
expect(dead.terminate).toHaveBeenCalledTimes(1);
|
||||
expect(alive.ping).toHaveBeenCalledTimes(1);
|
||||
expect(alive.isAlive).toBe(false);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user