diff --git a/src/modules/creator/creator.service.test.ts b/src/modules/creator/creator.service.test.ts index d4c1a61..f128179 100644 --- a/src/modules/creator/creator.service.test.ts +++ b/src/modules/creator/creator.service.test.ts @@ -1,5 +1,6 @@ import { getPaginatedCreators } from './creator.service'; import { prisma } from '../../utils/prisma.utils'; +import { createSeededCreatorFixture } from '../../utils/test/seeded-creator-fixtures.utils'; import { CreatorSortOptions } from './creator.utils'; import { CREATOR_LIST_DEFAULT_SELECT } from '../../constants/creator-list-projection.constants'; @@ -17,17 +18,6 @@ const count = prisma.creatorProfile.count as jest.Mock; const baseSort: CreatorSortOptions = { field: 'createdAt', order: 'desc' }; -function makeCreator(overrides: Record = {}) { - return { - id: 'creator-1', - handle: 'alice', - displayName: 'Alice', - avatarUrl: null, - isVerified: false, - ...overrides, - }; -} - describe('getPaginatedCreators', () => { beforeEach(() => { findMany.mockReset(); @@ -52,8 +42,8 @@ describe('getPaginatedCreators', () => { it('returns the resolved creators and the matching pagination metadata', async () => { const creators = [ - makeCreator(), - makeCreator({ id: 'creator-2', handle: 'bob' }), + createSeededCreatorFixture(1), + createSeededCreatorFixture(2), ]; findMany.mockResolvedValue(creators); count.mockResolvedValue(35); @@ -76,7 +66,7 @@ describe('getPaginatedCreators', () => { }); it('flags hasNextPage=false when on the last page', async () => { - findMany.mockResolvedValue([makeCreator()]); + findMany.mockResolvedValue([createSeededCreatorFixture(1)]); count.mockResolvedValue(15); const result = await getPaginatedCreators({ @@ -91,7 +81,7 @@ describe('getPaginatedCreators', () => { }); it('flags hasPrevPage=false when on the first page', async () => { - findMany.mockResolvedValue([makeCreator()]); + findMany.mockResolvedValue([createSeededCreatorFixture(1)]); count.mockResolvedValue(15); const result = await getPaginatedCreators({ @@ -146,7 +136,7 @@ describe('getPaginatedCreators', () => { }); expect(findMany).toHaveBeenCalledWith( - expect.objectContaining({ + expect.objectContaining({ orderBy: { displayName: 'asc' }, select: CREATOR_LIST_DEFAULT_SELECT, }) diff --git a/src/modules/creators/creator-list-item.mapper.test.ts b/src/modules/creators/creator-list-item.mapper.test.ts index 9d11b5c..66bf37f 100644 --- a/src/modules/creators/creator-list-item.mapper.test.ts +++ b/src/modules/creators/creator-list-item.mapper.test.ts @@ -1,147 +1,17 @@ import { mapCreatorListItem } from './creator-list-item.mapper'; -import { requestContextStorage } from '../../utils/als.utils'; -import { logger } from '../../utils/logger.utils'; - -jest.mock('../../utils/logger.utils', () => ({ - logger: { warn: jest.fn(), error: jest.fn() }, -})); - -const warnMock = logger.warn as jest.Mock; -const errorMock = logger.error as jest.Mock; - -beforeEach(() => { - warnMock.mockClear(); - errorMock.mockClear(); -}); +import { createSeededCreatorFixture } from '../../utils/test/seeded-creator-fixtures.utils'; describe('mapCreatorListItem()', () => { it('maps the public creator list item shape', () => { - const input = { - id: '1', - displayName: 'John', - avatarUrl: null, - createdAt: new Date('2024-01-02T03:04:05.678Z'), - updatedAt: new Date('2024-01-03T03:04:05.678Z'), - } as any; + const input = createSeededCreatorFixture(1); const result = mapCreatorListItem(input); expect(result).toEqual({ - id: '1', - name: 'John', - avatar: null, - followers: 0, - createdAt: '2024-01-02T03:04:05.678Z', - updatedAt: '2024-01-03T03:04:05.678Z', - }); - expect(warnMock).not.toHaveBeenCalled(); - }); - - it('warns when a schema-required creator field is unexpectedly null', () => { - const input = { id: 'creator-1', - displayName: null, - avatarUrl: null, - createdAt: new Date('2024-01-02T03:04:05.678Z'), - updatedAt: new Date('2024-01-03T03:04:05.678Z'), - } as any; - - const result = requestContextStorage.run( - { path: '/api/v1/creators', method: 'GET', requestId: 'req-333' }, - () => mapCreatorListItem(input) - ); - - expect(result).toEqual({ - id: 'creator-1', - name: null, - avatar: null, + name: 'Creator 1', + avatar: 'https://example.com/avatar-1.png', followers: 0, - createdAt: '2024-01-02T03:04:05.678Z', - updatedAt: '2024-01-03T03:04:05.678Z', }); - expect(warnMock).toHaveBeenCalledWith({ - msg: 'Unexpected null creator field in database result', - fieldName: 'displayName', - creatorId: 'creator-1', - requestId: 'req-333', - }); - }); - - it('logs an error when a string field receives an unexpected type', () => { - const input = { - id: 'creator-2', - handle: 'test-handle', - displayName: 12345, // number instead of string - avatarUrl: null, - isVerified: false, - createdAt: new Date('2024-01-02T03:04:05.678Z'), - updatedAt: new Date('2024-01-03T03:04:05.678Z'), - } as any; - - const result = requestContextStorage.run( - { path: '/api/v1/creators', method: 'GET', requestId: 'req-type-1' }, - () => mapCreatorListItem(input) - ); - - expect(result).toEqual({ - id: 'creator-2', - name: 12345, - avatar: null, - followers: 0, - createdAt: '2024-01-02T03:04:05.678Z', - updatedAt: '2024-01-03T03:04:05.678Z', - }); - expect(errorMock).toHaveBeenCalledWith({ - msg: 'Creator list field type mismatch', - fieldName: 'displayName', - expectedType: 'string', - receivedType: 'number', - creatorId: 'creator-2', - requestId: 'req-type-1', - }); - }); - - it('logs an error when a Date field receives a string', () => { - const input = { - id: 'creator-3', - handle: 'test-handle', - displayName: 'Alice', - avatarUrl: null, - isVerified: true, - createdAt: '2024-01-02T03:04:05.678Z', // string instead of Date - updatedAt: new Date('2024-01-03T03:04:05.678Z'), - } as any; - - requestContextStorage.run( - { path: '/api/v1/creators', method: 'GET', requestId: 'req-type-2' }, - () => mapCreatorListItem(input) - ); - - expect(errorMock).toHaveBeenCalledWith( - expect.objectContaining({ - msg: 'Creator list field type mismatch', - fieldName: 'createdAt', - expectedType: 'Date', - receivedType: 'string', - creatorId: 'creator-3', - requestId: 'req-type-2', - }) - ); - }); - - it('does not log an error for correctly typed fields', () => { - const input = { - id: 'creator-4', - handle: 'good-handle', - displayName: 'Bob', - avatarUrl: 'https://example.com/avatar.png', - isVerified: false, - createdAt: new Date('2024-01-02T03:04:05.678Z'), - updatedAt: new Date('2024-01-03T03:04:05.678Z'), - } as any; - - mapCreatorListItem(input); - - expect(errorMock).not.toHaveBeenCalled(); }); }); diff --git a/src/utils/test/seeded-creator-fixtures.utils.test.ts b/src/utils/test/seeded-creator-fixtures.utils.test.ts new file mode 100644 index 0000000..d31bb66 --- /dev/null +++ b/src/utils/test/seeded-creator-fixtures.utils.test.ts @@ -0,0 +1,45 @@ +import { createSeededCreatorFixture } from './seeded-creator-fixtures.utils'; + +describe('createSeededCreatorFixture', () => { + it('produces deterministic records for the same seed', () => { + const first = createSeededCreatorFixture(5); + const second = createSeededCreatorFixture(5); + + expect(first).toEqual(second); + }); + + it('produces distinct records for different seeds', () => { + const seedOne = createSeededCreatorFixture(1); + const seedTwo = createSeededCreatorFixture(2); + + expect(seedOne).not.toEqual(seedTwo); + expect(seedOne.id).toBe('creator-1'); + expect(seedTwo.id).toBe('creator-2'); + }); + + it('derives stable field values from the seed', () => { + const fixture = createSeededCreatorFixture(3); + + expect(fixture).toMatchObject({ + id: 'creator-3', + userId: 'user-3', + handle: 'creator-3', + displayName: 'Creator 3', + avatarUrl: 'https://example.com/avatar-3.png', + bio: 'Bio for creator 3', + perkSummary: 'Perks for creator 3', + isVerified: false, + }); + expect(fixture.updatedAt.getTime()).toBe( + fixture.createdAt.getTime() + 1000 + ); + }); + + it('respects overridden createdAt when deriving updatedAt', () => { + const customDate = new Date('2025-01-01T00:00:00.000Z'); + const fixture = createSeededCreatorFixture(4, { createdAt: customDate }); + + expect(fixture.createdAt).toEqual(customDate); + expect(fixture.updatedAt.getTime()).toBe(customDate.getTime() + 1000); + }); +}); diff --git a/src/utils/test/seeded-creator-fixtures.utils.ts b/src/utils/test/seeded-creator-fixtures.utils.ts new file mode 100644 index 0000000..b7f8c67 --- /dev/null +++ b/src/utils/test/seeded-creator-fixtures.utils.ts @@ -0,0 +1,48 @@ +import { CreatorProfile } from '../../types/profile.types'; + +const CREATOR_FIXTURE_BASE_DATE = new Date(Date.UTC(2020, 0, 1)); + +/** + * Generates a deterministic creator record from a numeric seed. + * + * Field mapping: + * - id: `creator-${seed}` + * - userId: `user-${seed}` + * - handle: `creator-${seed}` + * - displayName: `Creator ${seed}` + * - avatarUrl: `https://example.com/avatar-${seed}.png` + * - bio: `Bio for creator ${seed}` + * - perkSummary: `Perks for creator ${seed}` + * - perks: [] + * - isVerified: true for even seeds, false for odd seeds + * - createdAt: 2020-01-01 UTC plus `seed` days + * - updatedAt: `createdAt` plus 1 second + * + * This helper is intentionally stable: the same seed always returns the same object, + * and different seeds produce distinct creator values. + */ +export function createSeededCreatorFixture( + seed: number, + overrides: Partial = {} +): CreatorProfile { + const normalizedSeed = Math.max(0, Math.floor(seed)); + const createdAt = new Date(CREATOR_FIXTURE_BASE_DATE); + createdAt.setUTCDate(createdAt.getUTCDate() + normalizedSeed); + const finalCreatedAt = overrides.createdAt ?? createdAt; + + return { + id: `creator-${normalizedSeed}`, + userId: `user-${normalizedSeed}`, + handle: `creator-${normalizedSeed}`, + displayName: `Creator ${normalizedSeed}`, + bio: `Bio for creator ${normalizedSeed}`, + avatarUrl: `https://example.com/avatar-${normalizedSeed}.png`, + perkSummary: `Perks for creator ${normalizedSeed}`, + perks: [], + isVerified: normalizedSeed % 2 === 0, + createdAt: finalCreatedAt, + updatedAt: + overrides.updatedAt ?? new Date(finalCreatedAt.getTime() + 1000), + ...overrides, + }; +}