bigger refactoring of database.service.ts
This commit is contained in:
@@ -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
@@ -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};
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user