Skip to content
Open
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
232 changes: 230 additions & 2 deletions backend/src/analytics/analytics.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ describe('AnalyticsService', () => {
let module: TestingModule;
let usersRepository: jest.Mocked<Pick<Repository<User>, 'findOne'>>;
let predictionsRepository: jest.Mocked<
Pick<Repository<Prediction>, 'createQueryBuilder'>
Pick<Repository<Prediction>, 'createQueryBuilder' | 'find'>
>;
let leaderboardRepository: jest.Mocked<
Pick<Repository<LeaderboardEntry>, 'createQueryBuilder'>
Expand Down Expand Up @@ -78,7 +78,7 @@ describe('AnalyticsService', () => {
beforeEach(async () => {
usersRepository = { findOne: jest.fn() };
leaderboardRepository = { createQueryBuilder: jest.fn() };
predictionsRepository = { createQueryBuilder: jest.fn() };
predictionsRepository = { createQueryBuilder: jest.fn(), find: jest.fn() };
marketHistoryRepository = { createQueryBuilder: jest.fn() };

module = await Test.createTestingModule({
Expand Down Expand Up @@ -302,5 +302,233 @@ describe('AnalyticsService', () => {
'Market "invalid" not found',
);
});

it('should apply to/from date filters when provided', async () => {
const mockMarket = { id: 'market-1', title: 'Market 1' } as Market;
const marketsRepository = module.get(getRepositoryToken(Market));
const marketHistoryRepository = module.get(
getRepositoryToken(MarketHistory),
);

jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(mockMarket);

const qb = {
where: jest.fn().mockReturnThis(),
andWhere: jest.fn().mockReturnThis(),
orderBy: jest.fn().mockReturnThis(),
getMany: jest.fn().mockResolvedValue([]),
};
jest
.spyOn(marketHistoryRepository, 'createQueryBuilder')
.mockReturnValue(qb as any);

await service.getMarketHistory(
'market-1',
'2025-01-01',
'2025-12-31',
);

expect(qb.andWhere).toHaveBeenCalledWith(
'history.recorded_at >= :from',
expect.any(Object),
);
expect(qb.andWhere).toHaveBeenCalledWith(
'history.recorded_at <= :to',
expect.any(Object),
);
});
});

describe('logActivity', () => {
it('should create and save an activity log entry', async () => {
const activityLogsRepository = module.get(
getRepositoryToken(ActivityLog),
);
const mockLog = {
userId: 'user-id-1',
actionType: 'prediction_submitted',
} as ActivityLog;

jest.spyOn(activityLogsRepository, 'create').mockReturnValue(mockLog);
jest.spyOn(activityLogsRepository, 'save').mockResolvedValue(mockLog);

const result = await service.logActivity(
'user-id-1',
'prediction_submitted',
{ marketId: 'market-1' },
'192.168.1.1',
);

expect(activityLogsRepository.create).toHaveBeenCalledWith({
userId: 'user-id-1',
actionType: 'prediction_submitted',
actionDetails: { marketId: 'market-1' },
ipAddress: '192.168.1.1',
});
expect(result).toEqual(mockLog);
});
});

describe('getMarketAnalytics', () => {
it('should return market analytics with outcome distribution', async () => {
const mockMarket = {
id: 'market-1',
title: 'Will ETH reach $5k?',
total_pool_stroops: '50000000',
participant_count: 3,
outcome_options: ['Yes', 'No'],
end_time: new Date(Date.now() + 3600 * 1000).toISOString(),
} as unknown as Market;

const mockPredictions = [
{ chosen_outcome: 'Yes' },
{ chosen_outcome: 'Yes' },
{ chosen_outcome: 'No' },
] as any[];

const marketsRepository = module.get(getRepositoryToken(Market));
const predictionsRepository = module.get(getRepositoryToken(Prediction));

jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(mockMarket);
jest
.spyOn(predictionsRepository, 'find')
.mockResolvedValue(mockPredictions);

const result = await service.getMarketAnalytics('market-1');

expect(result.market_id).toBe('market-1');
expect(result.total_pool_stroops).toBe('50000000');
expect(result.participant_count).toBe(3);
expect(result.outcome_distribution).toHaveLength(2);

const yesEntry = result.outcome_distribution.find(
(o) => o.outcome === 'Yes',
);
expect(yesEntry?.count).toBe(2);
expect(yesEntry?.percentage).toBeCloseTo(66.67, 1);

const noEntry = result.outcome_distribution.find(
(o) => o.outcome === 'No',
);
expect(noEntry?.count).toBe(1);
});

it('should throw NotFoundException for unknown market', async () => {
const marketsRepository = module.get(getRepositoryToken(Market));
jest.spyOn(marketsRepository, 'findOne').mockResolvedValue(null);

await expect(service.getMarketAnalytics('unknown')).rejects.toThrow(
'Market "unknown" not found',
);
});
});

describe('getCategoryAnalytics', () => {
it('should aggregate markets by category', async () => {
const mockMarkets = [
{
category: 'Crypto',
is_resolved: false,
is_cancelled: false,
total_pool_stroops: '10000000',
participant_count: 20,
},
{
category: 'Crypto',
is_resolved: true,
is_cancelled: false,
total_pool_stroops: '5000000',
participant_count: 10,
},
{
category: 'Sports',
is_resolved: false,
is_cancelled: false,
total_pool_stroops: '2000000',
participant_count: 5,
},
] as Market[];

const marketsRepository = module.get(getRepositoryToken(Market));
jest.spyOn(marketsRepository, 'find').mockResolvedValue(mockMarkets);

const result = await service.getCategoryAnalytics();

expect(result.categories).toHaveLength(2);

const crypto = result.categories.find((c) => c.name === 'Crypto');
expect(crypto?.total_markets).toBe(2);
expect(crypto?.active_markets).toBe(1);
expect(crypto?.total_volume_stroops).toBe('15000000');
expect(crypto?.trending).toBe(false); // 1/2 = 50%, not > 50%

const sports = result.categories.find((c) => c.name === 'Sports');
expect(sports?.total_markets).toBe(1);
expect(sports?.active_markets).toBe(1);
expect(sports?.trending).toBe(true); // 1/1 = 100% > 50%
});

it('should sort categories by volume descending', async () => {
const mockMarkets = [
{
category: 'Low',
is_resolved: false,
is_cancelled: false,
total_pool_stroops: '1000',
participant_count: 1,
},
{
category: 'High',
is_resolved: false,
is_cancelled: false,
total_pool_stroops: '9000000',
participant_count: 50,
},
] as Market[];

const marketsRepository = module.get(getRepositoryToken(Market));
jest.spyOn(marketsRepository, 'find').mockResolvedValue(mockMarkets);

const result = await service.getCategoryAnalytics();

expect(result.categories[0].name).toBe('High');
expect(result.categories[1].name).toBe('Low');
});
});

describe('getUserTrends', () => {
it('should throw NotFoundException for unknown user address', async () => {
usersRepository.findOne.mockResolvedValue(null);

await expect(
service.getUserTrends('GUNKNOWN'),
).rejects.toThrow('User with address GUNKNOWN not found');
});

it('should return trend data for a known user', async () => {
usersRepository.findOne.mockResolvedValue(baseUser);

const predictionsRepository = module.get(getRepositoryToken(Prediction));
jest.spyOn(predictionsRepository, 'find').mockResolvedValue([]);

const result = await service.getUserTrends('GADDR', 30);

expect(result.address).toBe('GADDR');
expect(Array.isArray(result.accuracy_trend)).toBe(true);
expect(Array.isArray(result.prediction_volume_trend)).toBe(true);
expect(Array.isArray(result.profit_loss_trend)).toBe(true);
expect(Array.isArray(result.category_performance)).toBe(true);
});

it('should clamp days to max 90', async () => {
usersRepository.findOne.mockResolvedValue(baseUser);

const predictionsRepository = module.get(getRepositoryToken(Prediction));
jest.spyOn(predictionsRepository, 'find').mockResolvedValue([]);

const result = await service.getUserTrends('GADDR', 999);

expect(result.address).toBe('GADDR');
});
});
});
Loading
Loading