add and change multiple vitests

This commit is contained in:
StarAppeal
2025-09-09 05:37:57 +02:00
parent 63d9c796f6
commit 6bba31e575
25 changed files with 2107 additions and 874 deletions
+4 -1
View File
@@ -70,4 +70,7 @@ process.on("SIGTERM", () => {
console.log("HTTP server closed");
process.exit(0);
});
});
});
// Export the app for testing purposes
export default app; // optional
+8 -4
View File
@@ -1,9 +1,13 @@
import type { Request, Response, NextFunction, RequestHandler } from "express";
export function asyncHandler(
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
fn: (req: Request, res: Response, next: NextFunction) => Promise<any>
): RequestHandler {
return (req, res, next) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
return async (req, res, next) => {
try {
await fn(req, res, next);
} catch (err) {
next(err);
}
};
}
+7
View File
@@ -66,6 +66,13 @@ export const v = {
isArrayLength: (len: number): Validator => {
return (value: any) => (Array.isArray(value) && value.length === len ? true : `must be an array of length ${len}`);
},
isObject: (opts?: { nonEmpty?: boolean }): Validator => {
return (value: any) => {
if (typeof value !== "object" || value === null) return "must be an object";
if (opts?.nonEmpty && Object.keys(value).length === 0) return "must be a non-empty object";
return true;
};
},
isUrl: (): Validator => {
return (value: any) => {
if (typeof value !== "string") return "must be a string URL";
+1 -1
View File
@@ -84,7 +84,7 @@ export class RestUser {
const passwordValidation = PasswordUtils.validatePassword(password);
if (!passwordValidation.valid) {
return badRequest(res, passwordValidation.message ?? "Ungültiges Passwort");
return badRequest(res, passwordValidation.message ?? "Invalid password");
}
user.password = await PasswordUtils.hashPassword(password);
+3 -4
View File
@@ -1,7 +1,7 @@
import express, { Router, Request, Response } from "express";
import { ExtendedWebSocketServer } from "../websocket";
import { asyncHandler } from "./middleware/asyncHandler";
import { validateBody, v } from "./middleware/validate";
import {v, validateBody} from "./middleware/validate";
import { ok } from "./utils/responses";
import {ExtendedWebSocket} from "../interfaces/extendedWebsocket";
@@ -16,8 +16,7 @@ export class RestWebSocket {
validateBody({
payload: {
required: true,
// allow any json
validator: (_: unknown) => true,
validator: v.isObject({ nonEmpty: true }),
},
}),
asyncHandler(async (req: Request, res: Response) => {
@@ -32,7 +31,7 @@ export class RestWebSocket {
validateBody({
payload: {
required: true,
validator: (_: unknown) => true,
validator: v.isObject({ nonEmpty: true }),
},
users: {
required: true,
+97
View File
@@ -0,0 +1,97 @@
import { describe, it, expect, afterEach, vi } from "vitest";
const loadConfigWithEnv = async (envVars: Record<string, string | undefined>) => {
for (const key in envVars) {
vi.stubEnv(key, envVars[key] as string);
}
const { config } = await import("../src/config");
return config;
};
describe("config.ts", () => {
afterEach(() => {
vi.resetModules();
vi.unstubAllEnvs();
});
describe("PORT configuration", () => {
const requiredEnv = { FRONTEND_URL: "http://localhost:3000" };
it("should default to 3000 if PORT is not set", async () => {
const config = await loadConfigWithEnv({ ...requiredEnv, PORT: undefined });
expect(config.port).toBe(3000);
});
it("should parse a valid PORT from the environment", async () => {
const config = await loadConfigWithEnv({ ...requiredEnv, PORT: "8080" });
expect(config.port).toBe(8080);
});
it.each([
{ case: "not a number", value: "abc" },
{ case: "a negative number", value: "-100" },
{ case: "zero", value: "0" },
{ case: "Infinity", value: "Infinity" },
])("should throw an error if PORT is $case", async ({ value }) => {
const load = () => loadConfigWithEnv({ ...requiredEnv, PORT: value });
await expect(load).rejects.toThrow("Env var PORT must be a positive number");
});
});
describe("CORS Origin (FRONTEND_URL) configuration", () => {
it.each([
{ case: "a valid HTTP URL", value: "http://localhost:3000" },
{ case: "a valid HTTPS URL", value: "https://example.com" },
{ case: "a URL with a path", value: "https://example.com/app/path" },
])("should accept $case", async ({ value }) => {
const config = await loadConfigWithEnv({ FRONTEND_URL: value });
expect(config.cors.origin).toBe(value);
});
it.each([
{ case: "undefined", value: undefined, error: "Missing required env var: FRONTEND_URL" },
{ case: "an empty string", value: "", error: "Missing required env var: FRONTEND_URL" },
{ case: "only whitespace", value: " ", error: "Missing required env var: FRONTEND_URL" },
{ case: "not a valid URL", value: "not-a-url", error: "FRONTEND_URL must be a valid URL" },
])("should throw an error if FRONTEND_URL is $case", async ({ value, error }) => {
const load = () => loadConfigWithEnv({ FRONTEND_URL: value });
await expect(load).rejects.toThrow(error);
});
});
describe("Overall Config Object", () => {
it("should create a complete config object with default values", async () => {
const config = await loadConfigWithEnv({
FRONTEND_URL: "http://localhost:3000",
NODE_ENV: undefined,
PORT: undefined,
});
expect(config).toEqual({
env: "development",
port: 3000,
cors: {
origin: "http://localhost:3000",
credentials: true,
},
});
});
it("should create a config object with all custom values", async () => {
const config = await loadConfigWithEnv({
FRONTEND_URL: "https://myapp.com",
NODE_ENV: "production",
PORT: "9999",
});
expect(config).toEqual({
env: "production",
port: 9999,
cors: {
origin: "https://myapp.com",
credentials: true,
},
});
});
});
});
-359
View File
@@ -1,359 +0,0 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { UserService } from "../../../src/db/services/db/UserService";
import { UserModel, IUser, SpotifyConfig } from "../../../src/db/models/user";
import { connectToDatabase } from "../../../src/db/services/db/database.service";
vi.mock("../../../src/db/services/db/database.service", () => ({
connectToDatabase: vi.fn(),
}));
vi.mock("../../../src/db/models/user", () => ({
UserModel: {
findByIdAndUpdate: vi.fn(),
find: vi.fn(),
findById: vi.fn(),
findOne: vi.fn(),
findOneAndUpdate: vi.fn(),
create: vi.fn(),
},
SpotifyConfig: {},
}));
const mockedUserModel = vi.mocked(UserModel);
const mockedConnectToDatabase = vi.mocked(connectToDatabase);
describe("UserService", () => {
let userService: UserService;
beforeEach(async () => {
vi.clearAllMocks();
mockedConnectToDatabase.mockResolvedValue(undefined);
userService = await UserService.create();
});
afterEach(() => {
(UserService as any)._instance = undefined;
});
describe("create (singleton)", () => {
it("should create a singleton instance", async () => {
const instance1 = await UserService.create();
const instance2 = await UserService.create();
expect(instance1).toBe(instance2);
expect(mockedConnectToDatabase).toHaveBeenCalledTimes(1);
});
it("should connect to database on first creation", async () => {
await UserService.create();
expect(mockedConnectToDatabase).toHaveBeenCalledOnce();
});
});
describe("updateUserById", () => {
it("should update user by id and return updated user without password", async () => {
const userId = "507f1f77bcf86cd799439011";
const updateData = { name: "Updated Name" };
const updatedUser = { _id: userId, name: "Updated Name", email: "test@example.com" };
const mockExec = vi.fn().mockResolvedValue(updatedUser);
mockedUserModel.findByIdAndUpdate.mockReturnValue({ exec: mockExec } as any);
const result = await userService.updateUserById(userId, updateData);
expect(mockedUserModel.findByIdAndUpdate).toHaveBeenCalledWith(
userId,
updateData,
{
new: true,
projection: { password: 0 },
}
);
expect(result).toEqual(updatedUser);
});
it("should return null if user not found", async () => {
const userId = "507f1f77bcf86cd799439011";
const updateData = { name: "Updated Name" };
const mockExec = vi.fn().mockResolvedValue(null);
mockedUserModel.findByIdAndUpdate.mockReturnValue({ exec: mockExec } as any);
const result = await userService.updateUserById(userId, updateData);
expect(result).toBeNull();
});
});
describe("updateUser", () => {
it("should update user using id field", async () => {
const user = { id: "507f1f77bcf86cd799439011", name: "Test User" } as any;
const updatedUser = { _id: user.id, name: "Test User" };
const mockExec = vi.fn().mockResolvedValue(updatedUser);
mockedUserModel.findByIdAndUpdate.mockReturnValue({ exec: mockExec } as any);
const result = await userService.updateUser(user);
expect(mockedUserModel.findByIdAndUpdate).toHaveBeenCalledWith(
user.id,
{ name: "Test User" },
{
new: true,
projection: { password: 0 },
}
);
expect(result).toEqual(updatedUser);
});
it("should update user using _id field", async () => {
const user = { _id: "507f1f77bcf86cd799439011", name: "Test User" } as any;
const updatedUser = { _id: user._id, name: "Test User" };
const mockExec = vi.fn().mockResolvedValue(updatedUser);
mockedUserModel.findByIdAndUpdate.mockReturnValue({ exec: mockExec } as any);
const result = await userService.updateUser(user);
expect(mockedUserModel.findByIdAndUpdate).toHaveBeenCalledWith(
user._id,
{ name: "Test User" },
{
new: true,
projection: { password: 0 },
}
);
expect(result).toEqual(updatedUser);
});
it("should throw error if user has no id or _id", async () => {
const user = { name: "Test User" } as any;
await expect(userService.updateUser(user)).rejects.toThrow(
"updateUser requires user.id or user._id"
);
});
});
describe("getAllUsers", () => {
it("should return all users without sensitive fields", async () => {
const users = [
{ _id: "1", name: "User1", email: "user1@example.com" },
{ _id: "2", name: "User2", email: "user2@example.com" },
];
const mockExec = vi.fn().mockResolvedValue(users);
mockedUserModel.find.mockReturnValue({ exec: mockExec } as any);
const result = await userService.getAllUsers();
expect(mockedUserModel.find).toHaveBeenCalledWith(
{},
{ spotifyConfig: 0, lastState: 0 }
);
expect(result).toEqual(users);
});
});
describe("getUserById", () => {
it("should return user by id", async () => {
const userId = "507f1f77bcf86cd799439011";
const user = { _id: userId, name: "Test User" };
const mockExec = vi.fn().mockResolvedValue(user);
mockedUserModel.findById.mockReturnValue({ exec: mockExec } as any);
const result = await userService.getUserById(userId);
expect(mockedUserModel.findById).toHaveBeenCalledWith(userId);
expect(result).toEqual(user);
});
it("should return null if user not found", async () => {
const userId = "507f1f77bcf86cd799439011";
const mockExec = vi.fn().mockResolvedValue(null);
mockedUserModel.findById.mockReturnValue({ exec: mockExec } as any);
const result = await userService.getUserById(userId);
expect(result).toBeNull();
});
});
describe("getUserByUUID", () => {
it("should return user by UUID", async () => {
const uuid = "test-uuid-123";
const user = { _id: "507f1f77bcf86cd799439011", uuid, name: "Test User" };
const mockExec = vi.fn().mockResolvedValue(user);
mockedUserModel.findOne.mockReturnValue({ exec: mockExec } as any);
const result = await userService.getUserByUUID(uuid);
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ uuid });
expect(result).toEqual(user);
});
});
describe("getUserByName", () => {
it("should return user by name with case-insensitive search", async () => {
const name = "TestUser";
const user = { _id: "507f1f77bcf86cd799439011", name, email: "test@example.com" };
const mockExec = vi.fn().mockResolvedValue(user);
const mockCollation = vi.fn().mockReturnValue({ exec: mockExec });
mockedUserModel.findOne.mockReturnValue({ collation: mockCollation } as any);
const result = await userService.getUserByName(name);
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ name });
expect(mockCollation).toHaveBeenCalledWith({ locale: "en", strength: 2 });
expect(result).toEqual(user);
});
});
describe("getUserAuthByName", () => {
it("should return user with password for authentication", async () => {
const name = "TestUser";
const user = { _id: "507f1f77bcf86cd799439011", name, password: "hashedPassword" };
const mockExec = vi.fn().mockResolvedValue(user);
const mockSelect = vi.fn().mockReturnValue({ exec: mockExec });
const mockCollation = vi.fn().mockReturnValue({ select: mockSelect });
mockedUserModel.findOne.mockReturnValue({ collation: mockCollation } as any);
const result = await userService.getUserAuthByName(name);
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ name });
expect(mockCollation).toHaveBeenCalledWith({ locale: "en", strength: 2 });
expect(mockSelect).toHaveBeenCalledWith("+password");
expect(result).toEqual(user);
});
});
describe("getSpotifyConfigByUUID", () => {
it("should return spotify config for user", async () => {
const uuid = "test-uuid-123";
const spotifyConfig: SpotifyConfig = {
accessToken: "access-token",
refreshToken: "refresh-token",
expirationDate: new Date(),
scope: "user-read-playback-state",
};
const user = { spotifyConfig };
const mockExec = vi.fn().mockResolvedValue(user);
mockedUserModel.findOne.mockReturnValue({ exec: mockExec } as any);
const result = await userService.getSpotifyConfigByUUID(uuid);
expect(mockedUserModel.findOne).toHaveBeenCalledWith(
{ uuid },
{ spotifyConfig: 1 }
);
expect(result).toEqual(spotifyConfig);
});
it("should return undefined if user has no spotify config", async () => {
const uuid = "test-uuid-123";
const mockExec = vi.fn().mockResolvedValue(null);
mockedUserModel.findOne.mockReturnValue({ exec: mockExec } as any);
const result = await userService.getSpotifyConfigByUUID(uuid);
expect(result).toBeUndefined();
});
});
describe("createUser", () => {
it("should create user and return without password", async () => {
const userData: Partial<IUser> = {
name: "New User",
email: "new@example.com",
password: "hashedPassword",
uuid: "new-uuid",
} as Partial<IUser>;
const createdUser = {
...userData,
_id: "507f1f77bcf86cd799439011",
toObject: vi.fn().mockReturnValue({
...userData,
_id: "507f1f77bcf86cd799439011",
}),
};
mockedUserModel.create.mockResolvedValue(createdUser as any);
const result = await userService.createUser(userData as any);
expect(mockedUserModel.create).toHaveBeenCalledWith(userData);
expect(result).toEqual({
name: "New User",
email: "new@example.com",
uuid: "new-uuid",
_id: "507f1f77bcf86cd799439011",
});
expect(result).not.toHaveProperty("password");
});
});
describe("existsUserByName", () => {
it("should return true if user exists", async () => {
const name = "ExistingUser";
const user = { _id: "507f1f77bcf86cd799439011", name };
const mockExec = vi.fn().mockResolvedValue(user);
mockedUserModel.findOne.mockReturnValue({ exec: mockExec } as any);
const result = await userService.existsUserByName(name);
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ name });
expect(result).toBe(true);
});
it("should return false if user does not exist", async () => {
const name = "NonExistentUser";
const mockExec = vi.fn().mockResolvedValue(null);
mockedUserModel.findOne.mockReturnValue({ exec: mockExec } as any);
const result = await userService.existsUserByName(name);
expect(result).toBe(false);
});
});
describe("clearSpotifyConfigByUUID", () => {
it("should clear spotify config and return updated user", async () => {
const uuid = "test-uuid-123";
const updatedUser = { _id: "507f1f77bcf86cd799439011", uuid, name: "Test User" };
const mockExec = vi.fn().mockResolvedValue(updatedUser);
mockedUserModel.findOneAndUpdate.mockReturnValue({ exec: mockExec } as any);
const result = await userService.clearSpotifyConfigByUUID(uuid);
expect(mockedUserModel.findOneAndUpdate).toHaveBeenCalledWith(
{ uuid },
{ $unset: { spotifyConfig: 1 } },
{ new: true, projection: { password: 0 } }
);
expect(result).toEqual(updatedUser);
});
it("should return null if user not found", async () => {
const uuid = "non-existent-uuid";
const mockExec = vi.fn().mockResolvedValue(null);
mockedUserModel.findOneAndUpdate.mockReturnValue({ exec: mockExec } as any);
const result = await userService.clearSpotifyConfigByUUID(uuid);
expect(result).toBeNull();
});
});
});
+268
View File
@@ -0,0 +1,268 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { UserService } from "../../../../src/db/services/db/UserService";
import {UserModel} from "../../../../src/db/models/user";
import { connectToDatabase } from "../../../../src/db/services/db/database.service";
vi.mock("../../../../src/db/services/db/database.service", () => ({
connectToDatabase: vi.fn(),
}));
vi.mock("../../../../src/db/models/user");
const mockedUserModel = vi.mocked(UserModel);
const mockedConnectToDatabase = vi.mocked(connectToDatabase);
const createMockMongooseQuery = (resolveValue: any) => {
const exec = vi.fn().mockResolvedValue(resolveValue);
const select = vi.fn().mockReturnValue({ exec });
const collation = vi.fn().mockReturnValue({ select, exec });
return { exec, select, collation };
};
describe("UserService", () => {
let userService: UserService;
beforeEach(async () => {
vi.clearAllMocks();
mockedConnectToDatabase.mockResolvedValue(undefined);
(UserService as any)._instance = undefined;
userService = await UserService.create();
});
describe("create (singleton)", () => {
it("should create a singleton instance", async () => {
const instance1 = userService; // Bereits im beforeEach erstellt
const instance2 = await UserService.create();
expect(instance1).toBe(instance2);
});
it("should connect to database only on first creation", async () => {
await UserService.create();
await UserService.create();
expect(mockedConnectToDatabase).toHaveBeenCalledTimes(1);
});
});
describe("updateUserById", () => {
it("should update user by id and return updated user without password", async () => {
const userId = "507f1f77bcf86cd799439011";
const updateData = { name: "Updated Name" };
const updatedUser = { _id: userId, name: "Updated Name", email: "test@example.com" };
mockedUserModel.findByIdAndUpdate.mockReturnValue(createMockMongooseQuery(updatedUser) as any);
const result = await userService.updateUserById(userId, updateData);
expect(mockedUserModel.findByIdAndUpdate).toHaveBeenCalledWith(
userId,
updateData,
{ new: true, projection: { password: 0 } }
);
expect(result).toEqual(updatedUser);
});
it("should return null if user not found", async () => {
mockedUserModel.findByIdAndUpdate.mockReturnValue(createMockMongooseQuery(null) as any);
const result = await userService.updateUserById("some-id", { name: "Updated Name" });
expect(result).toBeNull();
});
});
describe("updateUser", () => {
it("should update user using id field", async () => {
const user = { id: "507f1f77bcf86cd799439011", name: "Test User" };
mockedUserModel.findByIdAndUpdate.mockReturnValue(createMockMongooseQuery(user) as any);
const result = await userService.updateUser(user as any);
expect(mockedUserModel.findByIdAndUpdate).toHaveBeenCalledWith(
user.id,
{ name: "Test User" },
expect.any(Object)
);
expect(result).toEqual(user);
});
it("should update user using _id field", async () => {
const user = { _id: "507f1f77bcf86cd799439011", name: "Test User" };
mockedUserModel.findByIdAndUpdate.mockReturnValue(createMockMongooseQuery(user) as any);
const result = await userService.updateUser(user as any);
expect(mockedUserModel.findByIdAndUpdate).toHaveBeenCalledWith(
user._id,
{ name: "Test User" },
expect.any(Object)
);
expect(result).toEqual(user);
});
it("should throw error if user has no id or _id", async () => {
await expect(userService.updateUser({ name: "Test User" } as any)).rejects.toThrow(
"updateUser requires user.id or user._id"
);
});
});
describe("getAllUsers", () => {
it("should return all users without sensitive fields", async () => {
const users = [{ name: "User1" }, { name: "User2" }];
mockedUserModel.find.mockReturnValue(createMockMongooseQuery(users) as any);
const result = await userService.getAllUsers();
expect(mockedUserModel.find).toHaveBeenCalledWith({}, { spotifyConfig: 0, lastState: 0 });
expect(result).toEqual(users);
});
});
describe("getUserById", () => {
it("should return user by id", async () => {
const user = { _id: "user1", name: "Test User" };
mockedUserModel.findById.mockReturnValue(createMockMongooseQuery(user) as any);
const result = await userService.getUserById("user1");
expect(mockedUserModel.findById).toHaveBeenCalledWith("user1");
expect(result).toEqual(user);
});
it("should return null if user not found", async () => {
mockedUserModel.findById.mockReturnValue(createMockMongooseQuery(null) as any);
const result = await userService.getUserById("non-existent-id");
expect(result).toBeNull();
});
});
describe("getUserByUUID", () => {
it("should return user by UUID", async () => {
const user = { uuid: "uuid-123", name: "Test User" };
mockedUserModel.findOne.mockReturnValue(createMockMongooseQuery(user) as any);
const result = await userService.getUserByUUID("uuid-123");
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ uuid: "uuid-123" });
expect(result).toEqual(user);
});
});
describe("getUserByName", () => {
it("should return user by name with case-insensitive search", async () => {
const user = { name: "TestUser" };
const mockQuery = createMockMongooseQuery(user);
mockedUserModel.findOne.mockReturnValue(mockQuery as any);
const result = await userService.getUserByName("TestUser");
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ name: "TestUser" });
expect(mockQuery.collation).toHaveBeenCalledWith({ locale: "en", strength: 2 });
expect(result).toEqual(user);
});
});
describe("getUserAuthByName", () => {
it("should return user with password for authentication", async () => {
const user = { name: "TestUser", password: "hashedPassword" };
const mockQuery = createMockMongooseQuery(user);
mockedUserModel.findOne.mockReturnValue(mockQuery as any);
const result = await userService.getUserAuthByName("TestUser");
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ name: "TestUser" });
expect(mockQuery.collation).toHaveBeenCalledWith({ locale: "en", strength: 2 });
expect(mockQuery.select).toHaveBeenCalledWith("+password");
expect(result).toEqual(user);
});
});
describe("getSpotifyConfigByUUID", () => {
it("should return spotify config for user", async () => {
const spotifyConfig = { accessToken: "access-token" };
mockedUserModel.findOne.mockReturnValue(createMockMongooseQuery({ spotifyConfig }) as any);
const result = await userService.getSpotifyConfigByUUID("uuid-123");
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ uuid: "uuid-123" }, { spotifyConfig: 1 });
expect(result).toEqual(spotifyConfig);
});
it("should return undefined if user has no spotify config", async () => {
mockedUserModel.findOne.mockReturnValue(createMockMongooseQuery(null) as any);
const result = await userService.getSpotifyConfigByUUID("uuid-123");
expect(result).toBeUndefined();
});
});
describe("createUser", () => {
it("should create user and return without password", async () => {
const userData = { name: "New User", password: "hashedPassword", uuid: "new-uuid" };
const createdUserWithPassword = { ...userData, _id: "newUser1" };
const createdUserDocument = {
...createdUserWithPassword,
toObject: () => createdUserWithPassword,
};
mockedUserModel.create.mockResolvedValue(createdUserDocument as any);
const result = await userService.createUser(userData as any)
expect(mockedUserModel.create).toHaveBeenCalledWith(userData);
expect(result).not.toHaveProperty("password");
expect(result.name).toBe("New User");
// @ts-ignore
expect(result._id).toBe("newUser1");
});
});
describe("existsUserByName", () => {
it("should return true if user exists", async () => {
mockedUserModel.findOne.mockReturnValue(createMockMongooseQuery({ _id: "some-id" }) as any);
const result = await userService.existsUserByName("ExistingUser");
expect(mockedUserModel.findOne).toHaveBeenCalledWith({ name: "ExistingUser" });
expect(result).toBe(true);
});
it("should return false if user does not exist", async () => {
mockedUserModel.findOne.mockReturnValue(createMockMongooseQuery(null) as any);
const result = await userService.existsUserByName("NonExistentUser");
expect(result).toBe(false);
});
});
describe("clearSpotifyConfigByUUID", () => {
it("should clear spotify config and return updated user", async () => {
const updatedUser = { uuid: "uuid-123", name: "Test User" };
mockedUserModel.findOneAndUpdate.mockReturnValue(createMockMongooseQuery(updatedUser) as any);
const result = await userService.clearSpotifyConfigByUUID("uuid-123");
expect(mockedUserModel.findOneAndUpdate).toHaveBeenCalledWith(
{ uuid: "uuid-123" },
{ $unset: { spotifyConfig: 1 } },
{ new: true, projection: { password: 0 } }
);
expect(result).toEqual(updatedUser);
});
it("should return null if user not found", async () => {
mockedUserModel.findOneAndUpdate.mockReturnValue(createMockMongooseQuery(null) as any);
const result = await userService.clearSpotifyConfigByUUID("non-existent-uuid");
expect(result).toBeNull();
});
});
});
@@ -0,0 +1,93 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import mongoose from "mongoose";
import { connectToDatabase } from "../../../../src/db/services/db/database.service";
vi.mock("mongoose");
vi.mock("dotenv/config", () => ({}));
describe("database.service", () => {
const mockedMongooseConnect = vi.mocked(mongoose.connect);
let consoleLogSpy: Mocked<typeof console.log>;
beforeEach(() => {
vi.clearAllMocks();
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}) as any;
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});
describe("connectToDatabase", () => {
describe("Success Scenarios", () => {
beforeEach(() => {
mockedMongooseConnect.mockResolvedValue(undefined as any);
});
it("should connect using the correct environment variables", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb://localhost:27017/test');
vi.stubEnv('DB_NAME', 'test_database');
await connectToDatabase();
expect(mockedMongooseConnect).toHaveBeenCalledWith(
"mongodb://localhost:27017/test",
{ dbName: "test_database" }
);
});
it("should log a success message on connection", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb://any');
vi.stubEnv('DB_NAME', 'any');
await connectToDatabase();
expect(consoleLogSpy).toHaveBeenCalledWith("Connected to MongoDB with Mongoose");
});
it("should resolve to undefined on successful connection", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb://any');
vi.stubEnv('DB_NAME', 'any');
await expect(connectToDatabase()).resolves.toBeUndefined();
});
it("should handle different connection string formats (e.g., Atlas)", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb+srv://user:pass@cluster.mongodb.net/');
vi.stubEnv('DB_NAME', 'cloud_database');
await connectToDatabase();
expect(mockedMongooseConnect).toHaveBeenCalledWith(
"mongodb+srv://user:pass@cluster.mongodb.net/",
{ dbName: "cloud_database" }
);
});
});
describe("Failure Scenarios", () => {
it("should propagate connection errors and not log success", async () => {
const connectionError = new Error("Connection failed");
mockedMongooseConnect.mockRejectedValue(connectionError);
vi.stubEnv('DB_CONN_STRING', 'mongodb://fail');
vi.stubEnv('DB_NAME', 'fail_db');
await expect(connectToDatabase()).rejects.toThrow(connectionError);
expect(consoleLogSpy).not.toHaveBeenCalledWith("Connected to MongoDB with Mongoose");
});
it("should handle specific Mongoose errors (e.g., MongoAuthenticationError)", async () => {
const authError = Object.assign(new Error("Authentication failed"), { name: "MongoAuthenticationError" });
mockedMongooseConnect.mockRejectedValue(authError);
vi.stubEnv('DB_CONN_STRING', 'mongodb://fail');
vi.stubEnv('DB_NAME', 'fail_db');
await expect(connectToDatabase()).rejects.toThrow(authError);
});
});
});
});
+70 -300
View File
@@ -1,318 +1,88 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import axios, { AxiosResponse } from "axios";
import { OAuthTokenResponse } from "../../../src/interfaces/OAuthTokenResponse";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import axios from "axios";
import type { SpotifyTokenService as SpotifyTokenServiceType } from "../../../src/db/services/spotifyTokenService";
import type { OAuthTokenResponse } from "../../../src/interfaces/OAuthTokenResponse";
vi.mock("axios", () => ({
default: {
post: vi.fn(),
},
}));
vi.mock("axios");
const mockedAxiosPost = vi.mocked(axios.post);
// Mock the SpotifyTokenService module to control environment variables
// probably a better way to do this would be to change the implementation
vi.mock("../../../src/db/services/spotifyTokenService", async () => {
const actual = await vi.importActual("../../../src/db/services/spotifyTokenService");
return {
...actual,
SpotifyTokenService: vi.fn().mockImplementation(() => {
const mockEnv = {
SPOTIFY_CLIENT_ID: "test-client-id",
SPOTIFY_CLIENT_SECRET: "test-client-secret",
};
const SPOTIFY_TOKEN_URL = "https://accounts.spotify.com/api/token";
return {
async refreshToken(refreshToken: string) {
console.log("refreshToken");
const response = await axios.post(
"https://accounts.spotify.com/api/token",
`grant_type=refresh_token&refresh_token=${refreshToken}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${mockEnv.SPOTIFY_CLIENT_ID}:${mockEnv.SPOTIFY_CLIENT_SECRET}`
).toString("base64")}`,
},
}
);
return response.data;
},
async generateToken(authorizationCode: string, redirectUri: string) {
console.log("generateToken");
const response = await axios.post(
"https://accounts.spotify.com/api/token",
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${mockEnv.SPOTIFY_CLIENT_ID}:${mockEnv.SPOTIFY_CLIENT_SECRET}`
).toString("base64")}`,
},
}
);
console.log(response.data);
return response.data;
}
};
})
};
afterEach(() => {
vi.resetModules();
vi.unstubAllEnvs();
});
import { SpotifyTokenService } from "../../../src/db/services/spotifyTokenService";
describe("SpotifyTokenService - Successful Initialization", () => {
let spotifyTokenService: SpotifyTokenServiceType;
const mockedAxios = vi.mocked(axios);
const mockedAxiosPost = mockedAxios.post as ReturnType<typeof vi.fn>;
beforeEach(async () => {
vi.stubEnv("SPOTIFY_CLIENT_ID", "test-client-id");
vi.stubEnv("SPOTIFY_CLIENT_SECRET", "test-client-secret");
// Mock environment variables
const mockEnv = {
SPOTIFY_CLIENT_ID: "test-client-id",
SPOTIFY_CLIENT_SECRET: "test-client-secret",
};
describe("SpotifyTokenService", () => {
let spotifyTokenService: SpotifyTokenService;
let consoleSpy: any;
beforeEach(() => {
vi.clearAllMocks();
spotifyTokenService = new SpotifyTokenService();
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
});
describe("refreshToken", () => {
it("should successfully refresh token", async () => {
const refreshToken = "test-refresh-token";
const mockResponse: OAuthTokenResponse = {
access_token: "new-access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "new-refresh-token",
scope: "user-read-playback-state",
};
mockedAxiosPost.mockResolvedValue({ data: mockResponse } as AxiosResponse);
const result = await spotifyTokenService.refreshToken(refreshToken);
expect(mockedAxiosPost).toHaveBeenCalledWith(
"https://accounts.spotify.com/api/token",
`grant_type=refresh_token&refresh_token=${refreshToken}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${mockEnv.SPOTIFY_CLIENT_ID}:${mockEnv.SPOTIFY_CLIENT_SECRET}`
).toString("base64")}`,
},
}
);
expect(result).toEqual(mockResponse);
expect(consoleSpy).toHaveBeenCalledWith("refreshToken");
const { SpotifyTokenService } = await import("../../../src/db/services/spotifyTokenService");
spotifyTokenService = new SpotifyTokenService();
});
it("should handle axios errors during token refresh", async () => {
const refreshToken = "test-refresh-token";
const error = new Error("Network error");
const getExpectedAuthHeader = () => {
const credentials = `test-client-id:test-client-secret`;
return `Basic ${Buffer.from(credentials).toString("base64")}`;
};
mockedAxiosPost.mockRejectedValue(error);
describe("refreshToken", () => {
it("should call the Spotify API with the correct parameters and return the data", async () => {
const refreshToken = "test-refresh-token";
const mockResponse = { access_token: "new-access-token" } as OAuthTokenResponse;
mockedAxiosPost.mockResolvedValue({ data: mockResponse });
await expect(spotifyTokenService.refreshToken(refreshToken)).rejects.toThrow("Network error");
const result = await spotifyTokenService.refreshToken(refreshToken);
expect(mockedAxiosPost).toHaveBeenCalledWith(
"https://accounts.spotify.com/api/token",
`grant_type=refresh_token&refresh_token=${refreshToken}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${mockEnv.SPOTIFY_CLIENT_ID}:${mockEnv.SPOTIFY_CLIENT_SECRET}`
).toString("base64")}`,
},
}
);
expect(result).toEqual(mockResponse);
expect(mockedAxiosPost).toHaveBeenCalledWith(
SPOTIFY_TOKEN_URL,
`grant_type=refresh_token&refresh_token=${refreshToken}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": getExpectedAuthHeader(),
},
}
);
});
it("should propagate errors from the Spotify API", async () => {
const apiError = new Error("Invalid refresh token");
mockedAxiosPost.mockRejectedValue(apiError);
await expect(spotifyTokenService.refreshToken("invalid-token")).rejects.toThrow(apiError);
});
});
it("should handle Spotify API errors", async () => {
const refreshToken = "invalid-refresh-token";
const spotifyError = {
response: {
status: 400,
data: {
error: "invalid_grant",
error_description: "Invalid refresh token",
},
},
};
describe("generateToken", () => {
it("should call the Spotify API with the correct parameters and return the data", async () => {
const authCode = "test-auth-code";
const redirectUri = "http://localhost:3000/callback";
const mockResponse = { access_token: "new-access-token" } as OAuthTokenResponse;
mockedAxiosPost.mockResolvedValue({ data: mockResponse });
mockedAxiosPost.mockRejectedValue(spotifyError);
const result = await spotifyTokenService.generateToken(authCode, redirectUri);
await expect(spotifyTokenService.refreshToken(refreshToken)).rejects.toEqual(spotifyError);
expect(result).toEqual(mockResponse);
expect(mockedAxiosPost).toHaveBeenCalledWith(
SPOTIFY_TOKEN_URL,
`grant_type=authorization_code&code=${authCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": getExpectedAuthHeader(),
},
}
);
});
it("should propagate errors from the Spotify API", async () => {
const apiError = new Error("Invalid auth code");
mockedAxiosPost.mockRejectedValue(apiError);
await expect(spotifyTokenService.generateToken("invalid-code", "uri")).rejects.toThrow(apiError);
});
});
});
describe("generateToken", () => {
it("should successfully generate token from authorization code", async () => {
const authorizationCode = "test-auth-code";
const redirectUri = "http://localhost:3000/callback";
const mockResponse: OAuthTokenResponse = {
access_token: "access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "refresh-token",
scope: "user-read-playback-state user-modify-playback-state",
};
mockedAxiosPost.mockResolvedValue({ data: mockResponse } as AxiosResponse);
const result = await spotifyTokenService.generateToken(authorizationCode, redirectUri);
expect(mockedAxiosPost).toHaveBeenCalledWith(
"https://accounts.spotify.com/api/token",
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${mockEnv.SPOTIFY_CLIENT_ID}:${mockEnv.SPOTIFY_CLIENT_SECRET}`
).toString("base64")}`,
},
}
);
expect(result).toEqual(mockResponse);
expect(consoleSpy).toHaveBeenCalledWith("generateToken");
expect(consoleSpy).toHaveBeenCalledWith(mockResponse);
});
it("should handle axios errors during token generation", async () => {
const authorizationCode = "test-auth-code";
const redirectUri = "http://localhost:3000/callback";
const error = new Error("Network error");
mockedAxiosPost.mockRejectedValue(error);
await expect(
spotifyTokenService.generateToken(authorizationCode, redirectUri)
).rejects.toThrow("Network error");
expect(mockedAxiosPost).toHaveBeenCalledWith(
"https://accounts.spotify.com/api/token",
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from(
`${mockEnv.SPOTIFY_CLIENT_ID}:${mockEnv.SPOTIFY_CLIENT_SECRET}`
).toString("base64")}`,
},
}
);
});
it("should handle invalid authorization code", async () => {
const authorizationCode = "invalid-auth-code";
const redirectUri = "http://localhost:3000/callback";
const spotifyError = {
response: {
status: 400,
data: {
error: "invalid_grant",
error_description: "Authorization code expired",
},
},
};
mockedAxiosPost.mockRejectedValue(spotifyError);
await expect(
spotifyTokenService.generateToken(authorizationCode, redirectUri)
).rejects.toEqual(spotifyError);
});
it("should handle missing environment variables", async () => {
// Create a service instance with undefined environment variables
const undefinedEnvService = {
async generateToken(authorizationCode: string, redirectUri: string) {
console.log("generateToken");
const response = await axios.post(
"https://accounts.spotify.com/api/token",
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from("undefined:undefined").toString("base64")}`,
},
}
);
console.log(response.data);
return response.data;
}
};
const authorizationCode = "test-auth-code";
const redirectUri = "http://localhost:3000/callback";
const mockResponse: OAuthTokenResponse = {
access_token: "access-token",
token_type: "Bearer",
expires_in: 3600,
refresh_token: "refresh-token",
scope: "user-read-playback-state",
};
mockedAxiosPost.mockResolvedValue({ data: mockResponse } as AxiosResponse);
const result = await undefinedEnvService.generateToken(authorizationCode, redirectUri);
expect(mockedAxiosPost).toHaveBeenCalledWith(
"https://accounts.spotify.com/api/token",
`grant_type=authorization_code&code=${authorizationCode}&redirect_uri=${redirectUri}`,
{
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Authorization: `Basic ${Buffer.from("undefined:undefined").toString("base64")}`,
},
}
);
expect(result).toEqual(mockResponse);
});
});
describe("Authorization header generation", () => {
it("should generate correct base64 encoded authorization header", () => {
const clientId = "test-client-id";
const clientSecret = "test-client-secret";
const expectedAuth = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
expect(Buffer.from(`${clientId}:${clientSecret}`).toString("base64")).toBe(expectedAuth);
});
});
describe("URL and request format", () => {
it("should use correct Spotify token endpoint URL", async () => {
const refreshToken = "test-refresh-token";
mockedAxiosPost.mockResolvedValue({ data: {} } as AxiosResponse);
await spotifyTokenService.refreshToken(refreshToken);
expect(mockedAxiosPost).toHaveBeenCalledWith(
"https://accounts.spotify.com/api/token",
expect.any(String),
expect.any(Object)
);
});
it("should use correct content type for form data", async () => {
const refreshToken = "test-refresh-token";
mockedAxiosPost.mockResolvedValue({ data: {} } as AxiosResponse);
await spotifyTokenService.refreshToken(refreshToken);
const [, , config] = mockedAxiosPost.mock.calls[0];
expect(config.headers["Content-Type"]).toBe("application/x-www-form-urlencoded");
});
});
});
+118
View File
@@ -0,0 +1,118 @@
import express, { Router } from "express";
import { vi, type Mocked } from "vitest";
import { UserService } from "../../src/db/services/db/UserService";
import { PasswordUtils } from "../../src/utils/passwordUtils";
export const defaultMockPayload = {
uuid: "test-user-uuid",
username: "testuser",
id: "test-user-id"
};
/**
* Definiert die Struktur des zurückgegebenen Test-Environments für Typsicherheit.
*/
export interface TestEnvironment {
app: express.Application;
mockUserService: ReturnType<typeof createMockUserService>;
mockPasswordUtils: Mocked<typeof PasswordUtils>;
}
/**
* Erstellt eine Express-App für Testzwecke.
* - Fügt JSON-Parsing-Middleware hinzu.
* - Fügt eine Mock-Authentifizierungs-Middleware hinzu, die eine Payload an die Anfrage anhängt.
* - Bindet den übergebenen Router an einen Basispfad.
* @param router Der zu testende Express-Router.
* @param basePath Der Basispfad, unter dem der Router erreichbar sein soll (z.B. "/user").
* @param payload Die zu simulierende Benutzer-Payload. Standardmäßig wird defaultMockPayload verwendet.
* @returns Eine konfigurierte Express-App-Instanz.
*/
export const createTestApp = (router: Router, basePath: string, payload: object = defaultMockPayload) => {
const app = express();
app.use(express.json());
app.use((req: any, res, next) => {
req.payload = payload;
next();
});
app.use(basePath, router);
return app;
};
/**
* Erstellt ein Mock-Objekt für den UserService mit allen Methoden als vi.fn().
* @returns Ein Mock-UserService-Objekt.
*/
export const createMockUserService = () => ({
getAllUsers: vi.fn(),
getUserByUUID: vi.fn(),
getUserById: vi.fn(),
updateUser: vi.fn(),
updateUserById: vi.fn(),
getUserByName: vi.fn(),
getSpotifyConfigByUUID: vi.fn(),
clearSpotifyConfigByUUID: vi.fn(),
existsUserByName: vi.fn(),
createUser: vi.fn(),
getUserAuthByName: vi.fn(),
});
/**
* Initialisiert die gesamte Testumgebung für einen Controller-Test.
* Erstellt Mocks, eine Test-App und verbindet alles miteinander.
* @param router Der spezifische Router, der getestet werden soll.
* @param basePath Der Basispfad für den Router (z.B. "/user").
* @returns Ein Objekt mit der konfigurierten App und allen Mock-Instanzen.
*/
export const setupTestEnvironment = (router: Router, basePath: string): TestEnvironment => {
const mockUserService = createMockUserService();
vi.mocked(UserService.create).mockResolvedValue(mockUserService);
const mockPasswordUtils = vi.mocked(PasswordUtils);
const app = createTestApp(router, basePath);
return { app, mockUserService, mockPasswordUtils };
};
/**
* Erstellt ein Mock-Objekt für den ExtendedWebSocketServer.
* @returns Ein Mock-WebSocketServer-Objekt.
*/
export const createMockWebSocketServer = () => ({
broadcast: vi.fn(),
sendMessageToUser: vi.fn(),
getConnectedClients: vi.fn(),
});
/**
* Erstellt eine "öffentliche" Express-App für Tests, ohne Mock-Authentifizierung.
* Ideal für Login-, Register- oder andere öffentliche Routen.
* @param router Der zu testende Router.
* @param basePath Der Basispfad für den Router.
* @returns Eine konfigurierte Express-App.
*/
export const createPublicTestApp = (router: Router, basePath: string) => {
const app = express();
app.use(express.json());
app.use(basePath, router);
return app;
};
/**
* Erstellt ein Mock-Objekt für den JwtAuthenticator.
*/
export const createMockJwtAuthenticator = () => ({
generateToken: vi.fn(),
verifyToken: vi.fn(),
});
/**
* Erstellt ein Mock-Objekt für den SpotifyTokenService.
*/
export const createMockSpotifyTokenService = () => ({
refreshToken: vi.fn(),
generateToken: vi.fn(),
});
+86
View File
@@ -0,0 +1,86 @@
import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest";
import request from "supertest";
import express from "express";
import {authLimiter} from "../src/rest/middleware/rateLimit";
vi.mock("../src/db/services/db/database.service", () => ({
connectToDatabase: vi.fn().mockResolvedValue(undefined),
}));
vi.mock("../src/websocket", () => ({
ExtendedWebSocketServer: vi.fn(),
}));
vi.mock("../src/config", () => ({
config: {
port: 3001,
cors: { origin: "http://test-origin.com", credentials: true },
},
}));
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()),
spotifyLimiter: vi.fn((req, res, next) => next()),
};
});
let app: express.Application;
beforeAll(async () => {
const indexModule = await import("../src/index");
app = indexModule.default;
});
describe("Express App Integration Test", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should start and respond to the healthz endpoint", async () => {
const response = await request(app).get("/api/healthz").expect(200);
expect(response.body).toEqual({ status: "ok" });
});
it("should apply CORS headers based on the configuration", async () => {
const response = await request(app)
.options("/api/healthz")
.set("Origin", "http://test-origin.com")
.expect(204);
expect(response.headers['access-control-allow-origin']).toBe("http://test-origin.com");
expect(response.headers['access-control-allow-credentials']).toBe("true");
});
it("should apply security headers to responses", async () => {
const response = await request(app).get("/api/healthz").expect(200);
expect(response.headers['x-frame-options']).toBe('DENY');
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 apply the auth rate limiter to an auth route", async () => {
await request(app).post("/api/auth/login").send({}).expect(400);
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);
});
});
+225
View File
@@ -0,0 +1,225 @@
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";
import crypto from "crypto";
vi.mock("../../src/db/services/db/UserService", () => ({
UserService: {
create: vi.fn(),
},
}));
vi.mock("../../src/utils/passwordUtils", () => ({
PasswordUtils: {
validatePassword: vi.fn(),
hashPassword: vi.fn(),
comparePassword: vi.fn(),
},
}));
vi.mock("../../src/utils/jwtAuthenticator");
vi.mock("crypto", () => ({
default: {
randomUUID: vi.fn(),
},
}));
describe("RestAuth", () => {
let app: express.Application;
let mockUserService: any;
let mockPasswordUtils: any;
let mockJwtAuthenticator: any;
let mockCrypto: any;
beforeEach(() => {
vi.clearAllMocks();
mockUserService = createMockUserService();
vi.mocked(UserService.create).mockResolvedValue(mockUserService);
mockPasswordUtils = vi.mocked(PasswordUtils);
mockCrypto = vi.mocked(crypto);
mockJwtAuthenticator = createMockJwtAuthenticator();
vi.mocked(JwtAuthenticator).mockImplementation(() => mockJwtAuthenticator);
const restAuth = new RestAuth();
app = createPublicTestApp(restAuth.createRouter(), "/auth");
process.env.SECRET_KEY = "test-secret-key";
});
afterEach(() => {
vi.resetAllMocks();
delete process.env.SECRET_KEY;
});
describe("POST /register", () => {
const validRegistrationData = {
username: "testuser",
password: "TestPassword123!",
timezone: "Europe/Berlin",
location: "Berlin, Germany",
};
it("should register a new user successfully", async () => {
const mockUUID = "test-uuid-123";
const hashedPassword = "hashed-password-123";
const createdUser = {
name: "testuser",
uuid: mockUUID,
timezone: "Europe/Berlin",
location: "Berlin, Germany",
config: { isVisible: false, isAdmin: false, canBeModified: false },
};
mockUserService.existsUserByName.mockResolvedValue(false);
mockPasswordUtils.validatePassword.mockReturnValue({ valid: true });
mockPasswordUtils.hashPassword.mockResolvedValue(hashedPassword);
mockCrypto.randomUUID.mockReturnValue(mockUUID);
mockUserService.createUser.mockResolvedValue(createdUser);
const response = await request(app).post("/auth/register").send(validRegistrationData).expect(201);
expect(response.body.ok).toBe(true);
expect(response.body.data.user).toEqual(createdUser);
expect(mockUserService.createUser).toHaveBeenCalledWith({
name: "testuser",
password: hashedPassword,
uuid: mockUUID,
config: { isVisible: false, isAdmin: false, canBeModified: false },
timezone: "Europe/Berlin",
location: "Berlin, Germany",
});
});
it("should return conflict when username already exists", async () => {
mockUserService.existsUserByName.mockResolvedValue(true);
const response = await request(app).post("/auth/register").send(validRegistrationData).expect(409);
expect(response.body.ok).toBe(false);
expect(response.body.data.message).toBe("Username already exists");
});
it("should return bad request for invalid password", async () => {
mockUserService.existsUserByName.mockResolvedValue(false);
mockPasswordUtils.validatePassword.mockReturnValue({
valid: false,
message: "Password is not valid.",
});
const response = await request(app).post("/auth/register").send(validRegistrationData).expect(400);
expect(response.body.ok).toBe(false);
expect(response.body.data.message).toBe("Password is not valid.");
expect(mockPasswordUtils.hashPassword).not.toHaveBeenCalled();
});
it.each([
{ field: "username" },
{ field: "password" },
{ field: "timezone" },
{ field: "location" },
])("should return bad request when $field is missing", async ({ field }) => {
const invalidData = { ...validRegistrationData };
delete (invalidData as any)[field];
const response = await request(app).post("/auth/register").send(invalidData).expect(400);
expect(response.body.ok).toBe(false);
expect(response.body.data.details[0]).toContain(field);
});
it.each([
{ field: "username", value: "", message: "username" },
{ field: "username", value: "ab", message: "username" },
{ field: "password", value: "", message: "password" },
{ field: "password", value: "short", message: "password" },
{ field: "timezone", value: "", message: "timezone" },
{ field: "location", value: "", message: "location" },
])("should return bad request for invalid value in $field", async ({ field, value, message }) => {
const response = await request(app)
.post("/auth/register")
.send({ ...validRegistrationData, [field]: value })
.expect(400);
expect(response.body.ok).toBe(false);
expect(response.body.data.details[0]).toContain(message);
});
});
describe("POST /login", () => {
const validLoginData = { username: "testuser", password: "TestPassword123!" };
it("should login successfully with valid credentials", async () => {
const mockUser = { name: "testuser", password: "hashed", uuid: "uuid-123", id: "user-id-123" };
const mockToken = "jwt-token-123";
mockUserService.getUserAuthByName.mockResolvedValue(mockUser);
mockPasswordUtils.comparePassword.mockResolvedValue(true);
mockJwtAuthenticator.generateToken.mockReturnValue(mockToken);
const response = await request(app).post("/auth/login").send(validLoginData).expect(200);
expect(response.body.ok).toBe(true);
expect(response.body.data.token).toBe(mockToken);
expect(mockJwtAuthenticator.generateToken).toHaveBeenCalledWith({
username: "testuser",
id: "user-id-123",
uuid: "uuid-123",
});
});
it("should handle user with _id instead of id", async () => {
const mockUser = { name: "testuser", password: "hashed", uuid: "uuid-123", _id: "user-id-123" };
const mockToken = "jwt-token-123";
mockUserService.getUserAuthByName.mockResolvedValue(mockUser);
mockPasswordUtils.comparePassword.mockResolvedValue(true);
mockJwtAuthenticator.generateToken.mockReturnValue(mockToken);
await request(app).post("/auth/login").send(validLoginData).expect(200);
expect(mockJwtAuthenticator.generateToken).toHaveBeenCalledWith({
username: "testuser",
id: "user-id-123",
uuid: "uuid-123",
});
});
it("should return not found when user does not exist", async () => {
mockUserService.getUserAuthByName.mockResolvedValue(null);
const response = await request(app).post("/auth/login").send(validLoginData).expect(404);
expect(response.body.ok).toBe(false);
expect(response.body.data.message).toBe("User not found");
});
it("should return unauthorized for invalid password", async () => {
const mockUser = { name: "testuser", password: "hashed" };
mockUserService.getUserAuthByName.mockResolvedValue(mockUser);
mockPasswordUtils.comparePassword.mockResolvedValue(false);
const response = await request(app).post("/auth/login").send(validLoginData).expect(401);
expect(response.body.ok).toBe(false);
expect(response.body.data.message).toBe("Invalid password");
});
it.each([
{ field: "username", value: "" },
{ field: "password", value: "" },
{ field: "username", value: undefined },
{ field: "password", value: undefined },
])("should return bad request if $field is '$value'", async ({ field, value }) => {
const invalidData = { ...validLoginData };
if (value === undefined) {
delete (invalidData as any)[field];
} else {
(invalidData as any)[field] = value;
}
const response = await request(app).post("/auth/login").send(invalidData).expect(400);
expect(response.body.ok).toBe(false);
expect(response.body.data.details[0]).toContain(field);
});
});
});
@@ -0,0 +1,59 @@
import { describe, it, expect } from "vitest";
import request from "supertest";
import { JwtTokenPropertiesExtractor } from "../../src/rest/jwtTokenPropertiesExtractor";
import { createTestApp } from "../helpers/testSetup";
describe("JwtTokenPropertiesExtractor", () => {
describe("Standard Payload Extraction", () => {
const standardPayload = {
id: "test-user-id-123",
username: "testuser",
uuid: "test-user-uuid-456",
};
const jwtExtractor = new JwtTokenPropertiesExtractor();
const app = createTestApp(jwtExtractor.createRouter(), "/jwt", standardPayload);
it.each([
{ endpoint: "id", expectedValue: standardPayload.id },
{ endpoint: "username", expectedValue: standardPayload.username },
{ endpoint: "uuid", expectedValue: standardPayload.uuid },
])("GET /$endpoint should return the correct value from JWT payload", async ({ endpoint, expectedValue }) => {
const response = await request(app).get(`/jwt/${endpoint}`).expect(200);
expect(response.body.data).toBe(expectedValue);
});
});
describe("Edge Case Payload Extraction", () => {
const edgeCasePayload = {
id: 12345,
username: undefined,
uuid: "",
};
const jwtExtractor = new JwtTokenPropertiesExtractor();
const edgeCaseApp = createTestApp(jwtExtractor.createRouter(), "/jwt", edgeCasePayload);
it.each([
{ endpoint: "id", expectedValue: edgeCasePayload.id },
{ endpoint: "username", expectedValue: edgeCasePayload.username },
{ endpoint: "uuid", expectedValue: edgeCasePayload.uuid },
])("should handle $endpoint with edge case value '$expectedValue' gracefully", async ({ endpoint, expectedValue }) => {
const response = await request(edgeCaseApp).get(`/jwt/${endpoint}`).expect(200);
if (expectedValue === undefined) {
expect(response.body.data).toBeUndefined();
} else {
expect(response.body.data).toBe(expectedValue);
}
});
it("should handle null values gracefully", async () => {
const jwtExtractor = new JwtTokenPropertiesExtractor();
const nullPayloadApp = createTestApp(jwtExtractor.createRouter(), "/jwt", { id: null });
const response = await request(nullPayloadApp).get('/jwt/id').expect(200);
expect(response.body.data).toBeNull();
});
});
});
@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeEach, type Mocked } from "vitest";
import { Request, Response, NextFunction } from "express";
import { asyncHandler } from "../../../src/rest/middleware/asyncHandler";
describe("asyncHandler", () => {
let mockReq: Mocked<Request>;
let mockRes: Mocked<Response>;
let mockNext: Mocked<NextFunction>;
beforeEach(() => {
mockReq = {} as Mocked<Request>;
mockRes = {
status: vi.fn().mockReturnThis(),
json: vi.fn(),
} as unknown as Mocked<Response>;
mockNext = vi.fn();
});
describe("Success Scenarios", () => {
it("should call the wrapped function with all parameters and not call next on success", async () => {
const mockAsyncFn = vi.fn().mockResolvedValue("success");
const wrappedHandler = asyncHandler(mockAsyncFn);
await wrappedHandler(mockReq, mockRes, mockNext);
expect(mockAsyncFn).toHaveBeenCalledWith(mockReq, mockRes, mockNext);
expect(mockNext).not.toHaveBeenCalled();
});
it("should handle functions that return non-promise values correctly", async () => {
const mockSyncFn = vi.fn().mockReturnValue("immediate success");
const wrappedHandler = asyncHandler(mockSyncFn);
await wrappedHandler(mockReq, mockRes, mockNext);
expect(mockSyncFn).toHaveBeenCalledWith(mockReq, mockRes, mockNext);
expect(mockNext).not.toHaveBeenCalled();
});
});
describe("Error Scenarios", () => {
it.each([
{
description: "a rejected promise with an Error object",
error: new Error("Test error"),
setup: (fn: Mocked<any>) => fn.mockRejectedValue(new Error("Test error")),
},
{
description: "a synchronously thrown Error",
error: new Error("Sync error"),
setup: (fn: Mocked<any>) => fn.mockImplementation(() => { throw new Error("Sync error"); }),
},
{
description: "a rejected promise with a string",
error: "String error",
setup: (fn: Mocked<any>) => fn.mockRejectedValue("String error"),
},
{
description: "a rejected promise with null",
error: null,
setup: (fn: Mocked<any>) => fn.mockRejectedValue(null),
},
])("should call next with the error when the function fails with $description", async ({ error, setup }) => {
const mockFailingFn = vi.fn();
setup(mockFailingFn);
const wrappedHandler = asyncHandler(mockFailingFn);
await wrappedHandler(mockReq, mockRes, mockNext);
expect(mockFailingFn).toHaveBeenCalledWith(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalledWith(error);
});
});
});
+49 -182
View File
@@ -1,203 +1,70 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import { Request, Response, NextFunction } from "express";
import { authenticateJwt } from "../../../src/rest/middleware/authenticateJwt";
import { JwtAuthenticator } from "../../../src/utils/jwtAuthenticator";
import { createMockJwtAuthenticator } from "../../helpers/testSetup";
vi.mock("../../../src/utils/jwtAuthenticator", () => ({
JwtAuthenticator: vi.fn().mockImplementation(() => ({
verifyToken: vi.fn(),
})),
}));
const MockedJwtAuthenticator = vi.mocked(JwtAuthenticator);
vi.stubGlobal("process", {
env: {
SECRET_KEY: "test-secret-key",
},
});
vi.mock("../../../src/utils/jwtAuthenticator");
describe("authenticateJwt middleware", () => {
let mockJwtAuthenticatorInstance: any;
let req: any;
let res: any;
let next: any;
let consoleSpy: any;
let mockJwtInstance: ReturnType<typeof createMockJwtAuthenticator>;
let req: Mocked<Request>;
let res: Mocked<Response>;
let next: Mocked<NextFunction>;
beforeEach(() => {
vi.clearAllMocks();
beforeEach(() => {
vi.clearAllMocks();
vi.stubEnv('SECRET_KEY', 'test-secret-key');
mockJwtAuthenticatorInstance = {
verifyToken: vi.fn(),
};
MockedJwtAuthenticator.mockReturnValue(mockJwtAuthenticatorInstance);
mockJwtInstance = createMockJwtAuthenticator();
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
vi.mocked(JwtAuthenticator).mockImplementation(() => mockJwtInstance as any);
req = {
headers: {},
payload: undefined,
};
res = {
status: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
};
next = vi.fn();
});
describe("successful authentication", () => {
it("should authenticate valid JWT token and set payload", () => {
const mockPayload = {
uuid: "test-uuid-123",
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + 3600,
};
req.headers.authorization = "Bearer valid-jwt-token";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(mockPayload);
authenticateJwt(req, res, next);
expect(MockedJwtAuthenticator).toHaveBeenCalledWith("test-secret-key");
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("valid-jwt-token");
expect(consoleSpy).toHaveBeenCalledWith(mockPayload);
expect(req.payload).toEqual(mockPayload);
expect(next).toHaveBeenCalledOnce();
expect(res.status).not.toHaveBeenCalled();
expect(res.send).not.toHaveBeenCalled();
req = { headers: {} } as Mocked<Request>;
res = {
status: vi.fn().mockReturnThis(),
send: vi.fn().mockReturnThis(),
} as unknown as Mocked<Response>;
next = vi.fn();
});
it("should work with different authorization header formats", () => {
const mockPayload = { uuid: "test-uuid-456" };
req.headers.authorization = "bearer another-valid-token";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(mockPayload);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("another-valid-token");
expect(req.payload).toEqual(mockPayload);
expect(next).toHaveBeenCalledOnce();
});
});
describe("missing authorization header", () => {
it("should return 401 when no authorization header is provided", () => {
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith(undefined);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
expect(req.payload).toBeUndefined();
afterEach(() => {
vi.unstubAllEnvs();
});
it("should return 401 when authorization header is empty", () => {
req.headers.authorization = "";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
describe("Success Scenarios", () => {
it("should authenticate a valid token, set req.payload, and call next", () => {
const mockPayload = { uuid: "test-uuid-123" };
req.headers.authorization = "Bearer valid-jwt-token";
mockJwtInstance.verifyToken.mockReturnValue(mockPayload);
authenticateJwt(req, res, next);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("");
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
});
});
describe("invalid authorization header format", () => {
it("should return 401 when authorization header doesn't start with Bearer", () => {
req.headers.authorization = "Bearer whatever-auth";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("whatever-auth");
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
expect(vi.mocked(JwtAuthenticator)).toHaveBeenCalledWith("test-secret-key");
expect(mockJwtInstance.verifyToken).toHaveBeenCalledWith("valid-jwt-token");
expect(req.payload).toEqual(mockPayload);
expect(next).toHaveBeenCalledOnce();
expect(res.status).not.toHaveBeenCalled();
});
});
it("should return 401 when authorization header has no token", () => {
req.headers.authorization = "Bearer";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
describe("Failure Scenarios", () => {
it.each([
{ description: "no authorization header", authHeader: undefined, expectedToken: undefined },
{ description: "empty authorization header", authHeader: "", expectedToken: "" },
{ description: "header with only 'Bearer '", authHeader: "Bearer ", expectedToken: "" },
{ description: "an invalid/expired token", authHeader: "Bearer invalid-token", expectedToken: "invalid-token" },
])("should return 401 Unauthorized when there is $description", ({ authHeader, expectedToken }) => {
req.headers.authorization = authHeader;
mockJwtInstance.verifyToken.mockReturnValue(null); // Alle Fehlerfälle führen zu null
authenticateJwt(req, res, next);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("");
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
expect(mockJwtInstance.verifyToken).toHaveBeenCalledWith(expectedToken);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
});
});
it("should return 401 when authorization header has only Bearer with space", () => {
req.headers.authorization = "Bearer ";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("");
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
});
});
describe("JWT verification errors", () => {
it("should return 401 when JWT is invalid", () => {
req.headers.authorization = "Bearer invalid-jwt-token";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
authenticateJwt(req, res, next);
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("invalid-jwt-token");
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
});
it("should return 401 when JWT is expired", () => {
req.headers.authorization = "Bearer expired-jwt-token";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
authenticateJwt(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith("Unauthorized");
expect(next).not.toHaveBeenCalled();
});
it("should return 401 when JWT signature is invalid", () => {
req.headers.authorization = "Bearer tampered-jwt-token";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(null);
authenticateJwt(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(next).not.toHaveBeenCalled();
});
});
describe("environment configuration", () => {
it("should use SECRET_KEY from environment", () => {
const mockPayload = { uuid: "test-uuid" };
req.headers.authorization = "Bearer test-token";
mockJwtAuthenticatorInstance.verifyToken.mockReturnValue(mockPayload);
authenticateJwt(req, res, next);
expect(MockedJwtAuthenticator).toHaveBeenCalledWith("test-secret-key");
expect(mockJwtAuthenticatorInstance.verifyToken).toHaveBeenCalledWith("test-token");
expect(req.payload).toEqual(mockPayload);
expect(next).toHaveBeenCalledOnce();
});
});
});
+13
View File
@@ -67,6 +67,19 @@ describe("v.isArrayLength", () => {
});
});
describe("v.isObject", () => {
it("accepts objects", () => {
expect(v.isObject()({})).toBe(true);
expect(v.isObject()({ a: 1 })).toBe(true);
expect(v.isObject()("not object")).toBe("must be an object");
});
it("forces nonEmpty", () => {
expect(v.isObject({ nonEmpty: true })({})).toBe("must be a non-empty object");
expect(v.isObject({ nonEmpty: true })({ a: 1 })).toBe(true);
});
});
describe("v.isUrl", () => {
it("accepts valid urls", () => {
expect(v.isUrl()("https://example.com")).toBe(true);
+391
View File
@@ -0,0 +1,391 @@
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";
vi.mock("../../src/db/services/db/UserService", () => ({
UserService: {
create: vi.fn()
}
}));
vi.mock("../../src/utils/passwordUtils", () => ({
PasswordUtils: {
validatePassword: vi.fn(),
hashPassword: vi.fn(),
comparePassword: vi.fn()
}
}));
describe("RestUser", () => {
let testEnv: TestEnvironment;
beforeEach(() => {
vi.clearAllMocks();
const restUser = new RestUser();
testEnv = setupTestEnvironment(restUser.createRouter(), "/user");
});
afterEach(() => {
vi.resetAllMocks();
});
describe("GET /", () => {
it("should return all users", async () => {
const mockUsers = [
{id: "1", name: "user1", uuid: "uuid1"},
{id: "2", name: "user2", uuid: "uuid2"}
];
testEnv.mockUserService.getAllUsers.mockResolvedValue(mockUsers);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual(mockUsers);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
});
it("should handle empty user list", async () => {
testEnv. mockUserService.getAllUsers.mockResolvedValue([]);
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(response.body.data.users).toEqual([]);
});
});
describe("GET /me", () => {
it("should return current user", async () => {
const mockUser = {
id: "test-user-id",
name: "testuser",
uuid: "test-user-uuid"
};
testEnv.mockUserService.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");
});
});
describe("PUT /me/spotify", () => {
const validSpotifyData = {
accessToken: "access-token-123",
refreshToken: "refresh-token-123",
scope: "user-read-playback-state",
expirationDate: "2024-12-31T23:59:59.000Z"
};
it("should update user spotify config successfully", async () => {
const mockUser = {
id: "test-user-id",
name: "testuser",
uuid: "test-user-uuid",
spotifyConfig: null
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
testEnv.mockUserService.updateUser.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(validSpotifyData)
.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({
...mockUser,
spotifyConfig: {
accessToken: "access-token-123",
refreshToken: "refresh-token-123",
scope: "user-read-playback-state",
expirationDate: new Date("2024-12-31T23:59:59.000Z")
}
});
});
it("should return bad request when user not found", async () => {
testEnv. mockUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(validSpotifyData)
.expect(400);
expect(response.body.data.message).toBe("User not found");
});
it("should return bad request for missing accessToken", async () => {
const invalidData: Partial<typeof validSpotifyData> = {...validSpotifyData};
delete invalidData.accessToken;
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("accessToken");
});
it("should return bad request for missing refreshToken", async () => {
const invalidData: Partial<typeof validSpotifyData> = {...validSpotifyData};
delete invalidData.refreshToken;
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("refreshToken");
});
it("should return bad request for missing scope", async () => {
const invalidData: Partial<typeof validSpotifyData> = {...validSpotifyData};
delete invalidData.scope;
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("scope");
});
it("should return bad request for missing expirationDate", async () => {
const invalidData: Partial<typeof validSpotifyData> = {...validSpotifyData};
delete invalidData.expirationDate;
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("expirationDate");
});
it("should return bad request for empty accessToken", async () => {
const invalidData = {...validSpotifyData, accessToken: ""};
const response = await request(testEnv.app)
.put("/user/me/spotify")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("accessToken");
});
});
describe("DELETE /me/spotify", () => {
it("should clear spotify config successfully", async () => {
const mockUser = {
id: "test-user-id",
name: "testuser",
uuid: "test-user-uuid"
};
const updatedUser = {
...mockUser,
spotifyConfig: null
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
testEnv.mockUserService.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");
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.delete("/user/me/spotify")
.expect(400);
expect(response.body.data.message).toBe("User not found");
});
});
describe("PUT /me/password", () => {
const validPasswordData = {
password: "newpassword123",
passwordConfirmation: "newpassword123"
};
it("should update password successfully", async () => {
const {PasswordUtils} = await import("../../src/utils/passwordUtils");
const mockUser = {
id: "test-user-id",
name: "testuser",
uuid: "test-user-uuid",
password: "old-hashed-password"
};
testEnv. mockUserService.getUserByUUID.mockResolvedValue(mockUser);
vi.mocked(PasswordUtils.validatePassword).mockReturnValue({valid: true});
vi.mocked(PasswordUtils.hashPassword).mockResolvedValue("new-hashed-password");
testEnv.mockUserService.updateUser.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.put("/user/me/password")
.send(validPasswordData)
.expect(200);
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({
...mockUser,
password: "new-hashed-password"
});
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserByUUID.mockResolvedValue(null);
const response = await request(testEnv.app)
.put("/user/me/password")
.send(validPasswordData)
.expect(400);
expect(response.body.data.message).toBe("User not found");
});
it("should return bad request when passwords don't match", async () => {
const mockUser = {
id: "test-user-id",
name: "testuser",
uuid: "test-user-uuid"
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
const invalidData = {
password: "newpassword123",
passwordConfirmation: "differentpassword"
};
const response = await request(testEnv.app)
.put("/user/me/password")
.send(invalidData)
.expect(400);
expect(response.body.data.message).toBe("Passwörter stimmen nicht überein");
});
it("should return bad request for invalid password", async () => {
const {PasswordUtils} = await import("../../src/utils/passwordUtils");
const mockUser = {
id: "test-user-id",
name: "testuser",
uuid: "test-user-uuid"
};
testEnv.mockUserService.getUserByUUID.mockResolvedValue(mockUser);
vi.mocked(PasswordUtils.validatePassword).mockReturnValue({
valid: false,
message: "Password too weak"
});
const response = await request(testEnv.app)
.put("/user/me/password")
.send(validPasswordData)
.expect(400);
expect(response.body.data.message).toBe("Password too weak");
});
it("should return bad request for missing password", async () => {
const invalidData = {passwordConfirmation: "newpassword123"};
const response = await request(testEnv.app)
.put("/user/me/password")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("password");
});
it("should return bad request for missing passwordConfirmation", async () => {
const invalidData = {password: "newpassword123"};
const response = await request(testEnv.app)
.put("/user/me/password")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("passwordConfirmation");
});
it("should return bad request for short password", async () => {
const invalidData = {
password: "short",
passwordConfirmation: "short"
};
const response = await request(testEnv.app)
.put("/user/me/password")
.send(invalidData)
.expect(400);
expect(response.body.data.details[0]).toContain("password");
});
});
describe("GET /:id", () => {
it("should return user by id", async () => {
const mockUser = {
id: "specific-user-id",
name: "specificuser",
uuid: "specific-uuid"
};
testEnv.mockUserService.getUserById.mockResolvedValue(mockUser);
const response = await request(testEnv.app)
.get("/user/specific-user-id")
.expect(200);
expect(response.body.data).toEqual(mockUser);
expect(testEnv.mockUserService.getUserById).toHaveBeenCalledWith("specific-user-id");
});
it("should return bad request when user not found", async () => {
testEnv.mockUserService.getUserById.mockResolvedValue(null);
const response = await request(testEnv.app)
.get("/user/nonexistent-id")
.expect(400);
expect(response.body.data.message).toBe("Unable to find matching document with id: nonexistent-id");
});
it("should return all users when id is empty", async () => {
const response = await request(testEnv.app)
.get("/user/")
.expect(200);
expect(testEnv.mockUserService.getAllUsers).toHaveBeenCalled();
});
});
});
+105
View File
@@ -0,0 +1,105 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import request from "supertest";
import express from "express";
import { RestWebSocket } from "../../src/rest/restWebSocket";
import { createTestApp, createMockWebSocketServer } from "../helpers/testSetup";
vi.mock("../../src/websocket", () => ({
ExtendedWebSocketServer: vi.fn()
}));
describe("RestWebSocket", () => {
let app: express.Application;
let mockWebSocketServer: ReturnType<typeof createMockWebSocketServer>;
beforeEach(() => {
vi.clearAllMocks();
mockWebSocketServer = createMockWebSocketServer();
const restWebSocket = new RestWebSocket(mockWebSocketServer as any);
app = createTestApp(restWebSocket.createRouter(), "/websocket");
});
afterEach(() => {
vi.resetAllMocks();
});
describe("POST /broadcast", () => {
it("should broadcast a complex object payload", async () => {
const payload = { type: "update", data: { a: 1 } };
await request(app).post("/websocket/broadcast").send({ payload }).expect(200);
expect(mockWebSocketServer.broadcast).toHaveBeenCalledWith(JSON.stringify(payload));
});
it("should return bad request for missing payload", async () => {
const response = await request(app).post("/websocket/broadcast").send({}).expect(400);
expect(response.body.data.details).toContain("payload is required");
expect(mockWebSocketServer.broadcast).not.toHaveBeenCalled();
});
});
describe("POST /send-message", () => {
const validPayload = { type: "private_message", content: "Hello!" };
it("should send message to specific users", async () => {
const users = ["user1", "user2"];
await request(app).post("/websocket/send-message").send({ payload: validPayload, users }).expect(200);
expect(mockWebSocketServer.sendMessageToUser).toHaveBeenCalledTimes(2);
expect(mockWebSocketServer.sendMessageToUser).toHaveBeenCalledWith("user1", JSON.stringify(validPayload));
expect(mockWebSocketServer.sendMessageToUser).toHaveBeenCalledWith("user2", JSON.stringify(validPayload));
});
it("should return bad request for missing payload", async () => {
const response = await request(app).post("/websocket/send-message").send({ users: ["user1"] }).expect(400);
expect(response.body.data.details).toContain("payload is required");
});
it("should return bad request for missing users", async () => {
const response = await request(app).post("/websocket/send-message").send({ payload: validPayload }).expect(400);
expect(response.body.data.details).toContain("users is required");
});
it.each([
{ description: "an empty array", users: [] },
{ description: "a non-array value", users: "user1" },
{ description: "an array with non-strings", users: ["user1", 123] },
{ description: "an array with empty strings", users: ["user1", ""] },
{ description: "an array with whitespace-only strings", users: ["user1", " "] },
])("should return bad request for users being $description", async ({ users }) => {
const response = await request(app)
.post("/websocket/send-message")
.send({ payload: validPayload, users })
.expect(400);
expect(response.body.data.details).toContain("users must be a non-empty array of strings");
expect(mockWebSocketServer.sendMessageToUser).not.toHaveBeenCalled();
});
});
describe("GET /all-clients", () => {
it("should return all connected clients", async () => {
const mockClients = new Set([
{ payload: { uuid: "user1", username: "alice" } },
{ payload: { uuid: "user2", username: "bob" } },
]);
mockWebSocketServer.getConnectedClients.mockReturnValue(mockClients);
const response = await request(app).get("/websocket/all-clients").expect(200);
expect(response.body.data.result).toEqual([
{ uuid: "user1", username: "alice" },
{ uuid: "user2", username: "bob" },
]);
expect(mockWebSocketServer.getConnectedClients).toHaveBeenCalled();
});
it("should return an empty array when no clients are connected", async () => {
mockWebSocketServer.getConnectedClients.mockReturnValue(new Set());
const response = await request(app).get("/websocket/all-clients").expect(200);
expect(response.body.data.result).toEqual([]);
});
});
});
+90
View File
@@ -0,0 +1,90 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import request from "supertest";
import express from "express";
import { SpotifyTokenGenerator } from "../../src/rest/spotifyTokenGenerator";
import { SpotifyTokenService } from "../../src/db/services/spotifyTokenService";
import { createTestApp, createMockSpotifyTokenService } from "../helpers/testSetup";
vi.mock("../../src/db/services/spotifyTokenService");
describe("SpotifyTokenGenerator", () => {
let app: express.Application;
let mockTokenService: ReturnType<typeof createMockSpotifyTokenService>;
beforeEach(() => {
vi.clearAllMocks();
mockTokenService = createMockSpotifyTokenService();
vi.mocked(SpotifyTokenService).mockImplementation(() => mockTokenService as any);
const spotifyGenerator = new SpotifyTokenGenerator();
app = createTestApp(spotifyGenerator.createRouter(), "/spotify");
});
afterEach(() => {
vi.resetAllMocks();
});
describe("POST /token/refresh", () => {
const validRefreshData = { refreshToken: "valid-refresh-token-123" };
it("should refresh token successfully", async () => {
const mockTokenResponse = { access_token: "new-access-token" };
mockTokenService.refreshToken.mockResolvedValue(mockTokenResponse);
const response = await request(app).post("/spotify/token/refresh").send(validRefreshData).expect(200);
expect(response.body.data.token).toEqual(mockTokenResponse);
expect(mockTokenService.refreshToken).toHaveBeenCalledWith("valid-refresh-token-123");
});
it("should handle token service errors", async () => {
mockTokenService.refreshToken.mockRejectedValue(new Error("Spotify API error"));
const response = await request(app).post("/spotify/token/refresh").send(validRefreshData).expect(500);
expect(response.body.data.message).toBe("Failed to handle spotify token request");
});
it.each([
{ description: "missing refreshToken", body: {}, expectedError: "refreshToken" },
{ description: "empty refreshToken", body: { refreshToken: "" }, expectedError: "refreshToken" },
{ description: "non-string refreshToken", body: { refreshToken: 123 }, expectedError: "refreshToken" },
])("should return bad request for $description", async ({ body, expectedError }) => {
const response = await request(app).post("/spotify/token/refresh").send(body).expect(400);
expect(response.body.data.details[0]).toContain(expectedError);
expect(mockTokenService.refreshToken).not.toHaveBeenCalled();
});
});
describe("POST /token/generate", () => {
const validGenerateData = { authCode: "valid-auth-code", redirectUri: "http://localhost:3000/callback" };
it("should generate token successfully", async () => {
const mockTokenResponse = { access_token: "generated-access-token" };
mockTokenService.generateToken.mockResolvedValue(mockTokenResponse);
const response = await request(app).post("/spotify/token/generate").send(validGenerateData).expect(200);
expect(response.body.data.token).toEqual(mockTokenResponse);
expect(mockTokenService.generateToken).toHaveBeenCalledWith(validGenerateData.authCode, validGenerateData.redirectUri);
});
it("should handle token service errors", async () => {
mockTokenService.generateToken.mockRejectedValue(new Error("Invalid auth code"));
const response = await request(app).post("/spotify/token/generate").send(validGenerateData).expect(500);
expect(response.body.data.message).toBe("Failed to handle spotify token request");
});
it.each([
{ description: "missing authCode", body: { redirectUri: "http://uri" }, expectedError: "authCode" },
{ description: "missing redirectUri", body: { authCode: "code" }, expectedError: "redirectUri" },
{ description: "empty authCode", body: { authCode: "", redirectUri: "http://uri" }, expectedError: "authCode" },
{ description: "invalid redirectUri", body: { authCode: "code", redirectUri: "not-a-url" }, expectedError: "redirectUri" },
])("should return bad request for $description", async ({ body, expectedError }) => {
const response = await request(app).post("/spotify/token/generate").send(body).expect(400);
expect(response.body.data.details[0]).toContain(expectedError);
expect(mockTokenService.generateToken).not.toHaveBeenCalled();
});
});
});
@@ -1,40 +1,77 @@
import { describe, it, expect, vi } from "vitest";
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";
type MockWs = {
user: {
timezone: string;
lastState: { global: { mode: string; brightness: number } };
};
send: Mocked<(data: any, options: { binary: boolean }) => void>;
};
describe("websocketEventUtils.getEventListeners", () => {
function makeWs() {
return {
let mockWs: MockWs;
let listeners: CustomWebsocketEvent[];
beforeEach(() => {
mockWs = {
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);
listeners = getEventListeners(mockWs as any);
});
it("should return an array of event listener objects", () => {
expect(Array.isArray(listeners)).toBe(true);
expect(listeners.length).toBeGreaterThan(0);
const byType = Object.fromEntries(listeners.map(l => [l.event, l]));
for (const listener of listeners) {
expect(listener).toHaveProperty("event");
expect(listener).toHaveProperty("handler");
expect(typeof listener.handler).toBe("function");
}
});
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 },
);
describe("GET_STATE event handler", () => {
it("should include a handler for GET_STATE", () => {
const getStateListener = listeners.find(l => l.event === WebsocketEventType.GET_STATE);
expect(getStateListener).toBeDefined();
});
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 },
);
it("should send the user's last state when the handler is called", () => {
const getStateListener = listeners.find(l => l.event === WebsocketEventType.GET_STATE);
getStateListener!.handler({});
expect(mockWs.send).toHaveBeenCalledOnce();
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: "STATE", payload: mockWs.user.lastState }),
{ binary: false }
);
});
});
describe("GET_SETTINGS event handler", () => {
it("should include a handler for GET_SETTINGS", () => {
const getSettingsListener = listeners.find(l => l.event === WebsocketEventType.GET_SETTINGS);
expect(getSettingsListener).toBeDefined();
});
it("should send the user's timezone when the handler is called", () => {
const getSettingsListener = listeners.find(l => l.event === WebsocketEventType.GET_SETTINGS);
getSettingsListener!.handler({});
expect(mockWs.send).toHaveBeenCalledOnce();
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: "SETTINGS", payload: { timezone: mockWs.user.timezone } }),
{ binary: false }
);
});
});
});
@@ -0,0 +1,117 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import { WebsocketEventHandler } from "../../../src/utils/websocket/websocketEventHandler";
import { ExtendedWebSocket } from "../../../src/interfaces/extendedWebsocket";
import { CustomWebsocketEvent } from "../../../src/utils/websocket/websocketCustomEvents/customWebsocketEvent";
describe("WebsocketEventHandler", () => {
let mockWebSocket: Mocked<ExtendedWebSocket>;
let websocketEventHandler: WebsocketEventHandler;
let registeredHandlers: Map<string, (...args: any[]) => void>;
beforeEach(() => {
vi.clearAllMocks();
vi.spyOn(console, "error").mockImplementation(() => {});
vi.spyOn(console, "log").mockImplementation(() => {});
registeredHandlers = new Map();
mockWebSocket = {
on: vi.fn((event, handler) => {
registeredHandlers.set(event, handler);
return mockWebSocket;
}),
emit: vi.fn(),
isAlive: false,
payload: { username: "testuser", uuid: "test-uuid", id: "test-id" },
asyncUpdates: new Map([["update1", 123], ["update2", 456]]),
} as unknown as Mocked<ExtendedWebSocket>;
websocketEventHandler = new WebsocketEventHandler(mockWebSocket);
});
afterEach(() => {
vi.restoreAllMocks();
});
it("should register an error event handler", () => {
websocketEventHandler.enableErrorEvent();
expect(mockWebSocket.on).toHaveBeenCalledWith("error", console.error);
});
it("should register a pong event handler that sets isAlive to true", () => {
websocketEventHandler.enablePongEvent();
const pongHandler = registeredHandlers.get("pong");
expect(pongHandler).toBeDefined();
pongHandler!();
expect(mockWebSocket.isAlive).toBe(true);
expect(console.log).toHaveBeenCalledWith("Pong received");
});
describe("enableDisconnectEvent", () => {
it("should set onclose handler, clear async updates, and call the callback", () => {
const mockCallback = vi.fn();
const clearIntervalSpy = vi.spyOn(global, "clearInterval");
websocketEventHandler.enableDisconnectEvent(mockCallback);
expect(mockWebSocket.onclose).toBeInstanceOf(Function);
mockWebSocket.onclose!({ code: 1000, reason: "Normal" } as any);
expect(console.log).toHaveBeenCalledWith("User: testuser disconnected");
expect(clearIntervalSpy).toHaveBeenCalledWith(123);
expect(clearIntervalSpy).toHaveBeenCalledWith(456);
expect(mockCallback).toHaveBeenCalledOnce();
});
it("should handle disconnect with no async updates", () => {
const mockCallback = vi.fn();
mockWebSocket.asyncUpdates = new Map();
const clearIntervalSpy = vi.spyOn(global, "clearInterval");
websocketEventHandler.enableDisconnectEvent(mockCallback);
mockWebSocket.onclose!({ code: 1000, reason: "Normal" } as any);
expect(clearIntervalSpy).not.toHaveBeenCalled();
expect(mockCallback).toHaveBeenCalledOnce();
});
});
describe("enableMessageEvent", () => {
it("should parse incoming JSON messages and emit them as typed events", () => {
websocketEventHandler.enableMessageEvent();
const messageHandler = registeredHandlers.get("message");
expect(messageHandler).toBeDefined();
const message = { type: "test_event", data: { value: 42 } };
const rawData = { toString: () => JSON.stringify(message) };
messageHandler!(rawData);
expect(console.log).toHaveBeenCalledWith("Received message:", JSON.stringify(message));
expect(mockWebSocket.emit).toHaveBeenCalledWith("test_event", message);
});
});
describe("registerCustomEvent", () => {
it("should register a custom event with its handler bound to the event object", () => {
const mockHandler = vi.fn();
const customEvent: CustomWebsocketEvent = {
event: "custom_event",
handler: mockHandler,
} as any;
// @ts-ignore
const bindSpy = vi.spyOn(customEvent.handler, "bind");
websocketEventHandler.registerCustomEvent(customEvent);
expect(mockWebSocket.on).toHaveBeenCalledWith("custom_event", expect.any(Function));
expect(bindSpy).toHaveBeenCalledWith(customEvent);
});
});
});
+133
View File
@@ -0,0 +1,133 @@
import { describe, it, expect, vi, beforeEach, type Mocked } from "vitest";
import { Server } from "http";
import { WebSocket, Server as WebSocketServer } from "ws";
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";
let mockWssInstance: Mocked<WebSocketServer>;
let mockServerEventHandler: Mocked<WebsocketServerEventHandler>;
vi.mock("ws", () => ({
Server: vi.fn().mockImplementation(() => mockWssInstance),
WebSocket: { OPEN: 1, CLOSED: 3 },
}));
vi.mock("../src/utils/verifyClient");
vi.mock("../src/utils/websocket/websocketServerEventHandler", () => ({
WebsocketServerEventHandler: vi.fn().mockImplementation(() => mockServerEventHandler),
}));
vi.mock("../src/utils/websocket/websocketEventHandler");
vi.mock("../src/utils/websocket/websocketCustomEvents/websocketEventUtils");
describe("ExtendedWebSocketServer", () => {
let mockHttpServer: Mocked<Server>;
let extendedWss: ExtendedWebSocketServer;
beforeEach(() => {
vi.clearAllMocks();
mockHttpServer = {} as Mocked<Server>;
mockServerEventHandler = {
enableConnectionEvent: vi.fn(),
enableHeartbeat: vi.fn(),
enableCloseEvent: vi.fn(),
} as unknown as Mocked<WebsocketServerEventHandler>;
mockWssInstance = {
clients: new Set(),
on: vi.fn(),
close: vi.fn(),
} as unknown as Mocked<WebSocketServer>;
extendedWss = new ExtendedWebSocketServer(mockHttpServer);
});
describe("Constructor and Setup", () => {
it("should create a new WebSocket.Server", () => {
expect(WebSocketServer).toHaveBeenCalledWith({
server: mockHttpServer,
verifyClient: expect.any(Function),
});
});
it("should create and use a WebsocketServerEventHandler", () => {
expect(WebsocketServerEventHandler).toHaveBeenCalledWith(mockWssInstance);
});
it("should enable the heartbeat", () => {
expect(mockServerEventHandler.enableHeartbeat).toHaveBeenCalledWith(30000);
});
it("should register a connection handler", () => {
expect(mockServerEventHandler.enableConnectionEvent).toHaveBeenCalledWith(expect.any(Function));
});
});
describe("broadcast", () => {
it("should send a message to all connected clients that are OPEN", () => {
const client1 = { readyState: WebSocket.OPEN, send: vi.fn() };
const client2 = { readyState: WebSocket.CLOSED, send: vi.fn() };
mockWssInstance.clients.add(client1 as any).add(client2 as any);
extendedWss.broadcast("hello");
expect(client1.send).toHaveBeenCalledWith("hello", { binary: false });
expect(client2.send).not.toHaveBeenCalled();
});
});
describe("sendMessageToUser", () => {
it("should send a message to a specific user by their UUID", () => {
const client1 = { readyState: WebSocket.OPEN, payload: { uuid: "uuid-1" }, send: vi.fn() };
const client2 = { readyState: WebSocket.OPEN, payload: { uuid: "uuid-2" }, send: vi.fn() };
mockWssInstance.clients.add(client1 as any).add(client2 as any);
extendedWss.sendMessageToUser("uuid-1", "private");
expect(client1.send).toHaveBeenCalledWith("private", { binary: false });
expect(client2.send).not.toHaveBeenCalled();
});
});
describe("Connection Handler Logic", () => {
let connectionHandler: (ws: any, req: any) => void;
let mockWsClient: any;
let mockClientEventHandler: Mocked<WebsocketEventHandler>;
beforeEach(() => {
connectionHandler = vi.mocked(mockServerEventHandler.enableConnectionEvent).mock.calls[0][0];
mockWsClient = {
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(),
} as unknown as Mocked<WebsocketEventHandler>;
vi.mocked(WebsocketEventHandler).mockImplementation(() => mockClientEventHandler);
vi.mocked(getEventListeners).mockReturnValue([{ event: "custom", handler: vi.fn() } as any]);
});
it("should create and configure a WebsocketEventHandler for new clients", () => {
connectionHandler(mockWsClient, {});
expect(vi.mocked(WebsocketEventHandler)).toHaveBeenCalledWith(mockWsClient);
expect(mockClientEventHandler.enableErrorEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enablePongEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enableMessageEvent).toHaveBeenCalled();
expect(mockClientEventHandler.enableDisconnectEvent).toHaveBeenCalled();
expect(mockClientEventHandler.registerCustomEvent).toHaveBeenCalled();
});
it("should emit initial events to the new client", () => {
connectionHandler(mockWsClient, {});
expect(mockWsClient.emit).toHaveBeenCalledWith("GET_STATE", {});
expect(mockWsClient.emit).toHaveBeenCalledWith("GET_SETTINGS", {});
});
it("should emit GET_SPOTIFY_UPDATES if last state was 'music'", () => {
mockWsClient.user.lastState.global.mode = "music";
connectionHandler(mockWsClient, {});
expect(mockWsClient.emit).toHaveBeenCalledWith("GET_SPOTIFY_UPDATES", {});
});
});
});
+4 -1
View File
@@ -5,14 +5,17 @@
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"lib": ["ES2022"],
"skipLibCheck": true,
"outDir": "./dist",
"types": ["node", "vitest/globals"],
"typeRoots": [
"./node_modules/@types",
"./types"
]
},
"include": [
"src/**/*.ts"
"src/**/*.ts",
"tests/**/*.ts"
]
}
+33
View File
@@ -0,0 +1,33 @@
// vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
/**
* globals: true
* Das ist der wichtigste Punkt, um dein ursprüngliches Problem zu lösen.
* Diese Option weist Vitest an, die globalen APIs (describe, it, expect, vi)
* automatisch in allen Testdateien verfügbar zu machen.
*/
globals: true,
/**
* environment: 'node'
* Dies simuliert eine Node.js-Umgebung für deine Tests.
* Es ist essenziell für Backend-Tests, da es Node.js-APIs wie `process`
* zur Verfügung stellt.
*/
environment: 'node',
/**
* setupFiles: ['./tests/setup.ts']
* (Optional, aber sehr nützlich)
* Hier kannst du eine Datei angeben, die vor ALLEN Tests einmalig ausgeführt wird.
* Perfekt, um z.B. eine Verbindung zu einer Test-Datenbank aufzubauen oder
* globale Mocks zu definieren.
* Du kannst diese Zeile erstmal auskommentieren, wenn du sie nicht brauchst.
*/
// setupFiles: ['./tests/setup.ts'],
},
});