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
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Integration test: creator list search no-results state
//
// Verifies that a search query returning zero creators produces a response
// with a searchTerm field in the pagination metadata, distinguishing it
// from the generic unfiltered empty state.

import { httpListCreators } from './creators.controllers';
import * as creatorsUtils from './creators.utils';

function makeReq(query: Record<string, string> = {}): any {
return { query };
}

function makeRes(): any {
const res: any = {};
res.status = jest.fn().mockReturnValue(res);
res.json = jest.fn().mockReturnValue(res);
res.setHeader = jest.fn().mockReturnValue(res);
res.set = jest.fn().mockReturnValue(res);
return res;
}

function makeNext(): jest.Mock {
return jest.fn();
}

describe('GET /api/v1/creators — search no-results state', () => {
beforeEach(() => {
jest.spyOn(creatorsUtils, 'fetchCreatorList').mockResolvedValue([[], 0]);
});

afterEach(() => {
jest.restoreAllMocks();
});

it('includes searchTerm in meta when search returns zero results', async () => {
const req = makeReq({ search: 'zzznomatch' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.meta).toHaveProperty('searchTerm', 'zzznomatch');
});

it('does not include searchTerm in meta for unfiltered empty list', async () => {
const req = makeReq();
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.meta).not.toHaveProperty('searchTerm');
});

it('does not include searchTerm when search is cleared (empty string)', async () => {
const req = makeReq({ search: '' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.meta).not.toHaveProperty('searchTerm');
});

it('does not include searchTerm when search is whitespace-only', async () => {
const req = makeReq({ search: ' ' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.meta).not.toHaveProperty('searchTerm');
});

it('does not include searchTerm when search returns results (non-zero total)', async () => {
const now = new Date();
jest
.spyOn(creatorsUtils, 'fetchCreatorList')
.mockResolvedValue([
[
{
id: '1',
userId: 'u1',
handle: 'found1',
displayName: 'Found One',
isVerified: false,
createdAt: now,
updatedAt: now,
} as any,
{
id: '2',
userId: 'u2',
handle: 'found2',
displayName: 'Found Two',
isVerified: true,
createdAt: now,
updatedAt: now,
} as any,
],
2,
]);

const req = makeReq({ search: 'found' });
const res = makeRes();
await httpListCreators(req, res, makeNext());

const body = res.json.mock.calls[0][0];
expect(body.data.meta).not.toHaveProperty('searchTerm');
});

it('no-results search state is distinct from unfiltered empty state', async () => {
const searchReq = makeReq({ search: 'zzznomatch' });
const searchRes = makeRes();
await httpListCreators(searchReq, searchRes, makeNext());
const searchBody = searchRes.json.mock.calls[0][0];

const emptyReq = makeReq();
const emptyRes = makeRes();
await httpListCreators(emptyReq, emptyRes, makeNext());
const emptyBody = emptyRes.json.mock.calls[0][0];

expect(searchBody.data.meta).toHaveProperty('searchTerm', 'zzznomatch');
expect(emptyBody.data.meta).not.toHaveProperty('searchTerm');
});
});
5 changes: 4 additions & 1 deletion src/modules/creators/creators.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export const httpListCreators: AsyncController = async (req, res, next) => {
limit: validatedQuery.limit,
offset: validatedQuery.offset,
total,
...(validatedQuery.search !== undefined && total === 0
? { searchTerm: validatedQuery.search }
: {}),
})
);

Expand Down Expand Up @@ -380,7 +383,7 @@ export const httpGetTrendingCreators: AsyncController = async (
);

// Sort by volume descending
creatorsWithVolume.sort((a, b) => {
creatorsWithVolume.sort((a: { volume_24h: string }, b: { volume_24h: string }) => {
const volA = BigInt(a.volume_24h);
const volB = BigInt(b.volume_24h);
if (volB > volA) return 1;
Expand Down
1 change: 1 addition & 0 deletions src/modules/creators/creators.serializers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ export function serializeCreatorListOffsetMeta(
offset: meta.offset,
total: meta.total,
hasMore: meta.hasMore,
...(meta.searchTerm !== undefined ? { searchTerm: meta.searchTerm } : {}),
};
}

Expand Down
4 changes: 4 additions & 0 deletions src/utils/pagination.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ export type OffsetPaginationMeta = {
offset: number;
total: number;
hasMore: boolean;
searchTerm?: string;
};

export type OffsetPaginationMetaParams = {
limit: number;
offset: number;
total: number;
searchTerm?: string;
};

export const buildPaginationMeta = ({
Expand Down Expand Up @@ -53,6 +55,7 @@ export const buildOffsetPaginationMeta = ({
limit,
offset,
total,
searchTerm,
}: OffsetPaginationMetaParams): OffsetPaginationMeta => {
const safeLimit = Math.max(1, Math.floor(limit));
const safeOffset = Math.max(0, Math.floor(offset));
Expand All @@ -63,6 +66,7 @@ export const buildOffsetPaginationMeta = ({
offset: safeOffset,
total: safeTotal,
hasMore: safeOffset + safeLimit < safeTotal,
...(searchTerm !== undefined ? { searchTerm } : {}),
};
};

Expand Down
Loading