bigger refactoring of database.service.ts
This commit is contained in:
@@ -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