add and change multiple vitests
This commit is contained in:
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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", {});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user