Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 35 additions & 3 deletions backend/src/disputes/disputes.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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);

Expand All @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
6 changes: 3 additions & 3 deletions backend/src/disputes/disputes.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 4 additions & 0 deletions backend/src/markets/entities/market.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions backend/src/markets/markets.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand Down
52 changes: 52 additions & 0 deletions backend/src/notifications/notification-generator.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions backend/test/markets.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down
Loading