add comprehensive tests for db services and utils

This commit is contained in:
StarAppeal
2025-09-08 06:18:14 +02:00
parent 061338c91d
commit 63d9c796f6
8 changed files with 1938 additions and 70 deletions
+8 -4
View File
@@ -1,11 +1,15 @@
import OpenWeatherAPI from "openweather-api-node"
const weather = new OpenWeatherAPI({
key: process.env.OWM_API_KEY,
});
function getWeatherInstance(): OpenWeatherAPI {
return new OpenWeatherAPI({
key: process.env.OWM_API_KEY,
});
}
export async function getCurrentWeather(location: string) {
return weather.getCurrent({
return getWeatherInstance().getCurrent({
locationName: location,
units: "metric"
});
+359
View File
@@ -0,0 +1,359 @@
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();
});
});
});
+242
View File
@@ -0,0 +1,242 @@
import {describe, it, expect, vi, beforeEach} from "vitest";
import OpenWeatherAPI from "openweather-api-node";
import {getCurrentWeather} from "../../../src/db/services/owmApiService";
vi.mock("openweather-api-node", () => {
return {
default: vi.fn().mockImplementation((ignored: any) => {
return {
getCurrent: vi.fn(),
};
}),
};
});
const MockedOpenWeatherAPI = vi.mocked(OpenWeatherAPI, true); // true = deep mock
vi.stubGlobal("process", {
env: {
OWM_API_KEY: "test-api-key",
},
});
describe("owmApiService", () => {
let mockWeatherInstance: any;
beforeEach(() => {
vi.clearAllMocks();
mockWeatherInstance = {
getCurrent: vi.fn(),
} as Partial<OpenWeatherAPI> as OpenWeatherAPI;
MockedOpenWeatherAPI.mockImplementation(() => mockWeatherInstance);
});
describe("getCurrentWeather", () => {
it("should initialize OpenWeatherAPI with correct API key", async () => {
const location = "Berlin";
const mockWeatherData = {
name: "Berlin",
main: {
temp: 20.5,
feels_like: 19.8,
humidity: 65,
pressure: 1013,
},
weather: [
{
main: "Clear",
description: "clear sky",
icon: "01d",
},
],
wind: {
speed: 3.2,
deg: 180,
},
};
mockWeatherInstance.getCurrent.mockResolvedValue(mockWeatherData);
const result = await getCurrentWeather(location);
expect(MockedOpenWeatherAPI).toHaveBeenCalledWith({
key: "test-api-key",
});
expect(result).toEqual(mockWeatherData);
});
it("should call getCurrent with correct parameters", async () => {
const location = "London";
const mockWeatherData = {
name: "London",
main: {temp: 15.2},
weather: [{main: "Clouds"}],
};
mockWeatherInstance.getCurrent.mockResolvedValue(mockWeatherData);
await getCurrentWeather(location);
expect(mockWeatherInstance.getCurrent).toHaveBeenCalledWith({
locationName: location,
units: "metric",
});
});
it("should return weather data for valid location", async () => {
const location = "Tokyo";
const mockWeatherData = {
name: "Tokyo",
main: {
temp: 25.3,
feels_like: 27.1,
humidity: 78,
pressure: 1008,
},
weather: [
{
main: "Rain",
description: "light rain",
icon: "10d",
},
],
wind: {
speed: 2.1,
deg: 90,
},
clouds: {
all: 75,
},
};
mockWeatherInstance.getCurrent.mockResolvedValue(mockWeatherData);
const result:any = await getCurrentWeather(location);
expect(result).toEqual(mockWeatherData);
expect(result.name).toBe("Tokyo");
expect(result.main.temp).toBe(25.3);
expect(result.weather[0].main).toBe("Rain");
});
it("should handle API errors", async () => {
const location = "InvalidLocation";
const apiError = new Error("City not found");
mockWeatherInstance.getCurrent.mockRejectedValue(apiError);
await expect(getCurrentWeather(location)).rejects.toThrow("City not found");
expect(mockWeatherInstance.getCurrent).toHaveBeenCalledWith({
locationName: location,
units: "metric",
});
});
it("should handle network errors", async () => {
const location = "Paris";
const networkError = new Error("Network timeout");
mockWeatherInstance.getCurrent.mockRejectedValue(networkError);
await expect(getCurrentWeather(location)).rejects.toThrow("Network timeout");
});
it("should work with different location formats", async () => {
const locations = [
"New York",
"New York, US",
"40.7128,-74.0060", // coordinates
"10001", // zip code
];
const mockWeatherData = {name: "Test Location", main: {temp: 20}};
mockWeatherInstance.getCurrent.mockResolvedValue(mockWeatherData);
for (const location of locations) {
await getCurrentWeather(location);
expect(mockWeatherInstance.getCurrent).toHaveBeenCalledWith({
locationName: location,
units: "metric",
});
}
expect(mockWeatherInstance.getCurrent).toHaveBeenCalledTimes(locations.length);
});
it("should always use metric units", async () => {
const location = "Sydney";
mockWeatherInstance.getCurrent.mockResolvedValue({});
await getCurrentWeather(location);
const callArgs = mockWeatherInstance.getCurrent.mock.calls[0][0];
expect(callArgs.units).toBe("metric");
});
it("should handle empty location string", async () => {
const location = "";
const apiError = new Error("Invalid location");
mockWeatherInstance.getCurrent.mockRejectedValue(apiError);
await expect(getCurrentWeather(location)).rejects.toThrow("Invalid location");
expect(mockWeatherInstance.getCurrent).toHaveBeenCalledWith({
locationName: "",
units: "metric",
});
});
it("should handle special characters in location", async () => {
const location = "São Paulo";
const mockWeatherData = {
name: "São Paulo",
main: {temp: 22.5},
};
mockWeatherInstance.getCurrent.mockResolvedValue(mockWeatherData);
const result = await getCurrentWeather(location);
expect(result).toEqual(mockWeatherData);
expect(mockWeatherInstance.getCurrent).toHaveBeenCalledWith({
locationName: "São Paulo",
units: "metric",
});
});
});
describe("OpenWeatherAPI initialization", () => {
it("should create instance with environment API key", () => {
getCurrentWeather("test");
expect(MockedOpenWeatherAPI).toHaveBeenCalledWith({
key: "test-api-key",
});
});
it("should handle missing API key", () => {
vi.stubGlobal("process", {
env: {
OWM_API_KEY: undefined,
},
});
getCurrentWeather("test");
expect(MockedOpenWeatherAPI).toHaveBeenCalledWith({
key: undefined,
});
vi.stubGlobal("process", {
env: {
OWM_API_KEY: "test-api-key",
},
});
});
});
});
+339
View File
@@ -0,0 +1,339 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import axios from "axios";
import { getCurrentlyPlaying, CurrentlyPlaying } from "../../../src/db/services/spotifyApiService";
vi.mock("axios", () => ({
default: {
get: vi.fn(),
},
}));
const mockedAxios = vi.mocked(axios, true);
describe("spotifyApiService", () => {
let consoleSpy: any;
let consoleErrorSpy: any;
beforeEach(() => {
vi.clearAllMocks();
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
});
describe("getCurrentlyPlaying", () => {
it("should return currently playing track data", async () => {
const accessToken = "test-access-token";
const mockCurrentlyPlaying: CurrentlyPlaying = {
timestamp: 1640995200000,
context: {
type: "playlist",
uri: "spotify:playlist:37i9dQZF1DXcBWIGoYBM5M",
},
progress_ms: 45000,
item: {
name: "Test Song",
artists: [
{
name: "Test Artist",
uri: "spotify:artist:test123",
},
],
album: {
name: "Test Album",
uri: "spotify:album:test456",
},
duration_ms: 180000,
},
is_playing: true,
};
mockedAxios.get.mockResolvedValue({
status: 200,
data: mockCurrentlyPlaying,
});
const result = await getCurrentlyPlaying(accessToken);
expect(mockedAxios.get).toHaveBeenCalledWith(
"https://api.spotify.com/v1/me/player/currently-playing",
{
headers: {
Authorization: `Bearer ${accessToken}`,
},
params: {
additional_types: "episode",
},
}
);
expect(result).toEqual(mockCurrentlyPlaying);
});
it("should return null when nothing is playing (204 status)", async () => {
const accessToken = "test-access-token";
mockedAxios.get.mockResolvedValue({
status: 204,
data: null,
});
const result = await getCurrentlyPlaying(accessToken);
expect(result).toBeNull();
expect(consoleSpy).toHaveBeenCalledWith("Es wird gerade nichts abgespielt.");
});
it("should handle track with minimal data", async () => {
const accessToken = "test-access-token";
const mockMinimalTrack: CurrentlyPlaying = {
timestamp: 1640995200000,
is_playing: false,
};
mockedAxios.get.mockResolvedValue({
status: 200,
data: mockMinimalTrack,
});
const result = await getCurrentlyPlaying(accessToken);
expect(result).toEqual(mockMinimalTrack);
expect(result?.item).toBeUndefined();
expect(result?.progress_ms).toBeUndefined();
});
it("should handle track with multiple artists", async () => {
const accessToken = "test-access-token";
const mockTrackWithMultipleArtists: CurrentlyPlaying = {
timestamp: 1640995200000,
item: {
name: "Collaboration Song",
artists: [
{
name: "Artist One",
uri: "spotify:artist:artist1",
},
{
name: "Artist Two",
uri: "spotify:artist:artist2",
},
{
name: "Artist Three",
uri: "spotify:artist:artist3",
},
],
album: {
name: "Collaboration Album",
uri: "spotify:album:collab123",
},
duration_ms: 240000,
},
is_playing: true,
};
mockedAxios.get.mockResolvedValue({
status: 200,
data: mockTrackWithMultipleArtists,
});
const result = await getCurrentlyPlaying(accessToken);
expect(result?.item?.artists).toHaveLength(3);
expect(result?.item?.artists[0].name).toBe("Artist One");
expect(result?.item?.artists[2].name).toBe("Artist Three");
});
it("should handle episodes (podcasts)", async () => {
const accessToken = "test-access-token";
const mockEpisode: CurrentlyPlaying = {
timestamp: 1640995200000,
context: {
type: "show",
uri: "spotify:show:test123",
},
progress_ms: 600000,
item: {
name: "Test Podcast Episode",
artists: [
{
name: "Podcast Host",
uri: "spotify:artist:host123",
},
],
album: {
name: "Test Podcast Show",
uri: "spotify:show:test123",
},
duration_ms: 3600000,
},
is_playing: true,
};
mockedAxios.get.mockResolvedValue({
status: 200,
data: mockEpisode,
});
const result = await getCurrentlyPlaying(accessToken);
expect(result).toEqual(mockEpisode);
expect(result?.context?.type).toBe("show");
});
it("should handle 401 unauthorized error", async () => {
const accessToken = "invalid-token";
const unauthorizedError = {
response: {
status: 401,
data: {
error: {
status: 401,
message: "Invalid access token",
},
},
},
};
mockedAxios.get.mockRejectedValue(unauthorizedError);
const result = await getCurrentlyPlaying(accessToken);
expect(result).toBeUndefined();
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Fehler bei der Anfrage:",
401,
unauthorizedError.response.data
);
});
it("should handle 403 forbidden error (premium required)", async () => {
const accessToken = "valid-but-non-premium-token";
const forbiddenError = {
response: {
status: 403,
data: {
error: {
status: 403,
message: "Player command failed: Premium required",
},
},
},
};
mockedAxios.get.mockRejectedValue(forbiddenError);
const result = await getCurrentlyPlaying(accessToken);
expect(result).toBeUndefined();
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Fehler bei der Anfrage:",
403,
forbiddenError.response.data
);
});
it("should handle 429 rate limit error", async () => {
const accessToken = "test-access-token";
const rateLimitError = {
response: {
status: 429,
data: {
error: {
status: 429,
message: "API rate limit exceeded",
},
},
headers: {
"retry-after": "30",
},
},
};
mockedAxios.get.mockRejectedValue(rateLimitError);
const result = await getCurrentlyPlaying(accessToken);
expect(result).toBeUndefined();
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Fehler bei der Anfrage:",
429,
rateLimitError.response.data
);
});
it("should handle network errors", async () => {
const accessToken = "test-access-token";
const networkError = {
code: "ECONNREFUSED",
message: "Network Error",
};
mockedAxios.get.mockRejectedValue(networkError);
const result = await getCurrentlyPlaying(accessToken);
expect(result).toBeUndefined();
expect(consoleErrorSpy).toHaveBeenCalledWith(
"Fehler bei der Anfrage:",
undefined,
undefined
);
});
it("should include additional_types parameter for episodes", async () => {
const accessToken = "test-access-token";
mockedAxios.get.mockResolvedValue({
status: 204,
data: null,
});
await getCurrentlyPlaying(accessToken);
const [, config] = mockedAxios.get.mock.calls[0];
expect(config?.params?.additional_types).toBe("episode");
});
it("should use correct Spotify API endpoint", async () => {
const accessToken = "test-access-token";
mockedAxios.get.mockResolvedValue({
status: 204,
data: null,
});
await getCurrentlyPlaying(accessToken);
const [url] = mockedAxios.get.mock.calls[0];
expect(url).toBe("https://api.spotify.com/v1/me/player/currently-playing");
});
it("should format authorization header correctly", async () => {
const accessToken = "test-access-token-123";
mockedAxios.get.mockResolvedValue({
status: 204,
data: null,
});
await getCurrentlyPlaying(accessToken);
const [, config] = mockedAxios.get.mock.calls[0];
expect(config?.headers?.Authorization).toBe(`Bearer ${accessToken}`);
});
it("should handle empty access token", async () => {
const accessToken = "";
mockedAxios.get.mockResolvedValue({
status: 204,
data: null,
});
await getCurrentlyPlaying(accessToken);
const [, config] = mockedAxios.get.mock.calls[0];
expect(config?.headers?.Authorization).toBe("Bearer ");
});
});
});
@@ -0,0 +1,318 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import axios, { AxiosResponse } from "axios";
import { OAuthTokenResponse } from "../../../src/interfaces/OAuthTokenResponse";
vi.mock("axios", () => ({
default: {
post: vi.fn(),
},
}));
// 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",
};
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;
}
};
})
};
});
import { SpotifyTokenService } from "../../../src/db/services/spotifyTokenService";
const mockedAxios = vi.mocked(axios);
const mockedAxiosPost = mockedAxios.post as ReturnType<typeof vi.fn>;
// 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");
});
it("should handle axios errors during token refresh", async () => {
const refreshToken = "test-refresh-token";
const error = new Error("Network error");
mockedAxiosPost.mockRejectedValue(error);
await expect(spotifyTokenService.refreshToken(refreshToken)).rejects.toThrow("Network error");
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")}`,
},
}
);
});
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",
},
},
};
mockedAxiosPost.mockRejectedValue(spotifyError);
await expect(spotifyTokenService.refreshToken(refreshToken)).rejects.toEqual(spotifyError);
});
});
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,203 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
import { authenticateJwt } from "../../../src/rest/middleware/authenticateJwt";
import { JwtAuthenticator } from "../../../src/utils/jwtAuthenticator";
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",
},
});
describe("authenticateJwt middleware", () => {
let mockJwtAuthenticatorInstance: any;
let req: any;
let res: any;
let next: any;
let consoleSpy: any;
beforeEach(() => {
vi.clearAllMocks();
mockJwtAuthenticatorInstance = {
verifyToken: vi.fn(),
};
MockedJwtAuthenticator.mockReturnValue(mockJwtAuthenticatorInstance);
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
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();
});
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();
});
it("should return 401 when authorization header is empty", () => {
req.headers.authorization = "";
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("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();
});
it("should return 401 when authorization header has no token", () => {
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();
});
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();
});
});
});
+336
View File
@@ -0,0 +1,336 @@
import { describe, it, expect, vi } from "vitest";
import type { Response } from "express";
import {
ok,
created,
badRequest,
unauthorized,
forbidden,
notFound,
conflict,
tooManyRequests,
internalError,
} from "../../../src/rest/utils/responses";
function createMockResponse(): Response {
const res = {} as Response;
res.status = vi.fn().mockReturnValue(res);
res.send = vi.fn().mockReturnValue(res);
return res;
}
describe("Response Utilities", () => {
describe("ok", () => {
it("should return 200 status with success response", () => {
const res = createMockResponse();
const data = { message: "Success" };
ok(res, data);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
ok: true,
data,
});
});
it("should handle null data", () => {
const res = createMockResponse();
const data = null;
ok(res, data);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
ok: true,
data: null,
});
});
it("should handle complex data objects", () => {
const res = createMockResponse();
const data = {
users: [{ id: 1, name: "John" }, { id: 2, name: "Jane" }],
pagination: { page: 1, total: 2 },
};
ok(res, data);
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith({
ok: true,
data,
});
});
});
describe("created", () => {
it("should return 201 status with success response", () => {
const res = createMockResponse();
const data = { id: 1, name: "New User" };
created(res, data);
expect(res.status).toHaveBeenCalledWith(201);
expect(res.send).toHaveBeenCalledWith({
ok: true,
data,
});
});
});
describe("badRequest", () => {
it("should return 400 status with default message", () => {
const res = createMockResponse();
badRequest(res);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Bad Request",
details: undefined,
},
});
});
it("should return 400 status with custom message and details", () => {
const res = createMockResponse();
const message = "Invalid input";
const details = ["Field 'name' is required", "Field 'email' must be valid"];
badRequest(res, message, details);
expect(res.status).toHaveBeenCalledWith(400);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details,
},
});
});
});
describe("unauthorized", () => {
it("should return 401 status with default message", () => {
const res = createMockResponse();
unauthorized(res);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Unauthorized",
details: undefined,
},
});
});
it("should return 401 status with custom message and details", () => {
const res = createMockResponse();
const message = "Invalid token";
const details = { tokenExpired: true };
unauthorized(res, message, details);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details,
},
});
});
});
describe("forbidden", () => {
it("should return 403 status with default message", () => {
const res = createMockResponse();
forbidden(res);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Forbidden",
details: undefined,
},
});
});
it("should return 403 status with custom message", () => {
const res = createMockResponse();
const message = "Insufficient permissions";
forbidden(res, message);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details: undefined,
},
});
});
});
describe("notFound", () => {
it("should return 404 status with default message", () => {
const res = createMockResponse();
notFound(res);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Not Found",
details: undefined,
},
});
});
it("should return 404 status with custom message and details", () => {
const res = createMockResponse();
const message = "User not found";
const details = { userId: "123" };
notFound(res, message, details);
expect(res.status).toHaveBeenCalledWith(404);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details,
},
});
});
});
describe("conflict", () => {
it("should return 409 status with default message", () => {
const res = createMockResponse();
conflict(res);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Conflict",
details: undefined,
},
});
});
it("should return 409 status with custom message", () => {
const res = createMockResponse();
const message = "User already exists";
conflict(res, message);
expect(res.status).toHaveBeenCalledWith(409);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details: undefined,
},
});
});
});
describe("tooManyRequests", () => {
it("should return 429 status with default message", () => {
const res = createMockResponse();
tooManyRequests(res);
expect(res.status).toHaveBeenCalledWith(429);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Too Many Requests",
details: undefined,
},
});
});
it("should return 429 status with custom message and details", () => {
const res = createMockResponse();
const message = "Rate limit exceeded";
const details = { retryAfter: 60 };
tooManyRequests(res, message, details);
expect(res.status).toHaveBeenCalledWith(429);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details,
},
});
});
});
describe("internalError", () => {
it("should return 500 status with default message", () => {
const res = createMockResponse();
internalError(res);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message: "Internal Server Error",
details: undefined,
},
});
});
it("should return 500 status with custom message and details", () => {
const res = createMockResponse();
const message = "Database connection failed";
const details = { error: "Connection timeout" };
internalError(res, message, details);
expect(res.status).toHaveBeenCalledWith(500);
expect(res.send).toHaveBeenCalledWith({
ok: false,
data: {
message,
details,
},
});
});
});
describe("Response chaining", () => {
it("should return the response object for method chaining", () => {
const res = createMockResponse();
const data = { test: "data" };
const result = ok(res, data);
expect(result).toBe(res);
});
it("should work with error responses for chaining", () => {
const res = createMockResponse();
const result = badRequest(res, "Test error");
expect(result).toBe(res);
});
});
});
+133 -66
View File
@@ -1,83 +1,150 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { describe, it, expect, vi } from "vitest";
import bcrypt from "bcrypt";
import { PasswordUtils, ValidationResult } from "../../src/utils/passwordUtils";
const { hashMock, compareMock } = vi.hoisted(() => ({
hashMock: vi.fn(),
compareMock: vi.fn(),
vi.mock("bcrypt", () => ({
default: {
hash: vi.fn(),
compare: vi.fn(),
},
}));
vi.mock("bcrypt", () => {
return {
hash: hashMock,
compare: compareMock,
default: {
hash: hashMock,
compare: compareMock,
},
};
});
import { PasswordUtils } from "../../src/utils/passwordUtils";
const mockedBcrypt = vi.mocked(bcrypt);
describe("PasswordUtils", () => {
beforeEach(() => {
hashMock.mockReset();
compareMock.mockReset();
describe("hashPassword", () => {
it("should hash password with salt rounds of 10", async () => {
const password = "testPassword123!";
const hashedPassword = "hashedPassword123";
mockedBcrypt.hash.mockResolvedValue(hashedPassword as any);
const result = await PasswordUtils.hashPassword(password);
expect(mockedBcrypt.hash).toHaveBeenCalledWith(password, 10);
expect(result).toBe(hashedPassword);
});
afterEach(() => {
vi.restoreAllMocks();
it("should handle bcrypt errors", async () => {
const password = "testPassword123!";
const error = new Error("Bcrypt error");
mockedBcrypt.hash.mockRejectedValue(error);
await expect(PasswordUtils.hashPassword(password)).rejects.toThrow("Bcrypt error");
});
});
describe("comparePassword", () => {
it("should return true for matching passwords", async () => {
const password = "testPassword123!";
const hashedPassword = "hashedPassword123";
mockedBcrypt.compare.mockResolvedValue(true as any);
const result = await PasswordUtils.comparePassword(password, hashedPassword);
expect(mockedBcrypt.compare).toHaveBeenCalledWith(password, hashedPassword);
expect(result).toBe(true);
});
it("hashPassword uses bcrypt.hash with 10 saltrounds", async () => {
hashMock.mockResolvedValue("hashed");
const res = await PasswordUtils.hashPassword("secret");
expect(hashMock).toHaveBeenCalledWith("secret", 10);
expect(res).toBe("hashed");
it("should return false for non-matching passwords", async () => {
const password = "testPassword123!";
const hashedPassword = "hashedPassword123";
mockedBcrypt.compare.mockResolvedValue(false as any);
const result = await PasswordUtils.comparePassword(password, hashedPassword);
expect(mockedBcrypt.compare).toHaveBeenCalledWith(password, hashedPassword);
expect(result).toBe(false);
});
it("comparePassword uses bcrypt.compare", async () => {
compareMock.mockResolvedValue(true);
const ok = await PasswordUtils.comparePassword("secret", "hashed");
expect(compareMock).toHaveBeenCalledWith("secret", "hashed");
expect(ok).toBe(true);
it("should handle bcrypt comparison errors", async () => {
const password = "testPassword123!";
const hashedPassword = "hashedPassword123";
const error = new Error("Bcrypt comparison error");
mockedBcrypt.compare.mockRejectedValue(error);
await expect(PasswordUtils.comparePassword(password, hashedPassword)).rejects.toThrow("Bcrypt comparison error");
});
});
describe("validatePassword", () => {
it("should return valid for a strong password", () => {
const password = "StrongPass123!";
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(true);
expect(result.message).toBe("Passwort ist gültig.");
});
describe("validatePassword", () => {
it("fails when password too short", () => {
const res = PasswordUtils.validatePassword("A1!");
expect(res.valid).toBe(false);
expect(res.message).toMatch(/mindestens 8 Zeichen/);
});
it("should reject password shorter than 8 characters", () => {
const password = "Short1!";
it("fails without capital letter", () => {
const res = PasswordUtils.validatePassword("password1!");
expect(res.valid).toBe(false);
expect(res.message).toMatch(/Großbuchstaben/);
});
const result: ValidationResult = PasswordUtils.validatePassword(password);
it("fails without uncapitalized letter", () => {
const res = PasswordUtils.validatePassword("PASSWORD1!");
expect(res.valid).toBe(false);
expect(res.message).toMatch(/Kleinbuchstaben/);
});
it("fails without number", () => {
const res = PasswordUtils.validatePassword("Password!");
expect(res.valid).toBe(false);
expect(res.message).toMatch(/Zahl/);
});
it("fails without special characters", () => {
const res = PasswordUtils.validatePassword("Password1");
expect(res.valid).toBe(false);
expect(res.message).toMatch(/Sonderzeichen/);
});
it("accepts valid password", () => {
const res = PasswordUtils.validatePassword("ValidPassword1!");
expect(res.valid).toBe(true);
});
expect(result.valid).toBe(false);
expect(result.message).toBe("Passwort muss mindestens 8 Zeichen lang sein.");
});
it("should reject password without uppercase letter", () => {
const password = "lowercase123!";
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(false);
expect(result.message).toBe("Passwort muss mindestens einen Großbuchstaben enthalten.");
});
it("should reject password without lowercase letter", () => {
const password = "UPPERCASE123!";
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(false);
expect(result.message).toBe("Passwort muss mindestens einen Kleinbuchstaben enthalten.");
});
it("should reject password without number", () => {
const password = "NoNumbers!";
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(false);
expect(result.message).toBe("Passwort muss mindestens eine Zahl enthalten.");
});
it("should reject password without special character", () => {
const password = "NoSpecialChar123";
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(false);
expect(result.message).toBe("Passwort muss mindestens ein Sonderzeichen enthalten.");
});
it("should accept all valid special characters", () => {
const specialChars = "!@#$%^&*(),.?\":{}|<>";
for (const char of specialChars) {
const password = `ValidPass123${char}`;
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(true);
expect(result.message).toBe("Passwort ist gültig.");
}
});
it("should handle edge case with exactly 8 characters", () => {
const password = "Valid12!";
const result: ValidationResult = PasswordUtils.validatePassword(password);
expect(result.valid).toBe(true);
expect(result.message).toBe("Passwort ist gültig.");
});
});
});