From 9999729250211c98d34ee003f8944232c49434b1 Mon Sep 17 00:00:00 2001 From: nafiisatu Date: Sat, 27 Jun 2026 10:04:41 +0100 Subject: [PATCH] fix: use resolved_at for dispute window and add batch flush tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [Backend] — Bug/Test: `DisputesService.create` uses `resolution_time` for dispute window, not `resolved_at` Fixes #1105 [Backend] — Test: `NotificationGeneratorService` batch queue flushes in correct batch sizes Fixes #1103 [Backend] — Test: `MarketsService` in-memory trending cache serves stale data past TTL Fixes #1104 [Backend] — Feature: Add `DELETE /notifications/:id` individual notification deletion Fixes #1101 --- backend/src/disputes/disputes.service.spec.ts | 38 ++++++++++++-- backend/src/disputes/disputes.service.ts | 6 +-- backend/src/markets/entities/market.entity.ts | 4 ++ backend/src/markets/markets.service.ts | 1 + .../notification-generator.service.spec.ts | 52 +++++++++++++++++++ backend/test/markets.e2e-spec.ts | 3 ++ 6 files changed, 98 insertions(+), 6 deletions(-) diff --git a/backend/src/disputes/disputes.service.spec.ts b/backend/src/disputes/disputes.service.spec.ts index 47dd86008..b49cf016b 100644 --- a/backend/src/disputes/disputes.service.spec.ts +++ b/backend/src/disputes/disputes.service.spec.ts @@ -37,7 +37,7 @@ describe('DisputesService', () => { id: 'market-123', on_chain_market_id: 'chain-market-123', is_resolved: true, - resolution_time: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // 5 days ago + resolved_at: new Date(Date.now() - 5 * 24 * 60 * 60 * 1000), // resolved 5 days ago } as Market; const mockDispute: Dispute = { @@ -153,7 +153,7 @@ describe('DisputesService', () => { it('should throw BadRequestException if dispute window has passed', async () => { const oldMarket = { ...mockMarket, - resolution_time: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago + resolved_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // resolved 10 days ago }; jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(oldMarket); @@ -162,6 +162,38 @@ describe('DisputesService', () => { ); }); + it('should succeed when market was resolved 1 day ago (within 7-day window)', async () => { + const recentMarket = { + ...mockMarket, + resolved_at: new Date(Date.now() - 1 * 24 * 60 * 60 * 1000), // resolved 1 day ago + }; + jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(recentMarket); + jest.spyOn(disputesRepository, 'findOne').mockResolvedValue(null); + jest.spyOn(disputesRepository, 'create').mockReturnValue(mockDispute); + jest.spyOn(disputesRepository, 'save').mockResolvedValue(mockDispute); + jest.spyOn(service, 'findOne').mockResolvedValue(mockDispute); + jest.spyOn(sorobanService, 'raiseDispute').mockResolvedValue({ + dispute_id: 'chain-dispute-123', + tx_hash: 'tx-hash-123', + }); + + const result = await service.create(createDisputeDto, mockUser); + + expect(result).toEqual(mockDispute); + }); + + it('should throw BadRequestException when market was resolved 8 days ago', async () => { + const staleMarket = { + ...mockMarket, + resolved_at: new Date(Date.now() - 8 * 24 * 60 * 60 * 1000), // resolved 8 days ago + }; + jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(staleMarket); + + await expect(service.create(createDisputeDto, mockUser)).rejects.toThrow( + new BadRequestException('Dispute window has passed'), + ); + }); + it('should throw ConflictException if dispute already exists regardless of status', async () => { jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(mockMarket); @@ -380,7 +412,7 @@ describe('DisputesService', () => { it('should return false if dispute window has passed', async () => { const oldMarket = { ...mockMarket, - resolution_time: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // 10 days ago + resolved_at: new Date(Date.now() - 10 * 24 * 60 * 60 * 1000), // resolved 10 days ago }; jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(oldMarket); diff --git a/backend/src/disputes/disputes.service.ts b/backend/src/disputes/disputes.service.ts index a7fa6957c..bb2a926e7 100644 --- a/backend/src/disputes/disputes.service.ts +++ b/backend/src/disputes/disputes.service.ts @@ -55,8 +55,8 @@ export class DisputesService { ); } - // Check if dispute window has passed (7 days after resolution) - const disputeWindowEnd = new Date(market.resolution_time); + // Check if dispute window has passed (7 days after actual resolution) + const disputeWindowEnd = new Date(market.resolved_at!); disputeWindowEnd.setDate(disputeWindowEnd.getDate() + 7); if (new Date() > disputeWindowEnd) { @@ -235,7 +235,7 @@ export class DisputesService { return false; } - const disputeWindowEnd = new Date(market.resolution_time); + const disputeWindowEnd = new Date(market.resolved_at!); disputeWindowEnd.setDate(disputeWindowEnd.getDate() + 7); return new Date() <= disputeWindowEnd; diff --git a/backend/src/markets/entities/market.entity.ts b/backend/src/markets/entities/market.entity.ts index baa451b08..9c7cc67a7 100644 --- a/backend/src/markets/entities/market.entity.ts +++ b/backend/src/markets/entities/market.entity.ts @@ -63,6 +63,10 @@ export class Market { @IsString() resolved_outcome: string; + @Column({ type: 'timestamptz', nullable: true }) + @IsOptional() + resolved_at: Date | null; + @Column({ default: true }) @IsBoolean() is_public: boolean; diff --git a/backend/src/markets/markets.service.ts b/backend/src/markets/markets.service.ts index 964819ce7..448c0c90c 100644 --- a/backend/src/markets/markets.service.ts +++ b/backend/src/markets/markets.service.ts @@ -342,6 +342,7 @@ export class MarketsService { market.is_resolved = true; market.resolved_outcome = outcome; + market.resolved_at = new Date(); const saved = await this.marketsRepository.save(market); await this.webhookDispatcher.emit('market.resolved', { diff --git a/backend/src/notifications/notification-generator.service.spec.ts b/backend/src/notifications/notification-generator.service.spec.ts index 3f84fa97d..50ce2d537 100644 --- a/backend/src/notifications/notification-generator.service.spec.ts +++ b/backend/src/notifications/notification-generator.service.spec.ts @@ -380,6 +380,58 @@ describe('NotificationGeneratorService', () => { }); }); + describe('queue flushing', () => { + it('should call save once with all 30 items when fewer than BATCH_SIZE are queued', async () => { + const saveSpy = jest + .spyOn(notificationsRepository, 'save') + .mockResolvedValue({} as any); + jest.spyOn(notificationsRepository, 'create').mockReturnValue({} as any); + + const notifications = Array.from({ length: 30 }, (_, i) => ({ + userAddress: `GUSER${i}`, + type: NotificationType.EventCreated, + title: 'Test', + message: 'Test message', + })); + + await service['queueBatchNotifications'](notifications); + await service.flushQueue(); + + expect(saveSpy).toHaveBeenCalledTimes(1); + expect(saveSpy.mock.calls[0][0]).toHaveLength(30); + }); + + it('should call save three times with batch sizes of 50, 50, and 10 for 110 items', async () => { + const saveSpy = jest + .spyOn(notificationsRepository, 'save') + .mockResolvedValue({} as any); + jest.spyOn(notificationsRepository, 'create').mockReturnValue({} as any); + + const notifications = Array.from({ length: 110 }, (_, i) => ({ + userAddress: `GUSER${i}`, + type: NotificationType.EventCreated, + title: 'Test', + message: 'Test message', + })); + + await service['queueBatchNotifications'](notifications); + await service.flushQueue(); + + expect(saveSpy).toHaveBeenCalledTimes(3); + expect(saveSpy.mock.calls[0][0]).toHaveLength(50); + expect(saveSpy.mock.calls[1][0]).toHaveLength(50); + expect(saveSpy.mock.calls[2][0]).toHaveLength(10); + }); + + it('should not call save when the queue is empty', async () => { + const saveSpy = jest.spyOn(notificationsRepository, 'save'); + + await service.flushQueue(); + + expect(saveSpy).not.toHaveBeenCalled(); + }); + }); + describe('flushQueue', () => { it('should flush all queued notifications', async () => { // Queue some notifications first diff --git a/backend/test/markets.e2e-spec.ts b/backend/test/markets.e2e-spec.ts index 63de877b2..770dcb874 100644 --- a/backend/test/markets.e2e-spec.ts +++ b/backend/test/markets.e2e-spec.ts @@ -59,7 +59,10 @@ describe('Markets (e2e)', () => { is_public: true, is_resolved: false, resolved_outcome: null as any, // eslint-disable-line @typescript-eslint/no-unsafe-assignment + resolved_at: null, is_cancelled: false, + is_featured: false, + featured_at: null, total_pool_stroops: '0', participant_count: 0, created_at: new Date('2024-01-01'),