bigger refactoring of database.service.ts

This commit is contained in:
StarAppeal
2025-09-19 22:49:56 +02:00
parent 1d97b00811
commit 92bd66c6f3
3 changed files with 200 additions and 95 deletions
+60 -31
View File
@@ -1,16 +1,12 @@
import "dotenv/config";
import mongoose, { ConnectOptions } from "mongoose";
import mongoose from "mongoose";
let isConnected: boolean = false;
let connectionPromise: Promise<void> | null = null;
let isConnected = false;
export async function connectToDatabase() {
if (isConnected) {
console.log("Already connected to MongoDB.");
return;
}
const dbConnString = process.env.DB_CONN_STRING;
const dbName = process.env.DB_NAME;
const connectWithRetry = async (): Promise<void> => {
const dbConnString: string | undefined = process.env.DB_CONN_STRING;
const dbName: string | undefined = process.env.DB_NAME;
if (!dbConnString) {
throw new Error("Missing environment variable: DB_CONN_STRING is required for database connection.");
@@ -18,29 +14,62 @@ export async function connectToDatabase() {
if (!dbName) {
throw new Error("Missing environment variable: DB_NAME is required for database connection.");
}
const options: ConnectOptions = {
dbName: dbName,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
family: 4,
keepAliveInitialDelay: 300000,
};
try {
await mongoose.connect(dbConnString, {
dbName: dbName,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
family: 4
});
isConnected = true;
console.log("Connected to MongoDB with Mongoose");
mongoose.connection.on('disconnected', () => {
// TODO: add reconnecting
console.warn('Mongoose disconnected from DB. Attempting to reconnect...');
isConnected = false;
});
mongoose.connection.on('error', (err) => {
console.error('Mongoose connection error:', err);
isConnected = false;
});
console.log("Attempting to connect to MongoDB...");
await mongoose.connect(dbConnString, options);
} catch (error) {
console.error("Failed to connect to MongoDB:", error);
process.exit(1);
console.error("Failed to connect to MongoDB. Retrying in 5 seconds...", error);
await new Promise<void>(resolve => setTimeout(resolve, 5000));
return connectWithRetry();
}
};
export async function connectToDatabase(): Promise<void> {
if (connectionPromise) {
return connectionPromise;
}
connectionPromise = (async (): Promise<void> => {
if (isConnected) {
console.log("Already connected to MongoDB.");
return;
}
mongoose.connection.on('connected', () => {
isConnected = true;
console.log('Mongoose connected to DB.');
});
mongoose.connection.on('disconnected', () => {
isConnected = false;
console.warn('Mongoose disconnected from DB. Attempting to reconnect...');
});
mongoose.connection.on('error', (err: Error) => {
isConnected = false;
console.error('Mongoose connection error:', err);
});
await connectWithRetry();
})();
return connectionPromise;
}
export async function disconnectFromDatabase(): Promise<void> {
if (isConnected) {
await mongoose.disconnect();
isConnected = false;
connectionPromise = null;
console.log("Disconnected from MongoDB.");
}
}
+5 -1
View File
@@ -14,6 +14,7 @@ import {UserService} from "./db/services/db/UserService";
import {randomUUID} from "crypto";
import {JwtAuthenticator} from "./utils/jwtAuthenticator";
import {authenticateJwt} from "./rest/middleware/authenticateJwt";
import {disconnectFromDatabase} from "./db/services/db/database.service";
export async function startServer(jwtSecret: string) {
const app = express();
@@ -101,14 +102,17 @@ export async function startServer(jwtSecret: string) {
res.status(statusCode).send(errorResponse);
});
process.on("SIGTERM", () => {
process.on("SIGTERM", async () => {
console.log("SIGTERM signal received: closing HTTP server");
await disconnectFromDatabase();
server.close(() => {
console.log("HTTP server closed");
process.exit(0);
});
});
return {app, server};
}
+135 -63
View File
@@ -1,93 +1,165 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mocked } from "vitest";
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import mongoose from "mongoose";
import { connectToDatabase } from "../../../../src/db/services/db/database.service";
vi.mock("mongoose");
const MODULE_PATH = "../../../../src/db/services/db/database.service";
type SpyInstance<T extends (...args: any) => any> = ReturnType<typeof vi.spyOn<any, Parameters<T>[0]>>;
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,
},
};
});
vi.mock("dotenv/config", () => ({}));
describe("database.service", () => {
const mockedMongooseConnect = vi.mocked(mongoose.connect);
const mockedMongooseConnect = vi.mocked(mongoose.connect);
const mockedMongooseDisconnect = vi.mocked(mongoose.disconnect);
const mockedConnectionOn = vi.mocked(mongoose.connection.on);
let consoleLogSpy: Mocked<typeof console.log>;
describe("database.service", () => {
let consoleLogSpy: SpyInstance<typeof console.log>;
let consoleErrorSpy: SpyInstance<typeof console.error>;
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => {}) as any;
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(() => {});
});
afterEach(() => {
vi.restoreAllMocks();
vi.unstubAllEnvs();
});
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);
describe("Success Scenarios", () => {
beforeEach(() => {
await expect(connectToDatabase()).rejects.toThrow(
"Missing environment variable: DB_CONN_STRING is required for database connection."
);
});
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);
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();
expect(mockedConnectionOn).toHaveBeenCalledWith('connected', expect.any(Function));
expect(mockedConnectionOn).toHaveBeenCalledWith('disconnected', expect.any(Function));
expect(mockedConnectionOn).toHaveBeenCalledWith('error', expect.any(Function));
});
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 connect using the correct environment variables", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb://localhost:27017/test');
vi.stubEnv('DB_NAME', 'test_database');
const promise1 = connectToDatabase();
const promise2 = connectToDatabase();
await connectToDatabase();
await Promise.all([promise1, promise2]);
expect(mockedMongooseConnect).toHaveBeenCalledWith(
"mongodb://localhost:27017/test",
{ dbName: "test_database" }
);
});
it("should log a success message on connection", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb://any');
vi.stubEnv('DB_NAME', 'any');
await connectToDatabase();
expect(consoleLogSpy).toHaveBeenCalledWith("Connected to MongoDB with Mongoose");
});
it("should resolve to undefined on successful connection", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb://any');
vi.stubEnv('DB_NAME', 'any');
await expect(connectToDatabase()).resolves.toBeUndefined();
});
it("should handle different connection string formats (e.g., Atlas)", async () => {
vi.stubEnv('DB_CONN_STRING', 'mongodb+srv://user:pass@cluster.mongodb.net/');
vi.stubEnv('DB_NAME', 'cloud_database');
await connectToDatabase();
expect(mockedMongooseConnect).toHaveBeenCalledWith(
"mongodb+srv://user:pass@cluster.mongodb.net/",
{ dbName: "cloud_database" }
);
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
});
});
describe("Failure Scenarios", () => {
it("should propagate connection errors and not log success", async () => {
const connectionError = new Error("Connection failed");
mockedMongooseConnect.mockRejectedValue(connectionError);
vi.stubEnv('DB_CONN_STRING', 'mongodb://fail');
vi.stubEnv('DB_NAME', 'fail_db');
await expect(connectToDatabase()).rejects.toThrow(connectionError);
expect(consoleLogSpy).not.toHaveBeenCalledWith("Connected to MongoDB with Mongoose");
describe("Retry Logic", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("should handle specific Mongoose errors (e.g., MongoAuthenticationError)", async () => {
const authError = Object.assign(new Error("Authentication failed"), { name: "MongoAuthenticationError" });
mockedMongooseConnect.mockRejectedValue(authError);
vi.stubEnv('DB_CONN_STRING', 'mongodb://fail');
vi.stubEnv('DB_NAME', 'fail_db');
it("should retry after 5 seconds when first time fails", async () => {
const connectionError = new Error("DB not ready");
mockedMongooseConnect
.mockRejectedValueOnce(connectionError)
.mockResolvedValueOnce(undefined as any);
await expect(connectToDatabase()).rejects.toThrow(authError);
const { connectToDatabase } = await import(MODULE_PATH);
const connectionPromise = connectToDatabase();
await vi.advanceTimersByTimeAsync(1);
expect(mockedMongooseConnect).toHaveBeenCalledTimes(1);
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to connect to MongoDB. Retrying in 5 seconds...", connectionError);
await vi.advanceTimersByTimeAsync(5000);
expect(mockedMongooseConnect).toHaveBeenCalledTimes(2);
await expect(connectionPromise).resolves.toBeUndefined();
});
});
});
describe("disconnectFromDatabase", () => {
it("should call mongoose.disconnect, when connection is established", async () => {
mockedMongooseConnect.mockResolvedValue(undefined as any);
mockedMongooseDisconnect.mockResolvedValue(undefined as any);
const { connectToDatabase, disconnectFromDatabase } = await import(MODULE_PATH);
await connectToDatabase();
const connectedCallback = mockedConnectionOn.mock.calls.find(call => call[0] === 'connected')?.[1];
if (connectedCallback) {
connectedCallback();
}
await disconnectFromDatabase();
expect(mockedMongooseDisconnect).toHaveBeenCalledTimes(1);
expect(consoleLogSpy).toHaveBeenCalledWith("Disconnected from MongoDB.");
});
it("should NOT call.disconnect NICHT, when no connection is established", async () => {
const { disconnectFromDatabase } = await import(MODULE_PATH);
await disconnectFromDatabase();
expect(mockedMongooseDisconnect).not.toHaveBeenCalled();
});
});
});