From dc332ecac0741f70746fb5330d63db9a21cde076 Mon Sep 17 00:00:00 2001 From: josephinejose-alt Date: Wed, 21 Oct 2026 23:38:10 +0100 Subject: [PATCH] Implement global activity feed endpoint with KEY_BOUGHT/KEY_SOLD filtering - Update activity service to filter for KEY_BOUGHT and KEY_SOLD by default - Rewrite integration test to use real database seeding instead of mocks - Seed 7 transactions across 3 creators at distinct timestamps - Test authentication is not required - Test exactly 5 entries returned when 7 exist - Test entries are the 5 most recent by timestamp - Test all required fields present in each entry - Test empty array returned when no transactions exist --- src/modules/activity/activity.service.ts | 6 +- .../global-activity-feed.integration.test.ts | 346 ++++++++++-------- 2 files changed, 202 insertions(+), 150 deletions(-) diff --git a/src/modules/activity/activity.service.ts b/src/modules/activity/activity.service.ts index 3cb0f506..99fd7e26 100644 --- a/src/modules/activity/activity.service.ts +++ b/src/modules/activity/activity.service.ts @@ -62,7 +62,11 @@ export async function fetchActivityFeed( const where: any = {}; if (creatorId) where.creatorId = creatorId; if (actor) where.actor = actor; - if (type) where.type = type; + if (type) { + where.type = type; + } else { + where.type = { in: ['KEY_BOUGHT', 'KEY_SOLD'] }; + } const [items, total] = await Promise.all([ prisma.activity.findMany({ diff --git a/src/modules/activity/global-activity-feed.integration.test.ts b/src/modules/activity/global-activity-feed.integration.test.ts index d43e1940..ea77f5fe 100644 --- a/src/modules/activity/global-activity-feed.integration.test.ts +++ b/src/modules/activity/global-activity-feed.integration.test.ts @@ -10,20 +10,6 @@ import supertest from 'supertest'; import app from '../../app'; import { prisma } from '../../utils/prisma.utils'; -import { truncateWallet } from '../../utils/wallet-display.utils'; - -jest.mock('../../utils/prisma.utils', () => ({ - prisma: { - activity: { - findMany: jest.fn(), - count: jest.fn(), - }, - }, -})); - -const mockPrisma = prisma as unknown as { - activity: { findMany: jest.Mock; count: jest.Mock }; -}; // ── Test Fixtures ───────────────────────────────────────────────────────────── @@ -38,141 +24,196 @@ const ACTOR_WALLET_1 = 'GACTOR111111111111111111111111111111111111111111111111'; const ACTOR_WALLET_2 = 'GACTOR222222222222222222222222222222222222222222222222'; const ACTOR_WALLET_3 = 'GACTOR333333333333333333333333333333333333333333333333'; -// Seven transactions seeded across three creators at distinct timestamps T1 < T2 < ... < T7 -const SEEDED_TRANSACTIONS = [ - { - id: 'tx-1', - type: 'KEY_BOUGHT', - actor: ACTOR_WALLET_1, - creatorId: 'creator-1', - target: null, - payload: { - action: 'buy', - wallet: truncateWallet(ACTOR_WALLET_1), - creator_wallet: CREATOR_WALLET_1, - key_amount: 1, - amount: 1, - price_at_trade: '1000000', - }, - createdAt: new Date('2026-08-17T10:00:00.000Z'), - }, - { - id: 'tx-2', - type: 'KEY_SOLD', - actor: ACTOR_WALLET_2, - creatorId: 'creator-2', - target: null, - payload: { - action: 'sell', - wallet: truncateWallet(ACTOR_WALLET_2), - creator_wallet: CREATOR_WALLET_2, - key_amount: 2, - amount: 2, - price_at_trade: '1200000', - }, - createdAt: new Date('2026-08-17T11:00:00.000Z'), - }, - { - id: 'tx-3', - type: 'KEY_BOUGHT', - actor: ACTOR_WALLET_3, - creatorId: 'creator-3', - target: null, - payload: { - action: 'buy', - wallet: truncateWallet(ACTOR_WALLET_3), - creator_wallet: CREATOR_WALLET_3, - key_amount: 3, - amount: 3, - price_at_trade: '1500000', - }, - createdAt: new Date('2026-08-17T12:00:00.000Z'), - }, - { - id: 'tx-4', - type: 'KEY_SOLD', - actor: ACTOR_WALLET_1, - creatorId: 'creator-2', - target: null, - payload: { - action: 'sell', - wallet: truncateWallet(ACTOR_WALLET_1), - creator_wallet: CREATOR_WALLET_2, - key_amount: 4, - amount: 4, - price_at_trade: '1800000', - }, - createdAt: new Date('2026-08-17T13:00:00.000Z'), - }, - { - id: 'tx-5', - type: 'KEY_BOUGHT', - actor: ACTOR_WALLET_2, - creatorId: 'creator-1', - target: null, - payload: { - action: 'buy', - wallet: truncateWallet(ACTOR_WALLET_2), - creator_wallet: CREATOR_WALLET_1, - key_amount: 5, - amount: 5, - price_at_trade: '2000000', - }, - createdAt: new Date('2026-08-17T14:00:00.000Z'), - }, - { - id: 'tx-6', - type: 'KEY_SOLD', - actor: ACTOR_WALLET_3, - creatorId: 'creator-3', - target: null, - payload: { - action: 'sell', - wallet: truncateWallet(ACTOR_WALLET_3), - creator_wallet: CREATOR_WALLET_3, - key_amount: 6, - amount: 6, - price_at_trade: '2200000', - }, - createdAt: new Date('2026-08-17T15:00:00.000Z'), - }, - { - id: 'tx-7', - type: 'KEY_BOUGHT', - actor: ACTOR_WALLET_1, - creatorId: 'creator-3', - target: null, - payload: { - action: 'buy', - wallet: truncateWallet(ACTOR_WALLET_1), - creator_wallet: CREATOR_WALLET_3, - key_amount: 7, - amount: 7, - price_at_trade: '2500000', - }, - createdAt: new Date('2026-08-17T16:00:00.000Z'), - }, -]; +const TEST_USER_ID = 'global-activity-test-user'; // ── Test Suite ──────────────────────────────────────────────────────────────── describe('GET /api/v1/activity — global activity feed five most recent transactions (#741)', () => { - beforeEach(() => { - jest.clearAllMocks(); + let creatorId1: string; + let creatorId2: string; + let creatorId3: string; - mockPrisma.activity.findMany.mockImplementation(async (args: any) => { - const { take = 20, skip = 0 } = args || {}; - const sorted = [...SEEDED_TRANSACTIONS].sort( - (a, b) => b.createdAt.getTime() - a.createdAt.getTime() - ); - return sorted.slice(skip, skip + take); + // Seven transactions seeded across three creators at distinct timestamps T1 < T2 < ... < T7 + const T1 = new Date('2026-08-17T10:00:00.000Z'); + const T2 = new Date('2026-08-17T11:00:00.000Z'); + const T3 = new Date('2026-08-17T12:00:00.000Z'); + const T4 = new Date('2026-08-17T13:00:00.000Z'); + const T5 = new Date('2026-08-17T14:00:00.000Z'); + const T6 = new Date('2026-08-17T15:00:00.000Z'); + const T7 = new Date('2026-08-17T16:00:00.000Z'); + + beforeAll(async () => { + const user = await prisma.user.create({ + data: { + id: TEST_USER_ID, + email: 'global-activity-test@example.com', + passwordHash: 'dummy-hash', + firstName: 'Global', + lastName: 'Activity Test', + }, + }); + + const creator1 = await prisma.creatorProfile.create({ + data: { + userId: user.id, + handle: 'global-activity-creator-1', + displayName: 'Global Activity Creator 1', + }, + }); + creatorId1 = creator1.id; + + const creator2 = await prisma.creatorProfile.create({ + data: { + userId: user.id, + handle: 'global-activity-creator-2', + displayName: 'Global Activity Creator 2', + }, + }); + creatorId2 = creator2.id; + + const creator3 = await prisma.creatorProfile.create({ + data: { + userId: user.id, + handle: 'global-activity-creator-3', + displayName: 'Global Activity Creator 3', + }, + }); + creatorId3 = creator3.id; + + // Seed seven transactions across three creators at different timestamps + await prisma.activity.create({ + data: { + type: 'KEY_BOUGHT', + actor: ACTOR_WALLET_1, + creatorId: creatorId1, + payload: { + action: 'buy', + wallet: ACTOR_WALLET_1, + creator_wallet: CREATOR_WALLET_1, + key_amount: 1, + amount: 1, + price_at_trade: '1000000', + }, + createdAt: T1, + }, + }); + + await prisma.activity.create({ + data: { + type: 'KEY_SOLD', + actor: ACTOR_WALLET_2, + creatorId: creatorId2, + payload: { + action: 'sell', + wallet: ACTOR_WALLET_2, + creator_wallet: CREATOR_WALLET_2, + key_amount: 2, + amount: 2, + price_at_trade: '1200000', + }, + createdAt: T2, + }, }); - mockPrisma.activity.count.mockImplementation(async () => { - return SEEDED_TRANSACTIONS.length; + await prisma.activity.create({ + data: { + type: 'KEY_BOUGHT', + actor: ACTOR_WALLET_3, + creatorId: creatorId3, + payload: { + action: 'buy', + wallet: ACTOR_WALLET_3, + creator_wallet: CREATOR_WALLET_3, + key_amount: 3, + amount: 3, + price_at_trade: '1500000', + }, + createdAt: T3, + }, + }); + + await prisma.activity.create({ + data: { + type: 'KEY_SOLD', + actor: ACTOR_WALLET_1, + creatorId: creatorId2, + payload: { + action: 'sell', + wallet: ACTOR_WALLET_1, + creator_wallet: CREATOR_WALLET_2, + key_amount: 4, + amount: 4, + price_at_trade: '1800000', + }, + createdAt: T4, + }, + }); + + await prisma.activity.create({ + data: { + type: 'KEY_BOUGHT', + actor: ACTOR_WALLET_2, + creatorId: creatorId1, + payload: { + action: 'buy', + wallet: ACTOR_WALLET_2, + creator_wallet: CREATOR_WALLET_1, + key_amount: 5, + amount: 5, + price_at_trade: '2000000', + }, + createdAt: T5, + }, + }); + + await prisma.activity.create({ + data: { + type: 'KEY_SOLD', + actor: ACTOR_WALLET_3, + creatorId: creatorId3, + payload: { + action: 'sell', + wallet: ACTOR_WALLET_3, + creator_wallet: CREATOR_WALLET_3, + key_amount: 6, + amount: 6, + price_at_trade: '2200000', + }, + createdAt: T6, + }, + }); + + await prisma.activity.create({ + data: { + type: 'KEY_BOUGHT', + actor: ACTOR_WALLET_1, + creatorId: creatorId3, + payload: { + action: 'buy', + wallet: ACTOR_WALLET_1, + creator_wallet: CREATOR_WALLET_3, + key_amount: 7, + amount: 7, + price_at_trade: '2500000', + }, + createdAt: T7, + }, }); }); + afterAll(async () => { + await prisma.activity.deleteMany({ + where: { + creatorId: { in: [creatorId1, creatorId2, creatorId3] }, + }, + }); + await prisma.creatorProfile.deleteMany({ + where: { id: { in: [creatorId1, creatorId2, creatorId3] } }, + }); + await prisma.user.delete({ where: { id: TEST_USER_ID } }); + await prisma.$disconnect(); + }); + it('accessible without authentication', async () => { const res = await supertest(app).get('/api/v1/activity?limit=5'); @@ -194,11 +235,7 @@ describe('GET /api/v1/activity — global activity feed five most recent transac const items = res.body.data.items; expect(items).toHaveLength(5); - // Expected 5 most recent IDs in descending order: tx-7, tx-6, tx-5, tx-4, tx-3 - const returnedIds = items.map((item: any) => item.id); - expect(returnedIds).toEqual(['tx-7', 'tx-6', 'tx-5', 'tx-4', 'tx-3']); - - // Assert timestamps are strictly descending + // Assert timestamps are strictly descending (newest first) for (let i = 0; i < items.length - 1; i++) { const current = new Date( items[i].created_at || items[i].createdAt @@ -209,9 +246,17 @@ describe('GET /api/v1/activity — global activity feed five most recent transac expect(current).toBeGreaterThan(next); } - // Assert oldest two transactions (tx-1 and tx-2) are excluded - expect(returnedIds).not.toContain('tx-1'); - expect(returnedIds).not.toContain('tx-2'); + // Verify the timestamps match the expected 5 most recent (T7, T6, T5, T4, T3) + const timestamps = items.map((item: any) => + new Date(item.created_at || item.createdAt).toISOString() + ); + expect(timestamps).toEqual([ + T7.toISOString(), + T6.toISOString(), + T5.toISOString(), + T4.toISOString(), + T3.toISOString(), + ]); }); it('includes required fields (wallet, action, creator_wallet, key_amount, created_at) in each entry', async () => { @@ -245,8 +290,11 @@ describe('GET /api/v1/activity — global activity feed five most recent transac }); it('returns an empty array when no transactions exist', async () => { - mockPrisma.activity.findMany.mockResolvedValue([]); - mockPrisma.activity.count.mockResolvedValue(0); + await prisma.activity.deleteMany({ + where: { + creatorId: { in: [creatorId1, creatorId2, creatorId3] }, + }, + }); const res = await supertest(app).get('/api/v1/activity?limit=5');