add comprehensive tests for db services and utils
This commit is contained in:
@@ -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"
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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.");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user