refactoring, added s3Service.ts, add more depenedncy injection, fix tests as always i geuss
This commit is contained in:
@@ -4,7 +4,7 @@ const loadConfigWithEnv = async (envVars: Record<string, string | undefined>) =>
|
||||
for (const key in envVars) {
|
||||
vi.stubEnv(key, envVars[key] as string);
|
||||
}
|
||||
const { config } = await import("../src/config");
|
||||
const { config } = await import("../src/config/config");
|
||||
return config;
|
||||
};
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ describe("RestAuth", () => {
|
||||
let mockJwtAuthenticator: any;
|
||||
let mockCrypto: any;
|
||||
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
@@ -46,7 +47,7 @@ describe("RestAuth", () => {
|
||||
mockJwtAuthenticator = createMockJwtAuthenticator();
|
||||
vi.mocked(JwtAuthenticator).mockImplementation(() => mockJwtAuthenticator);
|
||||
|
||||
const restAuth = new RestAuth(mockUserService);
|
||||
const restAuth = new RestAuth(mockUserService, mockJwtAuthenticator);
|
||||
app = createPublicTestApp(restAuth.createRouter(), "/auth");
|
||||
|
||||
process.env.SECRET_KEY = "test-secret-key";
|
||||
|
||||
+37
-13
@@ -4,15 +4,33 @@ import { Server } from "../src/server";
|
||||
import { Router, type Request, type Response, type NextFunction } from "express"; // Import Express types
|
||||
import type { Express } from "express";
|
||||
import { authLimiter } from "../src/rest/middleware/rateLimit";
|
||||
import {
|
||||
createMockJwtAuthenticator,
|
||||
createMockSpotifyPollingService,
|
||||
createMockSpotifyTokenService,
|
||||
createMockUserService
|
||||
} from "./helpers/testSetup";
|
||||
|
||||
|
||||
const mockS3Service = {
|
||||
ensureBucketExists: vi.fn().mockResolvedValue(undefined),
|
||||
uploadFile: vi.fn(),
|
||||
getSignedDownloadUrl: vi.fn(),
|
||||
} as any;
|
||||
|
||||
const mockUserService = createMockUserService() as any;
|
||||
const mockSpotifyTokenService = createMockSpotifyTokenService() as any;
|
||||
const mockSpotifyPollingService = createMockSpotifyPollingService() as any;
|
||||
const mockWeatherPollingService = {
|
||||
subscribeUser: vi.fn(),
|
||||
unsubscribeUser: vi.fn()
|
||||
} as any;
|
||||
const mockJwtAuthenticator = createMockJwtAuthenticator() as any;
|
||||
|
||||
vi.mock("../src/services/db/database.service", () => ({
|
||||
connectToDatabase: vi.fn().mockResolvedValue(undefined),
|
||||
disconnectFromDatabase: vi.fn().mockResolvedValue(undefined),
|
||||
}));
|
||||
vi.mock("../src/services/db/UserService", () => ({
|
||||
UserService: { create: vi.fn().mockResolvedValue({}) },
|
||||
}));
|
||||
vi.mock("../src/services/spotifyTokenService", () => ({ SpotifyTokenService: vi.fn() }));
|
||||
|
||||
vi.mock("../src/websocket", () => ({
|
||||
ExtendedWebSocketServer: vi.fn().mockImplementation(() => {
|
||||
@@ -29,12 +47,6 @@ vi.mock("../src/rest/middleware/rateLimit", async (importOriginal) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../src/rest/middleware/authenticateJwt", () => ({
|
||||
authenticateJwt: vi.fn(() => (req: Request, res: Response, next: NextFunction) => {
|
||||
res.status(401).json({ error: "Unauthorized" });
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../src/rest/auth", () => {
|
||||
const MockRestAuth = vi.fn().mockImplementation(() => {
|
||||
return {
|
||||
@@ -59,8 +71,6 @@ vi.mock("../src/rest/auth", () => {
|
||||
const mockServerConfig = {
|
||||
port: 8888,
|
||||
jwtSecret: "a-very-secure-test-secret-that-is-at-least-32-chars-long",
|
||||
spotifyClientId: "test-id",
|
||||
spotifyClientSecret: "test-secret",
|
||||
cors: {
|
||||
origin: "http://test-origin.com",
|
||||
credentials: true,
|
||||
@@ -73,7 +83,14 @@ describe("Server Class Integration Tests", () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
server = new Server(mockServerConfig);
|
||||
server = new Server(mockServerConfig, {
|
||||
s3Service: mockS3Service,
|
||||
userService: mockUserService,
|
||||
spotifyTokenService: mockSpotifyTokenService,
|
||||
spotifyPollingService: mockSpotifyPollingService,
|
||||
weatherPollingService: mockWeatherPollingService,
|
||||
jwtAuthenticator: mockJwtAuthenticator,
|
||||
});
|
||||
await server.start();
|
||||
app = server.app;
|
||||
});
|
||||
@@ -82,6 +99,13 @@ describe("Server Class Integration Tests", () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
describe('Server Startup', () => {
|
||||
it('should call ensureBucketExists on S3Service during startup', () => {
|
||||
expect(mockS3Service.ensureBucketExists).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe("Server Setup and Middleware", () => {
|
||||
it("should start and respond to the healthz endpoint", async () => {
|
||||
const response = await request(app).get("/api/healthz").expect(200);
|
||||
|
||||
@@ -1,110 +1,84 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import mongoose from "mongoose";
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import mongoose from 'mongoose';
|
||||
|
||||
const MODULE_PATH = "../../../src/services/db/database.service";
|
||||
|
||||
type SpyInstance<T extends (...args: any) => any> = ReturnType<typeof vi.spyOn<any, Parameters<T>[0]>>;
|
||||
|
||||
vi.mock("mongoose", async (importOriginal) => {
|
||||
vi.mock('mongoose', async (importOriginal) => {
|
||||
const originalMongoose = await importOriginal<typeof mongoose>();
|
||||
const mockConnection = {
|
||||
on: vi.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
...originalMongoose,
|
||||
default: {
|
||||
...originalMongoose.default,
|
||||
connect: vi.fn(),
|
||||
disconnect: vi.fn(),
|
||||
connection: mockConnection,
|
||||
connection: {
|
||||
on: vi.fn(),
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("dotenv/config", () => ({}));
|
||||
|
||||
const mockedMongooseConnect = vi.mocked(mongoose.connect);
|
||||
const mockedMongooseDisconnect = vi.mocked(mongoose.disconnect);
|
||||
const mockedConnectionOn = vi.mocked(mongoose.connection.on);
|
||||
|
||||
describe("database.service", () => {
|
||||
let consoleLogSpy: SpyInstance<typeof console.log>;
|
||||
let consoleErrorSpy: SpyInstance<typeof console.error>;
|
||||
|
||||
beforeEach(() => {
|
||||
describe('database.service', () => {
|
||||
|
||||
let connectToDatabase: any;
|
||||
let disconnectFromDatabase: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.resetModules();
|
||||
vi.clearAllMocks();
|
||||
vi.unstubAllEnvs();
|
||||
|
||||
vi.stubEnv('DB_CONN_STRING', 'mongodb://test-host/testdb');
|
||||
vi.stubEnv('DB_NAME', 'testdb');
|
||||
|
||||
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
const dbService = await import('../../../src/services/db/database.service');
|
||||
connectToDatabase = dbService.connectToDatabase;
|
||||
disconnectFromDatabase = dbService.disconnectFromDatabase;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("connectToDatabase", () => {
|
||||
it("should throw error,when DB_CONN_STRING is not set", async () => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.stubEnv('DB_NAME', 'testdb');
|
||||
const { connectToDatabase } = await import(MODULE_PATH);
|
||||
const TEST_DB_NAME = 'testdb';
|
||||
const TEST_DB_CONN_STRING = 'mongodb://test-host/testdb';
|
||||
|
||||
await expect(connectToDatabase()).rejects.toThrow(
|
||||
"Missing environment variable: DB_CONN_STRING is required for database connection."
|
||||
);
|
||||
describe('connectToDatabase', () => {
|
||||
it('should attempt to connect to MongoDB with correct options', async () => {
|
||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||
|
||||
await connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledOnce();
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledWith(TEST_DB_CONN_STRING, expect.objectContaining({
|
||||
dbName: TEST_DB_NAME,
|
||||
family: 4,
|
||||
}));
|
||||
});
|
||||
|
||||
it("should throw error, when DB_NAME is not set", async () => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.stubEnv('DB_CONN_STRING', 'mongodb://test-host/testdb');
|
||||
const { connectToDatabase } = await import(MODULE_PATH);
|
||||
it('should correctly set up event listeners on the connection', async () => {
|
||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||
|
||||
await expect(connectToDatabase()).rejects.toThrow(
|
||||
"Missing environment variable: DB_NAME is required for database connection."
|
||||
);
|
||||
});
|
||||
|
||||
it("should connect successfully first try", async () => {
|
||||
mockedMongooseConnect.mockResolvedValueOnce(undefined as any);
|
||||
const { connectToDatabase } = await import(MODULE_PATH);
|
||||
|
||||
await connectToDatabase();
|
||||
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledWith('mongodb://test-host/testdb', expect.any(Object));
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Attempting to connect to MongoDB...");
|
||||
});
|
||||
|
||||
it("should configure event-listeners", async () => {
|
||||
mockedMongooseConnect.mockResolvedValueOnce(undefined as any);
|
||||
const { connectToDatabase } = await import(MODULE_PATH);
|
||||
|
||||
await connectToDatabase();
|
||||
await connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||
|
||||
expect(mockedConnectionOn).toHaveBeenCalledWith('connected', expect.any(Function));
|
||||
expect(mockedConnectionOn).toHaveBeenCalledWith('disconnected', expect.any(Function));
|
||||
expect(mockedConnectionOn).toHaveBeenCalledWith('error', expect.any(Function));
|
||||
expect(mockedConnectionOn).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
describe("Singleton", () => {
|
||||
it("should try to connect once, even if called multiple times", async () => {
|
||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||
const { connectToDatabase } = await import(MODULE_PATH);
|
||||
it('should only attempt to connect once when called multiple times (singleton pattern)', async () => {
|
||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||
|
||||
const promise1 = connectToDatabase();
|
||||
const promise2 = connectToDatabase();
|
||||
// Rufe die Funktion mehrmals parallel auf
|
||||
const promise1 = connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||
const promise2 = connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||
|
||||
await Promise.all([promise1, promise2]);
|
||||
await Promise.all([promise1, promise2]);
|
||||
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
describe("Retry Logic", () => {
|
||||
describe('Retry Logic', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
@@ -112,18 +86,19 @@ describe("database.service", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("should retry after 5 seconds when first time fails", async () => {
|
||||
const connectionError = new Error("DB not ready");
|
||||
it('should retry connecting after a 5-second delay if the first attempt fails', async () => {
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const connectionError = new Error('Database unavailable');
|
||||
|
||||
mockedMongooseConnect
|
||||
.mockRejectedValueOnce(connectionError)
|
||||
.mockResolvedValueOnce(undefined as any);
|
||||
|
||||
const { connectToDatabase } = await import(MODULE_PATH);
|
||||
const connectionPromise = connectToDatabase();
|
||||
const connectionPromise = connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(1);
|
||||
await vi.runAllTicks();
|
||||
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledOnce();
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to connect to MongoDB. Retrying in 5 seconds...", connectionError);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(5000);
|
||||
@@ -131,32 +106,31 @@ describe("database.service", () => {
|
||||
expect(mockedMongooseConnect).toHaveBeenCalledTimes(2);
|
||||
|
||||
await expect(connectionPromise).resolves.toBeUndefined();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("disconnectFromDatabase", () => {
|
||||
it("should call mongoose.disconnect, when connection is established", async () => {
|
||||
describe('disconnectFromDatabase', () => {
|
||||
it('should call mongoose.disconnect if the connection is established', async () => {
|
||||
mockedMongooseConnect.mockResolvedValue(undefined as any);
|
||||
mockedMongooseDisconnect.mockResolvedValue(undefined as any);
|
||||
const { connectToDatabase, disconnectFromDatabase } = await import(MODULE_PATH);
|
||||
|
||||
await connectToDatabase();
|
||||
await connectToDatabase(TEST_DB_NAME, TEST_DB_CONN_STRING);
|
||||
|
||||
const connectedCallback = mockedConnectionOn.mock.calls.find(call => call[0] === 'connected')?.[1];
|
||||
if (connectedCallback) {
|
||||
if (typeof connectedCallback === 'function') {
|
||||
connectedCallback();
|
||||
} else {
|
||||
throw new Error("Connected callback was not found or is not a function");
|
||||
}
|
||||
|
||||
await disconnectFromDatabase();
|
||||
|
||||
expect(mockedMongooseDisconnect).toHaveBeenCalledTimes(1);
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith("Disconnected from MongoDB.");
|
||||
expect(mockedMongooseDisconnect).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it("should NOT call.disconnect NICHT, when no connection is established", async () => {
|
||||
const { disconnectFromDatabase } = await import(MODULE_PATH);
|
||||
|
||||
it('should not call mongoose.disconnect if the connection was never established', async () => {
|
||||
await disconnectFromDatabase();
|
||||
|
||||
expect(mockedMongooseDisconnect).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,34 +1,29 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
vi.mock("../../src/utils/jwtAuthenticator", () => {
|
||||
return {
|
||||
JwtAuthenticator: vi.fn().mockImplementation(() => ({
|
||||
verifyToken: mockVerifyToken,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
const mockVerifyToken = vi.fn();
|
||||
import {describe, it, expect, vi, beforeEach, afterEach, Mocked} from "vitest";
|
||||
|
||||
import type { IncomingMessage } from "node:http";
|
||||
import { verifyClient } from "../../src/utils/verifyClient";
|
||||
import {JwtAuthenticator} from "../../src/utils/jwtAuthenticator";
|
||||
// @ts-ignore
|
||||
import {createMockJwtAuthenticator} from "../helpers/testSetup";
|
||||
|
||||
describe("verifyClient", () => {
|
||||
const payload = { id: "user-1", username: "hi", uuid: "1234" }
|
||||
const cb = vi.fn();
|
||||
|
||||
let mockJwtAuthenticator: Mocked<JwtAuthenticator>
|
||||
let consoleSpy: ReturnType<typeof vi.spyOn>;
|
||||
|
||||
function makeReq(authHeader?: string) {
|
||||
const headers: Record<string, string> = {};
|
||||
if (authHeader) headers["authorization"] = authHeader;
|
||||
|
||||
// socket infos just for log
|
||||
const socket: any = { remoteAddress: "127.0.0.1", remotePort: 12345 };
|
||||
return { headers, socket } as unknown as IncomingMessage & { [k: string]: any };
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cb.mockReset();
|
||||
mockVerifyToken.mockReset();
|
||||
mockJwtAuthenticator = createMockJwtAuthenticator() as any;
|
||||
consoleSpy = vi.spyOn(console, "log").mockImplementation(() => {});
|
||||
});
|
||||
|
||||
@@ -38,20 +33,20 @@ describe("verifyClient", () => {
|
||||
|
||||
it("accepts connections with valid token and sets payload", () => {
|
||||
const req = makeReq("Bearer valid.jwt");
|
||||
mockVerifyToken.mockReturnValue({ sub: "user-1" });
|
||||
mockJwtAuthenticator.verifyToken.mockReturnValue(payload);
|
||||
|
||||
verifyClient(req, cb);
|
||||
verifyClient(req, mockJwtAuthenticator ,cb);
|
||||
|
||||
expect(mockVerifyToken).toHaveBeenCalledWith("valid.jwt");
|
||||
expect(mockJwtAuthenticator.verifyToken).toHaveBeenCalledWith("valid.jwt");
|
||||
expect(cb).toHaveBeenCalledWith(true);
|
||||
expect((req as any).payload).toEqual({ sub: "user-1" });
|
||||
expect((req as any).payload).toEqual(payload);
|
||||
});
|
||||
|
||||
it("Rejects connection if no Authorization header is set", () => {
|
||||
const req = makeReq(undefined);
|
||||
mockVerifyToken.mockReturnValue(null);
|
||||
mockJwtAuthenticator.verifyToken.mockReturnValue(null);
|
||||
|
||||
verifyClient(req, cb);
|
||||
verifyClient(req, mockJwtAuthenticator, cb);
|
||||
|
||||
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
||||
expect(consoleSpy).toHaveBeenCalled();
|
||||
@@ -59,22 +54,22 @@ describe("verifyClient", () => {
|
||||
|
||||
it("rejects connection, if token is invalid", () => {
|
||||
const req = makeReq("Bearer bad.jwt");
|
||||
mockVerifyToken.mockReturnValue(null);
|
||||
mockJwtAuthenticator.verifyToken.mockReturnValue(null);
|
||||
|
||||
verifyClient(req, cb);
|
||||
verifyClient(req, mockJwtAuthenticator, cb);
|
||||
|
||||
expect(mockVerifyToken).toHaveBeenCalledWith("bad.jwt");
|
||||
expect(mockJwtAuthenticator.verifyToken).toHaveBeenCalledWith("bad.jwt");
|
||||
expect(cb).toHaveBeenCalledWith(false, 401, "Unauthorized");
|
||||
});
|
||||
|
||||
it("extracts token correctly after 'Bearer ' prefix", () => {
|
||||
const expectedToken = " fancy.token.with.spaces ";
|
||||
const req = makeReq(`Bearer ${expectedToken}`);
|
||||
mockVerifyToken.mockReturnValue({ ok: true });
|
||||
mockJwtAuthenticator.verifyToken.mockReturnValue(payload);
|
||||
|
||||
verifyClient(req, cb);
|
||||
verifyClient(req,mockJwtAuthenticator, cb);
|
||||
|
||||
expect(mockVerifyToken).toHaveBeenCalledWith(expectedToken);
|
||||
expect(mockJwtAuthenticator.verifyToken).toHaveBeenCalledWith(expectedToken);
|
||||
expect(cb).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user