diff --git a/__tests__/mongoClient.test.ts b/__tests__/mongoClient.test.ts new file mode 100644 index 000000000..67b49bd6d --- /dev/null +++ b/__tests__/mongoClient.test.ts @@ -0,0 +1,58 @@ +/** @jest-environment node */ +import { getClient, resetClient } from '@utils/mongodb/mongoClient.mjs'; +import { MongoClient } from 'mongodb'; + +describe('getClient', () => { + beforeEach(async () => { + await resetClient(); + jest.restoreAllMocks(); + }); + + it('should throw an error if MONGODB_URI is missing', async () => { + const originalUri = process.env.MONGODB_URI; + delete process.env.MONGODB_URI; + + await resetClient(); + + await expect(getClient()).rejects.toThrow( + 'Missing MONGODB_URI environment variable.' + ); + + process.env.MONGODB_URI = originalUri; + }); + + it('should return the same instance on multiple calls', async () => { + const mockDb = { db: jest.fn() }; + const spy = jest + .spyOn(MongoClient.prototype, 'connect') + .mockResolvedValue(mockDb as any); + + const c1 = await getClient(); + const c2 = await getClient(); + + expect(c1).toBe(c2); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should dedupe concurrent callers using cachedPromise', async () => { + const mockDb = { db: jest.fn() }; + const spy = jest + .spyOn(MongoClient.prototype, 'connect') + .mockResolvedValue(mockDb as any); + const [c1, c2] = await Promise.all([getClient(), getClient()]); + expect(c1).toBe(c2); + expect(spy).toHaveBeenCalledTimes(1); + }); + + it('should retry after a failed connection', async () => { + const spy = jest + .spyOn(MongoClient.prototype, 'connect') + .mockRejectedValueOnce(new Error('Network Fail')) + .mockResolvedValueOnce({ db: jest.fn() } as any); + + await expect(getClient()).rejects.toThrow('Network Fail'); + + await expect(getClient()).resolves.toBeDefined(); + expect(spy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/app/(api)/_utils/mongodb/mongoClient.mjs b/app/(api)/_utils/mongodb/mongoClient.mjs index 33bcfaaf5..615b4bca5 100644 --- a/app/(api)/_utils/mongodb/mongoClient.mjs +++ b/app/(api)/_utils/mongodb/mongoClient.mjs @@ -1,15 +1,41 @@ import { MongoClient } from 'mongodb'; -const uri = process.env.MONGODB_URI; let cachedClient = null; +let cachedPromise = null; export async function getClient() { + const uri = process.env.MONGODB_URI; + if (!uri) { + throw new Error('Missing MONGODB_URI environment variable.'); + } + if (cachedClient) { return cachedClient; } - const client = new MongoClient(uri); - cachedClient = client; - return cachedClient; + + if (!cachedPromise) { + const client = new MongoClient(uri); + cachedPromise = client + .connect() + .then((connectedClient) => { + cachedClient = connectedClient; + return connectedClient; + }) + .catch((error) => { + client.close().catch(() => {}); + cachedPromise = null; + cachedClient = null; + throw error; + }); + } + + return cachedPromise; +} + +// Helper function for testing +export async function resetClient() { + cachedClient = null; + cachedPromise = null; } export async function getDatabase() {