refactoring, added s3Service.ts, add more depenedncy injection, fix tests as always i geuss

This commit is contained in:
StarAppeal
2025-09-24 23:55:05 +02:00
parent 9a59e9681b
commit 06cffcd7af
18 changed files with 2593 additions and 196 deletions
+1 -1
View File
@@ -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;
};
+2 -1
View File
@@ -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
View File
@@ -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);
+57 -83
View File
@@ -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();
+20 -25
View File
@@ -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);
});
});