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
2 changes: 2 additions & 0 deletions jest.setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,5 @@ process.env.CLOUDINARY_API_KEY ??= 'test-api-key';
process.env.CLOUDINARY_API_SECRET ??= 'test-api-secret';
process.env.PAYSTACK_SECRET_KEY ??= 'test-paystack-secret';
process.env.APP_SECRET ??= 'accesslayer_test_secret_key_32_bytes_long_xxxx';
process.env.DB_QUERY_TIMEOUT_MS = '30000';
jest.setTimeout(30000);
168 changes: 168 additions & 0 deletions src/modules/creators/creator-list-price-filter.integration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// src/modules/creators/creator-list-price-filter.integration.test.ts
// Integration tests for #419 — min_price and max_price filtering.

import supertest from 'supertest';
import app from '../../app';
import { prisma } from '../../utils/prisma.utils';

const USER_IDS = ['price-filter-user-1', 'price-filter-user-2', 'price-filter-user-3'];
const HANDLES = ['price-filter-creator-1', 'price-filter-creator-2', 'price-filter-creator-3'];

describe('#419 min_price and max_price filtering', () => {
let creatorIds: string[];

beforeAll(async () => {
creatorIds = [];

for (let i = 0; i < 3; i++) {
await prisma.user.upsert({
where: { id: USER_IDS[i] },
create: {
id: USER_IDS[i],
email: `price-filter-${i}@example.test`,
passwordHash: 'dummy-hash',
firstName: 'Price',
lastName: `Filter ${i}`,
},
update: {},
});

const creator = await prisma.creatorProfile.upsert({
where: { userId: USER_IDS[i] },
create: {
userId: USER_IDS[i],
handle: HANDLES[i],
displayName: `Creator ${i}`,
},
update: {},
});

creatorIds.push(creator.id);
}

// Seed price snapshots: 1M, 3M, 5M stroops
const prices = [1_000_000n, 3_000_000n, 5_000_000n];
for (let i = 0; i < 3; i++) {
await prisma.creatorPriceSnapshot.upsert({
where: { creatorId: creatorIds[i] },
create: {
creatorId: creatorIds[i],
currentPrice: prices[i],
price24hAgo: prices[i],
lastTradeAt: new Date(),
},
update: {
currentPrice: prices[i],
price24hAgo: prices[i],
lastTradeAt: new Date(),
},
});
}
});

afterAll(async () => {
await prisma.creatorPriceSnapshot.deleteMany({
where: { creatorId: { in: creatorIds } },
});
await prisma.creatorProfile.deleteMany({
where: { handle: { in: HANDLES } },
});
await prisma.user.deleteMany({
where: { id: { in: USER_IDS } },
});
await prisma.$disconnect();
});

it('minPrice alone filters out creators below the value', async () => {
const res = await supertest(app).get('/api/v1/creators?minPrice=2000000');
expect(res.status).toBe(200);

const ids = (res.body.data.items as any[])
.filter((c: any) => creatorIds.includes(c.id))
.map((c: any) => c.id);

// Only creators with price >= 2M (creators 1 and 2)
expect(ids).toContain(creatorIds[1]); // 3M
expect(ids).toContain(creatorIds[2]); // 5M
expect(ids).not.toContain(creatorIds[0]); // 1M
});

it('maxPrice alone filters out creators above the value', async () => {
const res = await supertest(app).get('/api/v1/creators?maxPrice=4000000');
expect(res.status).toBe(200);

const ids = (res.body.data.items as any[])
.filter((c: any) => creatorIds.includes(c.id))
.map((c: any) => c.id);

// Only creators with price <= 4M (creators 0 and 1)
expect(ids).toContain(creatorIds[0]); // 1M
expect(ids).toContain(creatorIds[1]); // 3M
expect(ids).not.toContain(creatorIds[2]); // 5M
});

it('both params together return only creators within range (inclusive)', async () => {
const res = await supertest(app).get(
'/api/v1/creators?minPrice=2000000&maxPrice=4000000'
);
expect(res.status).toBe(200);

const ids = (res.body.data.items as any[])
.filter((c: any) => creatorIds.includes(c.id))
.map((c: any) => c.id);

// Only creator 1 (3M) is in range [2M, 4M]
expect(ids).toContain(creatorIds[1]);
expect(ids).not.toContain(creatorIds[0]);
expect(ids).not.toContain(creatorIds[2]);
});

it('returns 400 when minPrice > maxPrice', async () => {
const res = await supertest(app).get(
'/api/v1/creators?minPrice=5000000&maxPrice=1000000'
);
expect(res.status).toBe(400);
expect(res.body.success).toBe(false);
expect(res.body.error.code).toBe('VALIDATION_ERROR');
expect(res.body.error.message).toBe('Invalid query parameters');
expect(res.body.error.details[0].field).toBe('minPrice');
});

it('combines correctly with sort and pagination', async () => {
const res = await supertest(app).get(
'/api/v1/creators?minPrice=1000000&maxPrice=5000000&limit=10&offset=0&sort=createdAt&order=desc'
);
expect(res.status).toBe(200);
expect(res.body.success).toBe(true);
expect(res.body.data.meta.limit).toBe(10);
expect(res.body.data.meta.offset).toBe(0);
});

it('combines correctly with verified filter', async () => {
// Mark creator 1 as verified
await prisma.creatorProfile.update({
where: { id: creatorIds[1] },
data: { isVerified: true },
});

const res = await supertest(app).get(
'/api/v1/creators?minPrice=1000000&maxPrice=5000000&verified=true'
);
expect(res.status).toBe(200);

const ids = (res.body.data.items as any[])
.filter((c: any) => creatorIds.includes(c.id))
.map((c: any) => c.id);

// Only verified creator 1 within price range
expect(ids).toContain(creatorIds[1]);
expect(ids).not.toContain(creatorIds[0]);
expect(ids).not.toContain(creatorIds[2]);

// Cleanup
await prisma.creatorProfile.update({
where: { id: creatorIds[1] },
data: { isVerified: false },
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// src/modules/creators/creator-list-price-filtered-total.integration.test.ts

import supertest from 'supertest';
import app from '../../app';
import { prisma } from '../../utils/prisma.utils';

const USER_IDS = [
'filtered-total-user-1',
'filtered-total-user-2',
'filtered-total-user-3',
'filtered-total-user-4',
'filtered-total-user-5',
];

const HANDLES = [
'filtered-total-creator-1',
'filtered-total-creator-2',
'filtered-total-creator-3',
'filtered-total-creator-4',
'filtered-total-creator-5',
];

describe('GET /api/v1/creators — filtered total count with price range', () => {
let creatorIds: string[] = [];

beforeAll(async () => {
// Ensure database is completely clean of any conflicting data
await prisma.keyOwnership.deleteMany({});
await prisma.creatorPriceSnapshot.deleteMany({});
await prisma.creatorProfile.deleteMany({});
await prisma.user.deleteMany({});

creatorIds = [];

// Seed exactly 5 users and creators
for (let i = 0; i < 5; i++) {
await prisma.user.create({
data: {
id: USER_IDS[i],
email: `filtered-total-${i}@example.test`,
passwordHash: 'dummy-hash',
firstName: 'Filtered',
lastName: `Total ${i}`,
},
});

const creator = await prisma.creatorProfile.create({
data: {
userId: USER_IDS[i],
handle: HANDLES[i],
displayName: `Creator ${i}`,
},
});

creatorIds.push(creator.id);
}

// Seed exactly 5 creators with varied prices: 1M, 2M, 3M, 4M, 5M stroops
const prices = [1_000_000n, 2_000_000n, 3_000_000n, 4_000_000n, 5_000_000n];
for (let i = 0; i < 5; i++) {
await prisma.creatorPriceSnapshot.create({
data: {
creatorId: creatorIds[i],
currentPrice: prices[i],
price24hAgo: prices[i],
lastTradeAt: new Date(),
},
});
}
});

afterAll(async () => {
// Teardown
await prisma.creatorPriceSnapshot.deleteMany({
where: { creatorId: { in: creatorIds } },
});
await prisma.creatorProfile.deleteMany({
where: { id: { in: creatorIds } },
});
await prisma.user.deleteMany({
where: { id: { in: USER_IDS } },
});
await prisma.$disconnect();
});

it('should verify that meta.total reflects the filtered creator count and matches the response data length when a price range is applied', async () => {
// Apply a price range filter matching exactly three creators: [2M, 4M] (prices 2M, 3M, 4M)
const res = await supertest(app).get('/api/v1/creators?minPrice=2000000&maxPrice=4000000');
expect(res.status).toBe(200);

const response = {
data: res.body.data.items,
meta: res.body.data.meta,
};
const { meta } = response;

// Assert requirements:
// - meta.total === 3
// - response.data.length === 3
// - response.data.length === response.meta.total
expect(meta.total).toBe(3);
expect(response.data.length).toBe(3);
expect(response.data.length).toBe(response.meta.total);

// Verify that meta.total is the filtered count, not the total creator count in the database (5)
expect(meta.total).not.toBe(5);
});
});
2 changes: 2 additions & 0 deletions src/modules/creators/creators-cache-key.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export function buildCreatorFeedCacheKey(query: CreatorListQueryType): string {
query.include !== undefined && query.include.length > 0
? query.include.join(',')
: undefined,
minPrice: query.minPrice !== undefined ? query.minPrice.toString() : undefined,
maxPrice: query.maxPrice !== undefined ? query.maxPrice.toString() : undefined,
};

const canonical = buildCanonicalParamString(params);
Expand Down
2 changes: 1 addition & 1 deletion src/modules/creators/creators.sort.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@ export function mapCreatorListSort(
}

return {
[field]: { sort: order, nulls: 'last' },
[field]: order,
} as Prisma.CreatorProfileOrderByWithRelationInput;
}
Loading
Loading