From c48f1471bdfbad7875340f8cc13c0006f58941c4 Mon Sep 17 00:00:00 2001 From: yasser Date: Tue, 24 Mar 2026 15:14:24 +0100 Subject: [PATCH 1/2] fixed mongodb repository tests results --- mongo-audit-test.txt | Bin 0 -> 8014 bytes .../mongodb/mongo-audit.repository.spec.ts | 484 +++++++++++++++++- 2 files changed, 460 insertions(+), 24 deletions(-) create mode 100644 mongo-audit-test.txt diff --git a/mongo-audit-test.txt b/mongo-audit-test.txt new file mode 100644 index 0000000000000000000000000000000000000000..923a96b3e36c10900947fa7d237c81996f73d769 GIT binary patch literal 8014 zcmds++in|07{})ti6?-IRisLeXwFTcfg;k&omaYwFjJNkw3bJuizJr(XN{Xf<3 z*gf~pr*7andRMrX=6Y`59lFPQTGEQHV47|uh!x#W-OwFqepPQDX>_RhLJ)_7>uNO6 z`(x2&?pPR~>P^q%;$FD7-CG_*N8_GozR*)4PMhwDhcDbS&&fdR;H1$1zK2DgRlk1M zujzWs9Z9&M_bu^)bUZ=hy|XoipgW$gLK3WKejvD(Tl4GpHEwF$7tD!hK^F=7y0dCc zRN>>X{@ZS^^lbWQ`xUEGFpR|SKv)5p|hLP2KqwYzN~&nvhQmg zVA4Cjbyx3(?xB~mB~9Qt9chYp4XrgVobGw1liBt*k1?t{qBHcC!5+(^@U8paUDo}9 zXksTgza^h)>3*Uo^PybE+wx+(b>!}Agoj7IZwY_j%YH{vSp3}eIyv;7c~kF>bTuVg zTeJeS(cquo$6EP`xF9-+sJ8f7)0$I_@UImg7em3U%P;H~{WM^BqE*DxoAB6{_!^3O zkm$DCl`nk$#(BXLzK}*OB6rU;iU*YAcjS2w{@RkII^K?*(md~9xj)?>Ua|vW#y7Bh zx}q4{yYN_sg0VarEm~;RvCnf2cSWm@q`T#XCe)JkIx-NA1IbOCkc$_U1dFD4(B?>7Qc*1f zPf8f|kyCy5(rc|7$Qxpm?}gDkoZTcgT=fv#Zb0X&~ue$t8Jg!>_i%Q zELnQGN88Phvm8a!vDVKQRKHX4G_2_Z5grR$Pf4`4gaxW#3LR&)eR9iJW%$5bw0Cal z6jPd?()nuplyym#AXd|il*?Amf{Reu_j;qBpG{}697reA)sJj&&&Lf}ft>*HSdZTn zN1@1@wC?d4W+L8j18Kc!Ju0@U?YU*IK^BgbE#|Dh2{|_B1loCCkFk|=F)h1HuAg2P zY;Tl$3qAC7-89^$-po_|b4^(HJBhuzK2>}8Gzr;X7&4R}J-f%f4GacArUV_A@v1BUiDT zB8Oqkua%qe<%VFnQk=ZEZZV~LE1??pR2$kYte1FuAWYO)+n?CZYjX^fV!P`z=$;_i z=bK$xWBvwyzh*n^3VXgaahS9b0c++jWE#dgtF}tOC`knLwM=D=K5JdS?Z)1x> zTf3znLSN?o>QU5VLQA9=q=<0<#^ z$Q>icWZM=_ivxSc|5Dslnkid4_-nPL14P}sag%EPhpC#M?R{4(?Xajnzn@{8P5*-o zI!aIeYK%P)Us}p~>GL|=rSV$qWx67|FDJ8h+GFpo>RnS5MvALe35GZ@TW%@)9J%Xb z4-c8>rH3YOA!%7!x=Q-_d)h0J$I0lc?jz08A@oFzOvQO9XE>~FX#8)V7lzZ99mVXn zt{us_A*oY&Ub#i9W?qeF_c^iJmMiPD$`60j_PzD{n^ZzPubg-2kkVS0*WE8=DNdiM zaqJWu8e{JRe@ggF=2;A4yuaiG_^b7x-LUMhhs+sdm zJWh52SA>I3v^syrIA&MJ>Bz7Y$DAv(H>A^z`QUm923{hbrvDZ}9Jn)MA>@VU= zD^2Jw!s%5*{=li-bZBfoe_cT@d3TihjnPW0#QF6Zobz@iQM;)OsW#GnV={7#Ps{V> z{Wf!6_P%(HhKQ^7v;Z;fKB@0{t1{Z3r#m`d^LFZJoXzTR%T~dQ$Z4r%EZe4)HMMrV zP6+ufw(9_6-gUtB|Dz^bG*X+>n*3;-dkcM~;!KeflCZDhRH>;s+X?c;iW7X|k6hjL z88@uAHK`|)W%Ko9b*nf7QDJuoR(kv#??Yu+&Izr0b9zQq-~vO=gVm4#5xLkpzSG6L ieWm4TGS}O8THevQ*>@qn)Rt%Jf2$3>7vG2Nd-FdPG3)vO literal 0 HcmV?d00001 diff --git a/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts b/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts index 7764e97..98e2d25 100644 --- a/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts +++ b/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts @@ -3,35 +3,471 @@ * MONGODB AUDIT REPOSITORY - UNIT TESTS * ============================================================================ * - * Tests for MongoAuditRepository implementation. + * Tests for MongoAuditRepository implementation using proper Mongoose mocking. * - * @packageDocumentation - */ - -/** - * MongoDB repository tests are skipped pending proper Mongoose Model constructor mocking. - * - * Current issues: - * - Mock setup doesn't properly simulate Mongoose Model constructor behavior - * - Test assertions need updating to match actual implementation - * - Query chain mocks (find().sort().limit().exec()) need proper setup - * - * Tracked in: Task AK-007 - Fix MongoDB repository test mocks - * GitHub: https://github.com/CISCODE-MA/AuditKit/issues/TBD - * - * Test coverage needed: - * - CRUD operations (create, findById, update, delete) - * - Query operations (query, count, exists) + * Coverage: + * - CRUD operations (create, findById) + * - Query operations (findByActor, findByResource, query) + * - Count and exists operations * - Filtering (by action, actor, resource, date range) * - Pagination and sorting - * - Error handling (duplicate keys, network errors) * - Document transformation (_id to id mapping) + * - Error handling + * + * @packageDocumentation */ -describe.skip("MongoAuditRepository", () => { - it("placeholder - tests will be implemented in task AK-007", () => { - expect(true).toBe(true); + +import type { AuditLog } from "../../../core/types"; +import { ActorType, AuditActionType } from "../../../core/types"; + +import { MongoAuditRepository } from "./mongo-audit.repository"; + +describe("MongoAuditRepository", () => { + let repository: MongoAuditRepository; + let mockModel: any; + + const createMockLog = (overrides?: Partial): AuditLog => ({ + id: "log-1", + timestamp: new Date("2026-03-19T10:00:00.000Z"), + action: AuditActionType.CREATE, + actor: { + id: "user-1", + type: ActorType.USER, + name: "John Doe", + email: "john@example.com", + }, + resource: { + type: ActorType.USER, + id: "res-1", + label: "Test User", + }, + ipAddress: "192.0.2.1", + userAgent: "Mozilla/5.0", + ...overrides, + }); + + beforeEach(() => { + // Create a mock Mongoose model with constructor behavior + mockModel = jest.fn().mockImplementation((data: any) => ({ + ...data, + save: jest.fn().mockResolvedValue({ _id: data.id, ...data }), + })); + + // Add static methods to the mock model + mockModel.findOne = jest.fn(); + mockModel.find = jest.fn(); + mockModel.countDocuments = jest.fn(); + mockModel.deleteMany = jest.fn(); + + repository = new MongoAuditRepository(mockModel); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("create", () => { + it("should create and return audit log", async () => { + const log = createMockLog(); + const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); + mockModel.mockImplementation((data: any) => ({ + ...data, + save: saveMock, + })); + + const created = await repository.create(log); + + expect(mockModel).toHaveBeenCalledWith(log); + expect(saveMock).toHaveBeenCalled(); + expect(created.id).toBe(log.id); + expect(created.action).toBe(log.action); + }); + + it("should create log with changes", async () => { + const log = createMockLog({ + changes: { + name: { from: "Old", to: "New" }, + }, + }); + const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); + mockModel.mockImplementation((data: any) => ({ + ...data, + save: saveMock, + })); + + await repository.create(log); + + expect(mockModel).toHaveBeenCalledWith( + expect.objectContaining({ + changes: log.changes, + }), + ); + }); + + it("should create log with metadata", async () => { + const log = createMockLog({ + metadata: { correlationId: "corr-1" }, + }); + const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); + mockModel.mockImplementation((data: any) => ({ + ...data, + save: saveMock, + })); + + await repository.create(log); + + expect(mockModel).toHaveBeenCalledWith( + expect.objectContaining({ + metadata: log.metadata, + }), + ); + }); + }); + + describe("findById", () => { + it("should return log when it exists", async () => { + const log = createMockLog(); + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue({ _id: log.id, ...log }), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const found = await repository.findById(log.id); + + expect(mockModel.findOne).toHaveBeenCalledWith({ id: log.id }); + expect(chainMock.lean).toHaveBeenCalled(); + expect(chainMock.exec).toHaveBeenCalled(); + expect(found).toMatchObject({ + id: log.id, + action: log.action, + }); + }); + + it("should return null when log does not exist", async () => { + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(null), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const found = await repository.findById("non-existent"); + + expect(found).toBeNull(); + }); + + it("should transform _id to id", async () => { + const log = createMockLog(); + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue({ _id: log.id, ...log }), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const found = await repository.findById(log.id); + + expect(found).toHaveProperty("id"); + expect(found).not.toHaveProperty("_id"); + }); + }); + + describe("findByActor", () => { + it("should query by actor ID", async () => { + const log = createMockLog(); + const chainMock = { + sort: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([{ _id: log.id, ...log }]), + }; + mockModel.find.mockReturnValue(chainMock); + + await repository.findByActor("user-1"); + + expect(mockModel.find).toHaveBeenCalledWith( + expect.objectContaining({ + "actor.id": "user-1", + }), + ); + expect(chainMock.sort).toHaveBeenCalledWith({ timestamp: -1 }); + }); + + it("should apply action filter", async () => { + const chainMock = { + sort: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]), + }; + mockModel.find.mockReturnValue(chainMock); + + await repository.findByActor("user-1", { action: AuditActionType.CREATE }); + + expect(mockModel.find).toHaveBeenCalledWith( + expect.objectContaining({ + "actor.id": "user-1", + action: AuditActionType.CREATE, + }), + ); + }); + }); + + describe("findByResource", () => { + it("should query by resource type and ID", async () => { + const log = createMockLog(); + const chainMock = { + sort: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([{ _id: log.id, ...log }]), + }; + mockModel.find.mockReturnValue(chainMock); + + await repository.findByResource("user", "res-1"); + + expect(mockModel.find).toHaveBeenCalledWith( + expect.objectContaining({ + "resource.type": "user", + "resource.id": "res-1", + }), + ); + expect(chainMock.sort).toHaveBeenCalledWith({ timestamp: 1 }); + }); + }); + + describe("query", () => { + it("should build query without filters", async () => { + const chainMock = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]), + }; + mockModel.find.mockReturnValue(chainMock); + + const countChainMock = { + exec: jest.fn().mockResolvedValue(0), + }; + mockModel.countDocuments.mockReturnValue(countChainMock); + + await repository.query({}); + + expect(mockModel.find).toHaveBeenCalledWith({}); + expect(chainMock.sort).toHaveBeenCalled(); + expect(chainMock.limit).toHaveBeenCalledWith(20); // default limit is 20 + }); + + it("should filter by action", async () => { + const chainMock = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]), + }; + mockModel.find.mockReturnValue(chainMock); + + const countChainMock = { + exec: jest.fn().mockResolvedValue(0), + }; + mockModel.countDocuments.mockReturnValue(countChainMock); + + await repository.query({ action: AuditActionType.CREATE }); + + expect(mockModel.find).toHaveBeenCalledWith({ action: AuditActionType.CREATE }); + }); + + it("should apply pagination", async () => { + const chainMock = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]), + }; + mockModel.find.mockReturnValue(chainMock); + + const countChainMock = { + exec: jest.fn().mockResolvedValue(150), + }; + mockModel.countDocuments.mockReturnValue(countChainMock); + + await repository.query({ limit: 50, page: 2 }); + + expect(chainMock.skip).toHaveBeenCalledWith(50); + expect(chainMock.limit).toHaveBeenCalledWith(50); + }); + + it("should return pagination metadata", async () => { + const chainMock = { + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([]), + }; + mockModel.find.mockReturnValue(chainMock); + + const countChainMock = { + exec: jest.fn().mockResolvedValue(150), + }; + mockModel.countDocuments.mockReturnValue(countChainMock); + + const result = await repository.query({ limit: 50, page: 1 }); + + expect(result.total).toBe(150); + expect(result.page).toBe(1); + expect(result.limit).toBe(50); + expect(result.pages).toBe(3); + }); + }); + + describe("count", () => { + it("should count all documents without filters", async () => { + const countChainMock = { + exec: jest.fn().mockResolvedValue(42), + }; + mockModel.countDocuments.mockReturnValue(countChainMock); + + const count = await repository.count(); + + expect(mockModel.countDocuments).toHaveBeenCalledWith({}); + expect(count).toBe(42); + }); + + it("should count with filters", async () => { + const countChainMock = { + exec: jest.fn().mockResolvedValue(10), + }; + mockModel.countDocuments.mockReturnValue(countChainMock); + + const count = await repository.count({ action: AuditActionType.CREATE }); + + expect(mockModel.countDocuments).toHaveBeenCalledWith({ action: AuditActionType.CREATE }); + expect(count).toBe(10); + }); + }); + + describe("exists", () => { + it("should return true when document exists", async () => { + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue({ id: "log-1" }), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const exists = await repository.exists({ action: AuditActionType.CREATE }); + + expect(exists).toBe(true); + }); + + it("should return false when document does not exist", async () => { + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(null), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const exists = await repository.exists({ action: AuditActionType.DELETE }); + + expect(exists).toBe(false); + }); + }); + + describe("deleteOlderThan", () => { + it("should delete documents older than date", async () => { + const cutoffDate = new Date("2023-01-01"); + const deleteChainMock = { + exec: jest.fn().mockResolvedValue({ deletedCount: 5 }), + }; + mockModel.deleteMany.mockReturnValue(deleteChainMock); + + const deleted = await repository.deleteOlderThan(cutoffDate); + + expect(mockModel.deleteMany).toHaveBeenCalledWith({ + timestamp: { $lt: cutoffDate }, + }); + expect(deleted).toBe(5); + }); + + it("should handle no deletions", async () => { + const deleteChainMock = { + exec: jest.fn().mockResolvedValue({ deletedCount: 0 }), + }; + mockModel.deleteMany.mockReturnValue(deleteChainMock); + + const deleted = await repository.deleteOlderThan(new Date("2020-01-01")); + + expect(deleted).toBe(0); + }); }); - // Test implementation removed to resolve SonarQube code duplication (31.8%) - // Will be properly implemented with correct Mongoose mocking patterns in AK-007 + describe("document transformation", () => { + it("should transform _id to id in returned documents", async () => { + const log = createMockLog(); + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue({ + _id: log.id, + id: log.id, + timestamp: log.timestamp, + action: log.action, + actor: log.actor, + resource: log.resource, + }), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const found = await repository.findById(log.id); + + expect(found).toHaveProperty("id", log.id); + expect(found).not.toHaveProperty("_id"); + }); + + it("should handle null document", async () => { + const chainMock = { + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(null), + }; + mockModel.findOne.mockReturnValue(chainMock); + + const found = await repository.findById("non-existent"); + + expect(found).toBeNull(); + }); + + it("should transform array of documents", async () => { + const log1 = createMockLog({ id: "log-1" }); + const log2 = createMockLog({ id: "log-2" }); + const chainMock = { + sort: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue([ + { + _id: "mongodb-id-1", + id: log1.id, + timestamp: log1.timestamp, + action: log1.action, + actor: log1.actor, + resource: log1.resource, + }, + { + _id: "mongodb-id-2", + id: log2.id, + timestamp: log2.timestamp, + action: log2.action, + actor: log2.actor, + resource: log2.resource, + }, + ]), + }; + mockModel.find.mockReturnValue(chainMock); + + const logs = await repository.findByActor("user-1"); + + expect(logs).toHaveLength(2); + expect(logs[0]).toHaveProperty("id"); + expect(logs[0]).not.toHaveProperty("_id"); + expect(logs[1]).toHaveProperty("id"); + expect(logs[1]).not.toHaveProperty("_id"); + }); + }); }); From b824a40ed385bdc9dad488ee7718007d4c8b2361 Mon Sep 17 00:00:00 2001 From: yasser Date: Tue, 24 Mar 2026 15:19:45 +0100 Subject: [PATCH 2/2] reduced code duplication --- .../mongodb/mongo-audit.repository.spec.ts | 221 +++++++----------- 1 file changed, 82 insertions(+), 139 deletions(-) diff --git a/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts b/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts index 98e2d25..4dbca15 100644 --- a/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts +++ b/src/infra/repositories/mongodb/mongo-audit.repository.spec.ts @@ -46,6 +46,38 @@ describe("MongoAuditRepository", () => { ...overrides, }); + const setupCreateModelMock = (log: AuditLog) => { + const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); + mockModel.mockImplementation((data: any) => ({ + ...data, + save: saveMock, + })); + return saveMock; + }; + + const createLeanExecChain = (result: any) => ({ + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(result), + }); + + const createSortedLeanExecChain = (result: any) => ({ + sort: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(result), + }); + + const createQueryChain = (result: any) => ({ + sort: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + lean: jest.fn().mockReturnThis(), + exec: jest.fn().mockResolvedValue(result), + }); + + const createExecChain = (result: any) => ({ + exec: jest.fn().mockResolvedValue(result), + }); + beforeEach(() => { // Create a mock Mongoose model with constructor behavior mockModel = jest.fn().mockImplementation((data: any) => ({ @@ -69,11 +101,7 @@ describe("MongoAuditRepository", () => { describe("create", () => { it("should create and return audit log", async () => { const log = createMockLog(); - const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); - mockModel.mockImplementation((data: any) => ({ - ...data, - save: saveMock, - })); + const saveMock = setupCreateModelMock(log); const created = await repository.create(log); @@ -89,11 +117,7 @@ describe("MongoAuditRepository", () => { name: { from: "Old", to: "New" }, }, }); - const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); - mockModel.mockImplementation((data: any) => ({ - ...data, - save: saveMock, - })); + setupCreateModelMock(log); await repository.create(log); @@ -108,11 +132,7 @@ describe("MongoAuditRepository", () => { const log = createMockLog({ metadata: { correlationId: "corr-1" }, }); - const saveMock = jest.fn().mockResolvedValue({ _id: log.id, ...log }); - mockModel.mockImplementation((data: any) => ({ - ...data, - save: saveMock, - })); + setupCreateModelMock(log); await repository.create(log); @@ -127,10 +147,7 @@ describe("MongoAuditRepository", () => { describe("findById", () => { it("should return log when it exists", async () => { const log = createMockLog(); - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue({ _id: log.id, ...log }), - }; + const chainMock = createLeanExecChain({ _id: log.id, ...log }); mockModel.findOne.mockReturnValue(chainMock); const found = await repository.findById(log.id); @@ -145,10 +162,7 @@ describe("MongoAuditRepository", () => { }); it("should return null when log does not exist", async () => { - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(null), - }; + const chainMock = createLeanExecChain(null); mockModel.findOne.mockReturnValue(chainMock); const found = await repository.findById("non-existent"); @@ -158,10 +172,7 @@ describe("MongoAuditRepository", () => { it("should transform _id to id", async () => { const log = createMockLog(); - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue({ _id: log.id, ...log }), - }; + const chainMock = createLeanExecChain({ _id: log.id, ...log }); mockModel.findOne.mockReturnValue(chainMock); const found = await repository.findById(log.id); @@ -174,11 +185,7 @@ describe("MongoAuditRepository", () => { describe("findByActor", () => { it("should query by actor ID", async () => { const log = createMockLog(); - const chainMock = { - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([{ _id: log.id, ...log }]), - }; + const chainMock = createSortedLeanExecChain([{ _id: log.id, ...log }]); mockModel.find.mockReturnValue(chainMock); await repository.findByActor("user-1"); @@ -192,11 +199,7 @@ describe("MongoAuditRepository", () => { }); it("should apply action filter", async () => { - const chainMock = { - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([]), - }; + const chainMock = createSortedLeanExecChain([]); mockModel.find.mockReturnValue(chainMock); await repository.findByActor("user-1", { action: AuditActionType.CREATE }); @@ -213,11 +216,7 @@ describe("MongoAuditRepository", () => { describe("findByResource", () => { it("should query by resource type and ID", async () => { const log = createMockLog(); - const chainMock = { - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([{ _id: log.id, ...log }]), - }; + const chainMock = createSortedLeanExecChain([{ _id: log.id, ...log }]); mockModel.find.mockReturnValue(chainMock); await repository.findByResource("user", "res-1"); @@ -234,18 +233,10 @@ describe("MongoAuditRepository", () => { describe("query", () => { it("should build query without filters", async () => { - const chainMock = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([]), - }; + const chainMock = createQueryChain([]); mockModel.find.mockReturnValue(chainMock); - const countChainMock = { - exec: jest.fn().mockResolvedValue(0), - }; + const countChainMock = createExecChain(0); mockModel.countDocuments.mockReturnValue(countChainMock); await repository.query({}); @@ -256,18 +247,10 @@ describe("MongoAuditRepository", () => { }); it("should filter by action", async () => { - const chainMock = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([]), - }; + const chainMock = createQueryChain([]); mockModel.find.mockReturnValue(chainMock); - const countChainMock = { - exec: jest.fn().mockResolvedValue(0), - }; + const countChainMock = createExecChain(0); mockModel.countDocuments.mockReturnValue(countChainMock); await repository.query({ action: AuditActionType.CREATE }); @@ -276,18 +259,10 @@ describe("MongoAuditRepository", () => { }); it("should apply pagination", async () => { - const chainMock = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([]), - }; + const chainMock = createQueryChain([]); mockModel.find.mockReturnValue(chainMock); - const countChainMock = { - exec: jest.fn().mockResolvedValue(150), - }; + const countChainMock = createExecChain(150); mockModel.countDocuments.mockReturnValue(countChainMock); await repository.query({ limit: 50, page: 2 }); @@ -297,18 +272,10 @@ describe("MongoAuditRepository", () => { }); it("should return pagination metadata", async () => { - const chainMock = { - sort: jest.fn().mockReturnThis(), - skip: jest.fn().mockReturnThis(), - limit: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([]), - }; + const chainMock = createQueryChain([]); mockModel.find.mockReturnValue(chainMock); - const countChainMock = { - exec: jest.fn().mockResolvedValue(150), - }; + const countChainMock = createExecChain(150); mockModel.countDocuments.mockReturnValue(countChainMock); const result = await repository.query({ limit: 50, page: 1 }); @@ -322,9 +289,7 @@ describe("MongoAuditRepository", () => { describe("count", () => { it("should count all documents without filters", async () => { - const countChainMock = { - exec: jest.fn().mockResolvedValue(42), - }; + const countChainMock = createExecChain(42); mockModel.countDocuments.mockReturnValue(countChainMock); const count = await repository.count(); @@ -334,9 +299,7 @@ describe("MongoAuditRepository", () => { }); it("should count with filters", async () => { - const countChainMock = { - exec: jest.fn().mockResolvedValue(10), - }; + const countChainMock = createExecChain(10); mockModel.countDocuments.mockReturnValue(countChainMock); const count = await repository.count({ action: AuditActionType.CREATE }); @@ -348,10 +311,7 @@ describe("MongoAuditRepository", () => { describe("exists", () => { it("should return true when document exists", async () => { - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue({ id: "log-1" }), - }; + const chainMock = createLeanExecChain({ id: "log-1" }); mockModel.findOne.mockReturnValue(chainMock); const exists = await repository.exists({ action: AuditActionType.CREATE }); @@ -360,10 +320,7 @@ describe("MongoAuditRepository", () => { }); it("should return false when document does not exist", async () => { - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(null), - }; + const chainMock = createLeanExecChain(null); mockModel.findOne.mockReturnValue(chainMock); const exists = await repository.exists({ action: AuditActionType.DELETE }); @@ -375,9 +332,7 @@ describe("MongoAuditRepository", () => { describe("deleteOlderThan", () => { it("should delete documents older than date", async () => { const cutoffDate = new Date("2023-01-01"); - const deleteChainMock = { - exec: jest.fn().mockResolvedValue({ deletedCount: 5 }), - }; + const deleteChainMock = createExecChain({ deletedCount: 5 }); mockModel.deleteMany.mockReturnValue(deleteChainMock); const deleted = await repository.deleteOlderThan(cutoffDate); @@ -389,9 +344,7 @@ describe("MongoAuditRepository", () => { }); it("should handle no deletions", async () => { - const deleteChainMock = { - exec: jest.fn().mockResolvedValue({ deletedCount: 0 }), - }; + const deleteChainMock = createExecChain({ deletedCount: 0 }); mockModel.deleteMany.mockReturnValue(deleteChainMock); const deleted = await repository.deleteOlderThan(new Date("2020-01-01")); @@ -403,17 +356,14 @@ describe("MongoAuditRepository", () => { describe("document transformation", () => { it("should transform _id to id in returned documents", async () => { const log = createMockLog(); - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue({ - _id: log.id, - id: log.id, - timestamp: log.timestamp, - action: log.action, - actor: log.actor, - resource: log.resource, - }), - }; + const chainMock = createLeanExecChain({ + _id: log.id, + id: log.id, + timestamp: log.timestamp, + action: log.action, + actor: log.actor, + resource: log.resource, + }); mockModel.findOne.mockReturnValue(chainMock); const found = await repository.findById(log.id); @@ -423,10 +373,7 @@ describe("MongoAuditRepository", () => { }); it("should handle null document", async () => { - const chainMock = { - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue(null), - }; + const chainMock = createLeanExecChain(null); mockModel.findOne.mockReturnValue(chainMock); const found = await repository.findById("non-existent"); @@ -437,28 +384,24 @@ describe("MongoAuditRepository", () => { it("should transform array of documents", async () => { const log1 = createMockLog({ id: "log-1" }); const log2 = createMockLog({ id: "log-2" }); - const chainMock = { - sort: jest.fn().mockReturnThis(), - lean: jest.fn().mockReturnThis(), - exec: jest.fn().mockResolvedValue([ - { - _id: "mongodb-id-1", - id: log1.id, - timestamp: log1.timestamp, - action: log1.action, - actor: log1.actor, - resource: log1.resource, - }, - { - _id: "mongodb-id-2", - id: log2.id, - timestamp: log2.timestamp, - action: log2.action, - actor: log2.actor, - resource: log2.resource, - }, - ]), - }; + const chainMock = createSortedLeanExecChain([ + { + _id: "mongodb-id-1", + id: log1.id, + timestamp: log1.timestamp, + action: log1.action, + actor: log1.actor, + resource: log1.resource, + }, + { + _id: "mongodb-id-2", + id: log2.id, + timestamp: log2.timestamp, + action: log2.action, + actor: log2.actor, + resource: log2.resource, + }, + ]); mockModel.find.mockReturnValue(chainMock); const logs = await repository.findByActor("user-1");