refactor to inject userservice

This commit is contained in:
StarAppeal
2025-09-18 21:13:40 +02:00
parent da19a0aaf1
commit c7d4454359
19 changed files with 290 additions and 230 deletions
+78 -66
View File
@@ -1,80 +1,92 @@
import express from "express";
import {ExtendedWebSocketServer} from "./websocket";
import {RestWebSocket} from "./rest/restWebSocket";
import {RestUser} from "./rest/restUser";
import {authenticateJwt} from "./rest/middleware/authenticateJwt";
import {JwtTokenPropertiesExtractor} from "./rest/jwtTokenPropertiesExtractor";
import { ExtendedWebSocketServer } from "./websocket";
import { RestWebSocket } from "./rest/restWebSocket";
import { RestUser } from "./rest/restUser";
import { authenticateJwt } from "./rest/middleware/authenticateJwt";
import { JwtTokenPropertiesExtractor } from "./rest/jwtTokenPropertiesExtractor";
import cors from "cors";
import {SpotifyTokenGenerator} from "./rest/spotifyTokenGenerator";
import {RestAuth} from "./rest/auth";
import { SpotifyTokenGenerator } from "./rest/spotifyTokenGenerator";
import { RestAuth } from "./rest/auth";
import { config } from "./config";
import cookieParser from 'cookie-parser';
import {authLimiter, spotifyLimiter} from "./rest/middleware/rateLimit";
import {cookieJwtAuth} from "./rest/middleware/cookieAuth";
import { authLimiter, spotifyLimiter } from "./rest/middleware/rateLimit";
import { cookieJwtAuth } from "./rest/middleware/cookieAuth";
import { UserService } from "./db/services/db/UserService";
const app = express();
const port = config.port;
export async function startServer() {
const app = express();
const port = config.port;
app.set("trust proxy", 1);
app.use(cookieParser());
app.set("trust proxy", 1);
app.use(cookieParser());
app.use(cors({
origin: config.cors.origin,
credentials: config.cors.credentials,
}));
app.use(cors({
origin: config.cors.origin,
credentials: config.cors.credentials,
}));
app.use((_req, res, next) => {
res.set({
"X-DNS-Prefetch-Control": "off",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer",
"Permissions-Policy": "geolocation=()",
app.use((_req, res, next) => {
res.set({
"X-DNS-Prefetch-Control": "off",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
"Referrer-Policy": "no-referrer",
"Permissions-Policy": "geolocation=()",
});
next();
});
next();
});
app.use(express.json({limit: "2mb"}));
app.use(express.json({ limit: "2mb" }));
app.get("/api/healthz", (_req, res) => res.status(200).send({ status: "ok" }));
app.get("/api/healthz", (_req, res) => res.status(200).send({status: "ok"}));
console.log("Connecting to database and creating UserService...");
const userService = await UserService.create();
console.log("UserService created successfully.");
const server = app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
const webSocketServer = new ExtendedWebSocketServer(server);
const restWebSocket = new RestWebSocket(webSocketServer);
const restUser = new RestUser();
const auth = new RestAuth();
const jwtTokenPropertiesExtractor = new JwtTokenPropertiesExtractor();
const spotify = new SpotifyTokenGenerator();
app.use("/api/auth", authLimiter, auth.createRouter());
app.use(cookieJwtAuth);
app.use("/api/spotify", authenticateJwt, spotifyLimiter, spotify.createRouter());
app.use("/api/websocket", authenticateJwt, restWebSocket.createRouter());
app.use("/api/user", authenticateJwt, restUser.createRouter());
app.use(
"/api/jwt",
authenticateJwt,
jwtTokenPropertiesExtractor.createRouter(),
);
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res
.status(err?.status || 500)
.send({ ok: false, data: {}, error: err?.message || "Internal Server Error" });
});
process.on("SIGTERM", () => {
server.close(() => {
console.log("HTTP server closed");
process.exit(0);
const server = app.listen(port, () => {
console.log(`Server is running on port ${port}`);
});
});
// Export the app for testing purposes
export default app; // optional
const webSocketServer = new ExtendedWebSocketServer(server, userService);
const restWebSocket = new RestWebSocket(webSocketServer);
const restUser = new RestUser(userService);
const auth = new RestAuth(userService);
const jwtTokenPropertiesExtractor = new JwtTokenPropertiesExtractor();
const spotify = new SpotifyTokenGenerator();
app.use("/api/auth", authLimiter, auth.createRouter());
app.use(cookieJwtAuth);
app.use("/api/spotify", authenticateJwt, spotifyLimiter, spotify.createRouter());
app.use("/api/websocket", authenticateJwt, restWebSocket.createRouter());
app.use("/api/user", authenticateJwt, restUser.createRouter());
app.use(
"/api/jwt",
authenticateJwt,
jwtTokenPropertiesExtractor.createRouter(),
);
app.use((err: any, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
console.error(err);
res
.status(err?.status || 500)
.send({ ok: false, data: {}, error: err?.message || "Internal Server Error" });
});
process.on("SIGTERM", () => {
console.log("SIGTERM signal received: closing HTTP server");
server.close(() => {
console.log("HTTP server closed");
process.exit(0);
});
});
return { app, server };
}
if (process.env.NODE_ENV !== 'test') {
startServer().catch(error => {
console.error("Fatal error during server startup:", error);
process.exit(1);
});
}
+9 -5
View File
@@ -9,6 +9,12 @@ import {validateBody, v} from "./middleware/validate";
import {ok, badRequest, unauthorized, created, conflict, notFound} from "./utils/responses";
export class RestAuth {
private readonly userService: UserService;
constructor(userService: UserService) {
this.userService = userService;
}
public createRouter() {
const router = express.Router();
@@ -24,9 +30,8 @@ export class RestAuth {
const {username, password, timezone, location} = req.body as {
username: string; password: string; timezone: string; location: string;
};
const userService = await UserService.create();
if (await userService.existsUserByName(username)) {
if (await this.userService.existsUserByName(username)) {
return conflict(res, "Username already exists", {field: "username", code: "USERNAME_TAKEN"});
}
@@ -52,7 +57,7 @@ export class RestAuth {
location
};
const result = await userService.createUser(newUser);
const result = await this.userService.createUser(newUser);
return created(res, {user: result});
})
);
@@ -65,8 +70,7 @@ export class RestAuth {
}),
asyncHandler(async (req, res) => {
const {username, password} = req.body as { username: string; password: string };
const userService = await UserService.create();
const user = await userService.getUserAuthByName(username);
const user = await this.userService.getUserAuthByName(username);
if (!user) {
return notFound(res, "User not found", {field: "username", code: "INVALID_USER"});
+16 -19
View File
@@ -1,23 +1,20 @@
import type {NextFunction, Request, Response} from "express";
import {UserService} from "../../db/services/db/UserService";
import {notFound} from "../utils/responses";
import type { NextFunction, Request, Response } from "express";
import type { UserService } from "../../db/services/db/UserService";
import { notFound } from "../utils/responses";
export function isAdmin(userService: UserService) {
return async (req: Request, res: Response, next: NextFunction) => {
try {
const payload = req.payload;
const user = await userService.getUserByUUID(payload.uuid);
export async function isAdmin(
req: Request,
res: Response,
next: NextFunction) {
try {
const payload = req.payload;
const userService = await UserService.create();
const user = await userService.getUserByUUID(payload.uuid);
if (user && user.config.isAdmin) {
next();
} else {
return notFound(res);
if (user?.config?.isAdmin) {
return next();
} else {
return notFound(res);
}
} catch (error) {
return next(error);
}
} catch (error) {
next(error);
}
};
}
+18 -17
View File
@@ -7,18 +7,23 @@ import {badRequest, ok} from "./utils/responses";
import {isAdmin} from "./middleware/isAdmin";
export class RestUser {
private readonly userService: UserService;
constructor(userService: UserService) {
this.userService = userService;
}
public createRouter() {
const router = express.Router();
router.get("/",isAdmin, asyncHandler(async (_req, res) => {
const userService = await UserService.create();
const users = await userService.getAllUsers();
router.get("/",isAdmin(this.userService), asyncHandler(async (_req, res) => {
const users = await this.userService.getAllUsers();
return ok(res, { users });
}));
router.get("/me", asyncHandler(async (req, res) => {
const userService = await UserService.create();
const user = await userService.getUserByUUID(req.payload.uuid);
const user = await this.userService.getUserByUUID(req.payload.uuid);
return ok(res, user);
}));
@@ -31,8 +36,7 @@ export class RestUser {
expirationDate: { required: true, validator: v.isString({ nonEmpty: true }) },
}),
asyncHandler(async (req, res) => {
const userService = await UserService.create();
const user = await userService.getUserByUUID(req.payload.uuid);
const user = await this.userService.getUserByUUID(req.payload.uuid);
if (!user) {
return badRequest(res, "User not found");
}
@@ -48,19 +52,18 @@ export class RestUser {
expirationDate: new Date(expirationDate),
};
await userService.updateUser(user);
await this.userService.updateUser(user);
return ok(res, { message: "Spotify Config erfolgreich geändert" });
})
);
router.delete("/me/spotify", asyncHandler(async (req, res) => {
const userService = await UserService.create();
const user = await userService.getUserByUUID(req.payload.uuid);
const user = await this.userService.getUserByUUID(req.payload.uuid);
if (!user) {
return badRequest(res, "User not found");
}
const updated = await userService.clearSpotifyConfigByUUID(req.payload.uuid);
const updated = await this.userService.clearSpotifyConfigByUUID(req.payload.uuid);
return ok(res, { user: updated });
}));
@@ -71,8 +74,7 @@ export class RestUser {
passwordConfirmation: { required: true, validator: v.isString({ nonEmpty: true, min: 8 }) },
}),
asyncHandler(async (req, res) => {
const userService = await UserService.create();
const user = await userService.getUserByUUID(req.payload.uuid);
const user = await this.userService.getUserByUUID(req.payload.uuid);
if (!user) {
return badRequest(res, "User not found");
}
@@ -90,7 +92,7 @@ export class RestUser {
user.password = await PasswordUtils.hashPassword(password);
await userService.updateUser(user);
await this.userService.updateUser(user);
return ok(res, { message: "Passwort erfolgreich geändert" });
})
);
@@ -100,11 +102,10 @@ export class RestUser {
validateParams({
id: { required: true, validator: v.isString({ nonEmpty: true }) },
}),
isAdmin,
isAdmin(this.userService),
asyncHandler(async (req, res) => {
const userService = await UserService.create();
const id = req.params.id;
const user = await userService.getUserById(id);
const user = await this.userService.getUserById(id);
if (!user) {
return badRequest(res, `Unable to find matching document with id: ${id}`);
@@ -0,0 +1,13 @@
import {ExtendedWebSocket} from "../../../interfaces/extendedWebsocket";
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {UserService} from "../../../db/services/db/UserService";
export abstract class CustomWebsocketEventUserService extends CustomWebsocketEvent {
protected readonly userService: UserService;
public constructor(ws: ExtendedWebSocket, userService: UserService) {
super(ws);
this.userService = userService;
}
}
@@ -1,8 +1,8 @@
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {WebsocketEventType} from "./websocketEventType";
import {SpotifyTokenService} from "../../../db/services/spotifyTokenService";
import {UserService} from "../../../db/services/db/UserService";
import {getCurrentlyPlaying} from "../../../db/services/spotifyApiService";
import {CustomWebsocketEventUserService} from "./customWebsocketEventUserService";
export const SpotifyAsyncUpdateEvent = "SPOTIFY_UPDATE";
@@ -26,7 +26,7 @@ export class GetSpotifyUpdatesEvent extends CustomWebsocketEvent {
}
}
export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEvent {
export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEventUserService {
event = WebsocketEventType.GET_SINGLE_SPOTIFY_UPDATE;
@@ -56,8 +56,7 @@ export class GetSingleSpotifyUpdateEvent extends CustomWebsocketEvent {
expirationDate: new Date(Date.now() + token.expires_in * 1000),
scope: token.scope,
};
const userService = await UserService.create();
await userService.updateUser(user);
await this.userService.updateUser(user);
console.log("Token refreshed and database updated");
}
const musicData = await getCurrentlyPlaying(user.spotifyConfig!.accessToken);
@@ -1,11 +1,11 @@
import {WebsocketEventType} from "./websocketEventType";
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {IUser} from "../../../db/models/user";
import {UserService} from "../../../db/services/db/UserService";
import {CustomWebsocketEventUserService} from "./customWebsocketEventUserService";
export const UserAsyncUpdateEvent = "USER_UPDATE";
export class UpdateUserEvent extends CustomWebsocketEvent {
export class UpdateUserEvent extends CustomWebsocketEventUserService {
event = WebsocketEventType.UPDATE_USER;
handler = async () => {
@@ -16,8 +16,7 @@ export class UpdateUserEvent extends CustomWebsocketEvent {
}
this.ws.asyncUpdates.set(UserAsyncUpdateEvent, setInterval(async () => {
const userService = await UserService.create();
const user = await userService.getUserByUUID(this.ws.payload.uuid);
const user = await this.userService.getUserByUUID(this.ws.payload.uuid);
this.ws.emit(WebsocketEventType.UPDATE_USER_SINGLE, user);
}, 1000 * 15));
@@ -9,18 +9,19 @@ import {StopWeatherUpdatesEvent} from "./stopWeatherUpdatesEvent";
import {UpdateUserEvent, UpdateUserSingleEvent} from "./updateUserEvent";
import {CustomWebsocketEvent} from "./customWebsocketEvent";
import {StopUpdateUserEvent} from "./stopUpdateUserEvent";
import {UserService} from "../../../db/services/db/UserService";
export function getEventListeners(ws: ExtendedWebSocket): CustomWebsocketEvent[] {
export function getEventListeners(ws: ExtendedWebSocket, userService: UserService): CustomWebsocketEvent[] {
return [
new GetStateEvent(ws),
new GetSettingsEvent(ws),
new GetSingleSpotifyUpdateEvent(ws),
new GetSingleSpotifyUpdateEvent(ws, userService),
new GetSpotifyUpdatesEvent(ws),
new StopSpotifyUpdatesEvent(ws),
new GetSingleWeatherUpdateEvent(ws),
new GetWeatherUpdatesEvent(ws),
new StopWeatherUpdatesEvent(ws),
new UpdateUserEvent(ws),
new UpdateUserEvent(ws, userService),
new UpdateUserSingleEvent(ws),
new StopUpdateUserEvent(ws),
new ErrorEvent(ws)
+9 -3
View File
@@ -1,8 +1,10 @@
import {ExtendedWebSocket} from "../../interfaces/extendedWebsocket";
import {CustomWebsocketEvent} from "./websocketCustomEvents/customWebsocketEvent";
import {UserService} from "../../db/services/db/UserService";
import {getEventListeners} from "./websocketCustomEvents/websocketEventUtils";
export class WebsocketEventHandler {
constructor(private webSocket: ExtendedWebSocket) {
constructor(private webSocket: ExtendedWebSocket, private userService: UserService) {
}
public enableErrorEvent() {
@@ -42,8 +44,12 @@ export class WebsocketEventHandler {
);
}
public registerCustomEvent(customWebsocketEvent: CustomWebsocketEvent) {
// bind needed?
public registerCustomEvents() {
const events = getEventListeners(this.webSocket, this.userService);
events.forEach(this.registerCustomEvent, this);
}
private registerCustomEvent(customWebsocketEvent: CustomWebsocketEvent) {
this.webSocket.on(customWebsocketEvent.event, customWebsocketEvent.handler.bind(customWebsocketEvent));
}
@@ -6,9 +6,11 @@ import {UserService} from "../../db/services/db/UserService";
export class WebsocketServerEventHandler {
private readonly heartbeat: () => void;
private readonly userService: UserService;
constructor(private webSocketServer: WebSocketServer) {
constructor(private webSocketServer: WebSocketServer, userService: UserService) {
this.heartbeat = heartbeat(this.webSocketServer);
this.userService = userService;
}
public enableConnectionEvent(
@@ -17,7 +19,7 @@ export class WebsocketServerEventHandler {
this.webSocketServer.on(
"connection",
async (ws: ExtendedWebSocket, request: ExtendedIncomingMessage) => {
const user = await (await UserService.create()).getUserByUUID(request.payload.uuid);
const user = await this.userService.getUserByUUID(request.payload.uuid);
ws.user = user!;
+8 -5
View File
@@ -5,18 +5,21 @@ import {ExtendedWebSocket} from "./interfaces/extendedWebsocket";
import {DecodedToken} from "./interfaces/decodedToken";
import {WebsocketServerEventHandler} from "./utils/websocket/websocketServerEventHandler";
import {WebsocketEventHandler} from "./utils/websocket/websocketEventHandler";
import {getEventListeners} from "./utils/websocket/websocketCustomEvents/websocketEventUtils";
import {WebsocketEventType} from "./utils/websocket/websocketCustomEvents/websocketEventType";
import {UserService} from "./db/services/db/UserService";
export class ExtendedWebSocketServer {
private readonly _wss: WebSocketServer;
private readonly userService: UserService;
constructor(server: Server) {
constructor(server: Server, userService: UserService) {
this._wss = new WebSocketServer({
server,
verifyClient: (info, callback) => verifyClient(info.req, callback),
});
this.userService = userService;
this.setupWebSocket();
}
@@ -50,9 +53,9 @@ export class ExtendedWebSocketServer {
}
private setupWebSocket() {
const serverEventHandler = new WebsocketServerEventHandler(this.wss);
const serverEventHandler = new WebsocketServerEventHandler(this.wss, this.userService);
serverEventHandler.enableConnectionEvent((ws) => {
const socketEventHandler = new WebsocketEventHandler(ws);
const socketEventHandler = new WebsocketEventHandler(ws, this.userService);
console.log("WebSocket client connected");
@@ -61,7 +64,7 @@ export class ExtendedWebSocketServer {
socketEventHandler.enableMessageEvent();
// Register custom events
getEventListeners(ws).forEach(socketEventHandler.registerCustomEvent, socketEventHandler);
socketEventHandler.registerCustomEvents();
socketEventHandler.enableDisconnectEvent(() => {
console.log("User disconnected");
+21 -11
View File
@@ -1,7 +1,8 @@
import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
import request from "supertest";
import express from "express";
import {authLimiter} from "../src/rest/middleware/rateLimit";
import http from "http";
import { authLimiter } from "../src/rest/middleware/rateLimit";
vi.mock("../src/db/services/db/database.service", () => ({
connectToDatabase: vi.fn().mockResolvedValue(undefined),
@@ -20,7 +21,6 @@ vi.mock("../src/config", () => ({
vi.mock("../src/rest/middleware/rateLimit", async (importOriginal) => {
const original = await importOriginal<typeof import("../src/rest/middleware/rateLimit")>();
return {
...original,
authLimiter: vi.fn((req, res, next) => next()),
@@ -29,12 +29,25 @@ vi.mock("../src/rest/middleware/rateLimit", async (importOriginal) => {
});
let app: express.Application;
let server: http.Server;
beforeAll(async () => {
const indexModule = await import("../src/index");
app = indexModule.default;
const { startServer } = await import("../src/index");
const instances = await startServer();
app = instances.app;
server = instances.server;
});
afterAll(async () => {
await new Promise<void>((resolve) => {
if (server) {
server.close(() => resolve());
} else {
resolve();
}
});
});
describe("Express App Integration Test", () => {
@@ -63,24 +76,21 @@ describe("Express App Integration Test", () => {
expect(response.headers['referrer-policy']).toBe('no-referrer');
});
it("should protect a route with the authenticateJwt middleware", async () => {
const response = await request(app).get("/api/user/me").expect(401);
expect(response.text).toBe("Unauthorized");
it("should protect a route with authentication middleware", async () => {
await request(app).get("/api/user/me").expect(401);
});
it("should apply the auth rate limiter to an auth route", async () => {
await request(app).post("/api/auth/login").send({}).expect(400);
await request(app).post("/api/auth/login").send({});
expect(authLimiter).toHaveBeenCalledOnce();
});
it("should NOT apply the auth rate limiter to a non-auth route", async () => {
await request(app).get("/api/healthz").expect(200);
expect(authLimiter).not.toHaveBeenCalled();
});
it("should return a 404 for an unknown route", async () => {
await request(app).get("/api/this-route-does-not-exist").expect(404);
});
});
+1 -3
View File
@@ -2,7 +2,6 @@ import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
import request from "supertest";
import express from "express";
import {RestAuth} from "../../src/rest/auth";
import {UserService} from "../../src/db/services/db/UserService";
import {JwtAuthenticator} from "../../src/utils/jwtAuthenticator";
import {PasswordUtils} from "../../src/utils/passwordUtils";
import {createMockJwtAuthenticator, createMockUserService, createPublicTestApp} from "../helpers/testSetup";
@@ -40,7 +39,6 @@ describe("RestAuth", () => {
vi.clearAllMocks();
mockUserService = createMockUserService();
vi.mocked(UserService.create).mockResolvedValue(mockUserService);
mockPasswordUtils = vi.mocked(PasswordUtils);
mockCrypto = vi.mocked(crypto);
@@ -48,7 +46,7 @@ describe("RestAuth", () => {
mockJwtAuthenticator = createMockJwtAuthenticator();
vi.mocked(JwtAuthenticator).mockImplementation(() => mockJwtAuthenticator);
const restAuth = new RestAuth();
const restAuth = new RestAuth(mockUserService);
app = createPublicTestApp(restAuth.createRouter(), "/auth");
process.env.SECRET_KEY = "test-secret-key";
+4 -7
View File
@@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vi
import { Request, Response, NextFunction } from "express";
import { isAdmin } from "../../../src/rest/middleware/isAdmin";
import { createMockUserService } from "../../helpers/testSetup";
import { UserService } from "../../../src/db/services/db/UserService";
import { notFound } from "../../../src/rest/utils/responses";
vi.mock("../../../src/db/services/db/UserService", () => ({
@@ -27,7 +26,6 @@ describe("isAdmin middleware", () => {
vi.clearAllMocks();
mockedUserService = createMockUserService();
vi.mocked(UserService.create).mockResolvedValue(mockedUserService);
req = {
payload: { uuid, username: "username", id: ""}
@@ -50,9 +48,8 @@ describe("isAdmin middleware", () => {
const mockUser = { uuid, config: { isAdmin: true } };
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
await isAdmin(req as Request, res, next);
await isAdmin(mockedUserService)(req as Request, res, next);
expect(UserService.create).toHaveBeenCalledOnce();
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(next).toHaveBeenCalledOnce();
expect(res.status).not.toHaveBeenCalled();
@@ -66,7 +63,7 @@ describe("isAdmin middleware", () => {
const mockUser = { uuid, config: { isAdmin: false } };
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
await isAdmin(req as Request, res, next);
await isAdmin(mockedUserService)(req as Request, res, next);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(notFound).toHaveBeenCalledWith(res);
@@ -76,7 +73,7 @@ describe("isAdmin middleware", () => {
it("should call notFound if user does not exist", async () => {
mockedUserService.getUserByUUID.mockResolvedValue(null);
await isAdmin(req as Request, res, next);
await isAdmin(mockedUserService)(req as Request, res, next);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(uuid);
expect(notFound).toHaveBeenCalledWith(res);
@@ -87,7 +84,7 @@ describe("isAdmin middleware", () => {
const dbError = new Error("Database error");
mockedUserService.getUserByUUID.mockRejectedValue(dbError);
await isAdmin(req as Request, res, next);
await isAdmin(mockedUserService)(req as Request, res, next);
expect(next).toHaveBeenCalledWith(expect.objectContaining({ message: 'Database error' }));
});
+35 -34
View File
@@ -2,7 +2,7 @@ import {describe, it, expect, vi, beforeEach, afterEach} from "vitest";
import request from "supertest";
import {RestUser} from "../../src/rest/restUser";
import {setupTestEnvironment, type TestEnvironment} from "../helpers/testSetup";
import {createMockUserService, setupTestEnvironment, type TestEnvironment} from "../helpers/testSetup";
vi.mock("../../src/db/services/db/UserService", () => ({
UserService: {
@@ -24,11 +24,12 @@ describe("RestUser", () => {
const requestingUserUUID = "test-user-uuid";
const adminUser = { uuid: requestingUserUUID, config: { isAdmin: true } };
const nonAdminUser = { uuid: requestingUserUUID, config: { isAdmin: false } };
const mockedUserService = createMockUserService();
beforeEach(() => {
vi.clearAllMocks();
const restUser = new RestUser();
const restUser = new RestUser(mockedUserService);
testEnv = setupTestEnvironment(restUser.createRouter(), "/user");
});
@@ -44,14 +45,14 @@ describe("RestUser", () => {
uuid: "test-user-uuid"
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.get("/user/me")
.expect(200);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith("test-user-uuid");
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith("test-user-uuid");
});
});
@@ -71,8 +72,8 @@ describe("RestUser", () => {
spotifyConfig: null
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
testEnv.mockUserService.updateUser.mockResolvedValue(mockUser);
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
mockedUserService.updateUser.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.put("/user/me/spotify")
@@ -80,8 +81,8 @@ describe("RestUser", () => {
.expect(200);
expect(response.body.data.message).toBe("Spotify Config erfolgreich geändert");
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith("test-user-uuid");
expect(testEnv.mockUserService.updateUser).toHaveBeenCalledWith({
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith("test-user-uuid");
expect(mockedUserService.updateUser).toHaveBeenCalledWith({
...mockUser,
spotifyConfig: {
accessToken: "access-token-123",
@@ -93,7 +94,7 @@ describe("RestUser", () => {
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
mockedUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.put("/user/me/spotify")
@@ -176,20 +177,20 @@ describe("RestUser", () => {
spotifyConfig: null
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
testEnv.mockUserService.clearSpotifyConfigByUUID.mockResolvedValue(updatedUser);
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
mockedUserService.clearSpotifyConfigByUUID.mockResolvedValue(updatedUser);
const response = await request(testEnv.app)
.delete("/user/me/spotify")
.expect(200);
expect(response.body.data.user).toEqual(updatedUser);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith("test-user-uuid");
expect(testEnv.mockUserService.clearSpotifyConfigByUUID).toHaveBeenCalledWith("test-user-uuid");
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith("test-user-uuid");
expect(mockedUserService.clearSpotifyConfigByUUID).toHaveBeenCalledWith("test-user-uuid");
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
mockedUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.delete("/user/me/spotify")
@@ -215,10 +216,10 @@ describe("RestUser", () => {
password: "old-hashed-password"
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
vi.mocked(PasswordUtils.validatePassword).mockReturnValue({valid: true});
vi.mocked(PasswordUtils.hashPassword).mockResolvedValue("new-hashed-password");
testEnv.mockUserService.updateUser.mockResolvedValue(mockUser);
mockedUserService.updateUser.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.put("/user/me/password")
@@ -228,14 +229,14 @@ describe("RestUser", () => {
expect(response.body.data.message).toBe("Passwort erfolgreich geändert");
expect(PasswordUtils.validatePassword).toHaveBeenCalledWith("newpassword123");
expect(PasswordUtils.hashPassword).toHaveBeenCalledWith("newpassword123");
expect(testEnv.mockUserService.updateUser).toHaveBeenCalledWith({
expect(mockedUserService.updateUser).toHaveBeenCalledWith({
...mockUser,
password: "new-hashed-password"
});
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
mockedUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.put("/user/me/password")
@@ -252,7 +253,7 @@ describe("RestUser", () => {
uuid: "test-user-uuid"
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
const invalidData = {
password: "newpassword123",
@@ -276,7 +277,7 @@ describe("RestUser", () => {
uuid: "test-user-uuid"
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
mockedUserService.getUserByUUID.mockResolvedValue(mockUser);
vi.mocked(PasswordUtils.validatePassword).mockReturnValue({
valid: false,
message: "Password too weak"
@@ -331,7 +332,7 @@ describe("RestUser", () => {
describe("when user is an admin", () => {
beforeEach(() => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(adminUser);
mockedUserService.getUserByUUID.mockResolvedValue(adminUser);
});
it("should return all users", async () => {
@@ -339,19 +340,19 @@ describe("RestUser", () => {
{id: "1", name: "user1", uuid: "uuid1"},
{id: "2", name: "user2", uuid: "uuid2"}
];
testEnv.mockUserService.getAllUsers.mockResolvedValue(mockUsers);
mockedUserService.getAllUsers.mockResolvedValue(mockUsers);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual(mockUsers);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(mockedUserService.getAllUsers).toHaveBeenCalled();
});
it("should handle empty user list", async () => {
testEnv.mockUserService.getAllUsers.mockResolvedValue([]);
mockedUserService.getAllUsers.mockResolvedValue([]);
const response = await request(testEnv.app)
.get("/user/")
@@ -363,7 +364,7 @@ describe("RestUser", () => {
describe("when user is not an admin", () => {
it("should return 404 Not Found if user is not an admin", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
mockedUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
await request(testEnv.app)
.get("/user/")
@@ -371,7 +372,7 @@ describe("RestUser", () => {
});
it("should return 404 Not Found if user does not exist", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
mockedUserService.getUserByUUID.mockResolvedValue(null);
await request(testEnv.app)
.get("/user/")
@@ -390,23 +391,23 @@ describe("RestUser", () => {
describe("when user is an admin", () => {
beforeEach(() => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(adminUser);
mockedUserService.getUserByUUID.mockResolvedValue(adminUser);
});
it("should return user by id", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(mockUser);
mockedUserService.getUserById.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.get(`/user/${specificUserId}`)
.expect(200);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(testEnv.mockUserService.getUserById).toHaveBeenCalledWith(specificUserId);
expect(mockedUserService.getUserByUUID).toHaveBeenCalledWith(requestingUserUUID);
expect(mockedUserService.getUserById).toHaveBeenCalledWith(specificUserId);
});
it("should return bad request when target user is not found", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(null);
mockedUserService.getUserById.mockResolvedValue(null);
const response = await request(testEnv.app)
.get(`/user/nonexistent-id`)
@@ -418,7 +419,7 @@ describe("RestUser", () => {
describe("when user is not an admin", () => {
it("should return 404 Not Found if user is not an admin", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
mockedUserService.getUserByUUID.mockResolvedValue(nonAdminUser);
await request(testEnv.app)
.get(`/user/${specificUserId}`)
@@ -426,7 +427,7 @@ describe("RestUser", () => {
});
it("should return 404 Not Found if user does not exist", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
mockedUserService.getUserByUUID.mockResolvedValue(null);
await request(testEnv.app)
.get(`/user/${specificUserId}`)
@@ -2,6 +2,10 @@ import { describe, it, expect, vi, beforeEach, type Mocked } from "vitest";
import { getEventListeners } from "../../../../src/utils/websocket/websocketCustomEvents/websocketEventUtils";
import { WebsocketEventType } from "../../../../src/utils/websocket/websocketCustomEvents/websocketEventType";
import { CustomWebsocketEvent } from "../../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent";
import type { UserService } from "../../../../src/db/services/db/UserService";
import {
CustomWebsocketEventUserService
} from "../../../../src/utils/websocket/websocketCustomEvents/customWebsocketEventUserService";
type MockWs = {
user: {
@@ -9,10 +13,16 @@ type MockWs = {
lastState: { global: { mode: string; brightness: number } };
};
send: Mocked<(data: any, options: { binary: boolean }) => void>;
on: Mocked<(event: string, listener: (...args: any[]) => void) => void>;
emit: Mocked<(event: string, ...args: any[]) => void>;
};
type MockUserService = Mocked<UserService>;
describe("websocketEventUtils.getEventListeners", () => {
let mockWs: MockWs;
let mockUserService: MockUserService;
let listeners: CustomWebsocketEvent[];
beforeEach(() => {
@@ -22,8 +32,16 @@ describe("websocketEventUtils.getEventListeners", () => {
lastState: { global: { mode: "idle", brightness: 42 } },
},
send: vi.fn(),
on: vi.fn(),
emit: vi.fn(),
};
listeners = getEventListeners(mockWs as any);
mockUserService = {
getUserByUUID: vi.fn(),
updateUser: vi.fn(),
} as any;
listeners = getEventListeners(mockWs as any, mockUserService);
});
it("should return an array of event listener objects", () => {
@@ -34,6 +52,9 @@ describe("websocketEventUtils.getEventListeners", () => {
expect(listener).toHaveProperty("event");
expect(listener).toHaveProperty("handler");
expect(typeof listener.handler).toBe("function");
if (typeof listener === typeof CustomWebsocketEventUserService){
expect(listener).toHaveProperty("userService", mockUserService);
}
}
});
@@ -2,10 +2,12 @@ import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vi
import { WebsocketEventHandler } from "../../../src/utils/websocket/websocketEventHandler";
import { ExtendedWebSocket } from "../../../src/interfaces/extendedWebsocket";
import { CustomWebsocketEvent } from "../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent";
import {UserService} from "../../../src/db/services/db/UserService";
describe("WebsocketEventHandler", () => {
let mockWebSocket: Mocked<ExtendedWebSocket>;
let websocketEventHandler: WebsocketEventHandler;
let mockUserService: Mocked<UserService>;
let registeredHandlers: Map<string, (...args: any[]) => void>;
beforeEach(() => {
@@ -27,7 +29,10 @@ describe("WebsocketEventHandler", () => {
asyncUpdates: new Map([["update1", 123], ["update2", 456]]),
} as unknown as Mocked<ExtendedWebSocket>;
websocketEventHandler = new WebsocketEventHandler(mockWebSocket);
// not used in this test
mockUserService = {} as Mocked<UserService>;
websocketEventHandler = new WebsocketEventHandler(mockWebSocket, mockUserService);
});
afterEach(() => {
@@ -108,6 +113,7 @@ describe("WebsocketEventHandler", () => {
// @ts-ignore
const bindSpy = vi.spyOn(customEvent.handler, "bind");
// @ts-ignore - Access to private method for testing purposes
websocketEventHandler.registerCustomEvent(customEvent);
expect(mockWebSocket.on).toHaveBeenCalledWith("custom_event", expect.any(Function));
@@ -1,16 +1,12 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import { WebsocketServerEventHandler } from "../../../src/utils/websocket/websocketServerEventHandler";
import type { UserService } from "../../../src/db/services/db/UserService";
const { heartbeatSpy, getUserByUUID } = vi.hoisted(() => ({
heartbeatSpy: vi.fn(),
getUserByUUID: vi.fn(),
const heartbeatSpy = vi.fn();
vi.mock("../../../src/utils/websocket/websocketServerHeartbeatInterval", () => ({
heartbeat: () => heartbeatSpy,
}));
vi.mock("../../../src/utils/websocket/websocketServerHeartbeatInterval", () => {
return {
heartbeat: () => heartbeatSpy,
};
});
const userObj = {
name: "tester",
uuid: "uuid-1",
@@ -19,38 +15,27 @@ const userObj = {
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);
this.handlers.get(event)?.(...args);
}
}
import { WebsocketServerEventHandler } from "../../../src/utils/websocket/websocketServerEventHandler";
describe("WebsocketServerEventHandler", () => {
let wss: FakeWSS;
let mockUserService: Mocked<UserService>; // Variable für unseren Mock-Service
beforeEach(() => {
wss = new FakeWSS();
heartbeatSpy.mockReset();
getUserByUUID.mockReset();
getUserByUUID.mockResolvedValue(userObj);
heartbeatSpy.mockClear();
mockUserService = {
getUserByUUID: vi.fn().mockResolvedValue(userObj),
} as any;
});
afterEach(() => {
@@ -58,12 +43,10 @@ describe("WebsocketServerEventHandler", () => {
});
it("enableConnectionEvent sets user/payload/isAlive/asyncUpdates and calls callback", async () => {
const handler = new WebsocketServerEventHandler(wss as any);
const handler = new WebsocketServerEventHandler(wss as any, mockUserService);
const cb = vi.fn();
const done = new Promise<void>((resolve) => {
cb.mockImplementation(() => resolve());
});
const done = new Promise<void>((resolve) => cb.mockImplementation(() => resolve()));
handler.enableConnectionEvent(cb);
@@ -74,7 +57,7 @@ describe("WebsocketServerEventHandler", () => {
await done;
expect(getUserByUUID).toHaveBeenCalledWith("uuid-1");
expect(mockUserService.getUserByUUID).toHaveBeenCalledWith("uuid-1");
expect(ws.user).toEqual(userObj);
expect(ws.payload).toEqual(req.payload);
expect(ws.isAlive).toBe(true);
@@ -84,7 +67,7 @@ describe("WebsocketServerEventHandler", () => {
it("enableHeartbeat starts interval and calls heartbeat()", () => {
vi.useFakeTimers();
const handler = new WebsocketServerEventHandler(wss as any);
const handler = new WebsocketServerEventHandler(wss as any, mockUserService);
const id = handler.enableHeartbeat(1000);
expect(["number", "object"]).toContain(typeof id);
@@ -97,16 +80,16 @@ describe("WebsocketServerEventHandler", () => {
});
it("enableCloseEvent registers Listener and calls callback on close", () => {
const handler = new WebsocketServerEventHandler(wss as any);
const handler = new WebsocketServerEventHandler(wss as any, mockUserService);
const cb = vi.fn();
const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
handler.enableCloseEvent(cb);
handler.enableCloseEvent(cb);
wss.emit("close");
expect(cb).toHaveBeenCalledTimes(1);
expect(logSpy).toHaveBeenCalledWith("WebSocket server closed");
logSpy.mockRestore();
});
});
+15 -8
View File
@@ -5,6 +5,8 @@ import { ExtendedWebSocketServer } from "../src/websocket";
import { WebsocketServerEventHandler } from "../src/utils/websocket/websocketServerEventHandler";
import { WebsocketEventHandler } from "../src/utils/websocket/websocketEventHandler";
import { getEventListeners } from "../src/utils/websocket/websocketCustomEvents/websocketEventUtils";
import {UserService} from "../src/db/services/db/UserService";
import {createMockUserService} from "./helpers/testSetup";
let mockWssInstance: Mocked<WebSocketServer>;
let mockServerEventHandler: Mocked<WebsocketServerEventHandler>;
@@ -24,6 +26,7 @@ vi.mock("../src/utils/websocket/websocketCustomEvents/websocketEventUtils");
describe("ExtendedWebSocketServer", () => {
let mockHttpServer: Mocked<Server>;
let extendedWss: ExtendedWebSocketServer;
let mockUserService: Mocked<UserService>;
beforeEach(() => {
vi.clearAllMocks();
@@ -42,7 +45,9 @@ describe("ExtendedWebSocketServer", () => {
close: vi.fn(),
} as unknown as Mocked<WebSocketServer>;
extendedWss = new ExtendedWebSocketServer(mockHttpServer);
mockUserService = createMockUserService();
extendedWss = new ExtendedWebSocketServer(mockHttpServer, mockUserService);
});
describe("Constructor and Setup", () => {
@@ -53,8 +58,8 @@ describe("ExtendedWebSocketServer", () => {
});
});
it("should create and use a WebsocketServerEventHandler", () => {
expect(WebsocketServerEventHandler).toHaveBeenCalledWith(mockWssInstance);
it("should create and use a WebsocketServerEventHandler with the correct service", () => {
expect(WebsocketServerEventHandler).toHaveBeenCalledWith(mockWssInstance, mockUserService);
});
it("should enable the heartbeat", () => {
@@ -99,9 +104,11 @@ describe("ExtendedWebSocketServer", () => {
emit: vi.fn(), on: vi.fn(), user: { lastState: { global: { mode: "idle" } } },
};
mockClientEventHandler = {
enableErrorEvent: vi.fn(), enablePongEvent: vi.fn(),
enableMessageEvent: vi.fn(), enableDisconnectEvent: vi.fn(),
registerCustomEvent: vi.fn(),
enableErrorEvent: vi.fn(),
enablePongEvent: vi.fn(),
enableMessageEvent: vi.fn(),
enableDisconnectEvent: vi.fn(),
registerCustomEvents: vi.fn(),
} as unknown as Mocked<WebsocketEventHandler>;
vi.mocked(WebsocketEventHandler).mockImplementation(() => mockClientEventHandler);
@@ -110,12 +117,12 @@ describe("ExtendedWebSocketServer", () => {
it("should create and configure a WebsocketEventHandler for new clients", () => {
connectionHandler(mockWsClient, {});
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient);
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient, mockUserService);
expect(mockClientEventHandler.enableErrorEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enablePongEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enableMessageEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enableDisconnectEvent).toHaveBeenCalled();
expect(mockClientEventHandler.registerCustomEvent).toHaveBeenCalled();
expect(mockClientEventHandler.registerCustomEvents).toHaveBeenCalled();
});
it("should emit initial events to the new client", () => {