diff --git a/src/modules/creators/creator-list-search-no-results.integration.test.ts b/src/modules/creators/creator-list-search-no-results.integration.test.ts new file mode 100644 index 0000000..6e2057d --- /dev/null +++ b/src/modules/creators/creator-list-search-no-results.integration.test.ts @@ -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 = {}): 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'); + }); +}); diff --git a/src/modules/creators/creators.controllers.ts b/src/modules/creators/creators.controllers.ts index 23508a1..febe102 100644 --- a/src/modules/creators/creators.controllers.ts +++ b/src/modules/creators/creators.controllers.ts @@ -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 } + : {}), }) ); @@ -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; diff --git a/src/modules/creators/creators.serializers.ts b/src/modules/creators/creators.serializers.ts index e24419e..e3cc3ba 100644 --- a/src/modules/creators/creators.serializers.ts +++ b/src/modules/creators/creators.serializers.ts @@ -161,6 +161,7 @@ export function serializeCreatorListOffsetMeta( offset: meta.offset, total: meta.total, hasMore: meta.hasMore, + ...(meta.searchTerm !== undefined ? { searchTerm: meta.searchTerm } : {}), }; } diff --git a/src/utils/pagination.utils.ts b/src/utils/pagination.utils.ts index f959c8e..90b1e5d 100644 --- a/src/utils/pagination.utils.ts +++ b/src/utils/pagination.utils.ts @@ -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 = ({ @@ -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)); @@ -63,6 +66,7 @@ export const buildOffsetPaginationMeta = ({ offset: safeOffset, total: safeTotal, hasMore: safeOffset + safeLimit < safeTotal, + ...(searchTerm !== undefined ? { searchTerm } : {}), }; };