Skip to content

Commit 7968276

Browse files
authored
Merge pull request #406 from shadrach68/databaseFixtures
Database fixtures
2 parents a42cbb1 + c9f2138 commit 7968276

4 files changed

Lines changed: 103 additions & 150 deletions

File tree

‎src/modules/creator/creator.service.test.ts‎

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getPaginatedCreators } from './creator.service';
22
import { prisma } from '../../utils/prisma.utils';
3+
import { createSeededCreatorFixture } from '../../utils/test/seeded-creator-fixtures.utils';
34
import { CreatorSortOptions } from './creator.utils';
45
import { CREATOR_LIST_DEFAULT_SELECT } from '../../constants/creator-list-projection.constants';
56

@@ -17,17 +18,6 @@ const count = prisma.creatorProfile.count as jest.Mock;
1718

1819
const baseSort: CreatorSortOptions = { field: 'createdAt', order: 'desc' };
1920

20-
function makeCreator(overrides: Record<string, unknown> = {}) {
21-
return {
22-
id: 'creator-1',
23-
handle: 'alice',
24-
displayName: 'Alice',
25-
avatarUrl: null,
26-
isVerified: false,
27-
...overrides,
28-
};
29-
}
30-
3121
describe('getPaginatedCreators', () => {
3222
beforeEach(() => {
3323
findMany.mockReset();
@@ -52,8 +42,8 @@ describe('getPaginatedCreators', () => {
5242

5343
it('returns the resolved creators and the matching pagination metadata', async () => {
5444
const creators = [
55-
makeCreator(),
56-
makeCreator({ id: 'creator-2', handle: 'bob' }),
45+
createSeededCreatorFixture(1),
46+
createSeededCreatorFixture(2),
5747
];
5848
findMany.mockResolvedValue(creators);
5949
count.mockResolvedValue(35);
@@ -76,7 +66,7 @@ describe('getPaginatedCreators', () => {
7666
});
7767

7868
it('flags hasNextPage=false when on the last page', async () => {
79-
findMany.mockResolvedValue([makeCreator()]);
69+
findMany.mockResolvedValue([createSeededCreatorFixture(1)]);
8070
count.mockResolvedValue(15);
8171

8272
const result = await getPaginatedCreators({
@@ -91,7 +81,7 @@ describe('getPaginatedCreators', () => {
9181
});
9282

9383
it('flags hasPrevPage=false when on the first page', async () => {
94-
findMany.mockResolvedValue([makeCreator()]);
84+
findMany.mockResolvedValue([createSeededCreatorFixture(1)]);
9585
count.mockResolvedValue(15);
9686

9787
const result = await getPaginatedCreators({
@@ -146,7 +136,7 @@ describe('getPaginatedCreators', () => {
146136
});
147137

148138
expect(findMany).toHaveBeenCalledWith(
149-
expect.objectContaining({
139+
expect.objectContaining({
150140
orderBy: { displayName: 'asc' },
151141
select: CREATOR_LIST_DEFAULT_SELECT,
152142
})
Lines changed: 4 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,147 +1,17 @@
11
import { mapCreatorListItem } from './creator-list-item.mapper';
2-
import { requestContextStorage } from '../../utils/als.utils';
3-
import { logger } from '../../utils/logger.utils';
4-
5-
jest.mock('../../utils/logger.utils', () => ({
6-
logger: { warn: jest.fn(), error: jest.fn() },
7-
}));
8-
9-
const warnMock = logger.warn as jest.Mock;
10-
const errorMock = logger.error as jest.Mock;
11-
12-
beforeEach(() => {
13-
warnMock.mockClear();
14-
errorMock.mockClear();
15-
});
2+
import { createSeededCreatorFixture } from '../../utils/test/seeded-creator-fixtures.utils';
163

174
describe('mapCreatorListItem()', () => {
185
it('maps the public creator list item shape', () => {
19-
const input = {
20-
id: '1',
21-
displayName: 'John',
22-
avatarUrl: null,
23-
createdAt: new Date('2024-01-02T03:04:05.678Z'),
24-
updatedAt: new Date('2024-01-03T03:04:05.678Z'),
25-
} as any;
6+
const input = createSeededCreatorFixture(1);
267

278
const result = mapCreatorListItem(input);
289

2910
expect(result).toEqual({
30-
id: '1',
31-
name: 'John',
32-
avatar: null,
33-
followers: 0,
34-
createdAt: '2024-01-02T03:04:05.678Z',
35-
updatedAt: '2024-01-03T03:04:05.678Z',
36-
});
37-
expect(warnMock).not.toHaveBeenCalled();
38-
});
39-
40-
it('warns when a schema-required creator field is unexpectedly null', () => {
41-
const input = {
4211
id: 'creator-1',
43-
displayName: null,
44-
avatarUrl: null,
45-
createdAt: new Date('2024-01-02T03:04:05.678Z'),
46-
updatedAt: new Date('2024-01-03T03:04:05.678Z'),
47-
} as any;
48-
49-
const result = requestContextStorage.run(
50-
{ path: '/api/v1/creators', method: 'GET', requestId: 'req-333' },
51-
() => mapCreatorListItem(input)
52-
);
53-
54-
expect(result).toEqual({
55-
id: 'creator-1',
56-
name: null,
57-
avatar: null,
12+
name: 'Creator 1',
13+
avatar: 'https://example.com/avatar-1.png',
5814
followers: 0,
59-
createdAt: '2024-01-02T03:04:05.678Z',
60-
updatedAt: '2024-01-03T03:04:05.678Z',
6115
});
62-
expect(warnMock).toHaveBeenCalledWith({
63-
msg: 'Unexpected null creator field in database result',
64-
fieldName: 'displayName',
65-
creatorId: 'creator-1',
66-
requestId: 'req-333',
67-
});
68-
});
69-
70-
it('logs an error when a string field receives an unexpected type', () => {
71-
const input = {
72-
id: 'creator-2',
73-
handle: 'test-handle',
74-
displayName: 12345, // number instead of string
75-
avatarUrl: null,
76-
isVerified: false,
77-
createdAt: new Date('2024-01-02T03:04:05.678Z'),
78-
updatedAt: new Date('2024-01-03T03:04:05.678Z'),
79-
} as any;
80-
81-
const result = requestContextStorage.run(
82-
{ path: '/api/v1/creators', method: 'GET', requestId: 'req-type-1' },
83-
() => mapCreatorListItem(input)
84-
);
85-
86-
expect(result).toEqual({
87-
id: 'creator-2',
88-
name: 12345,
89-
avatar: null,
90-
followers: 0,
91-
createdAt: '2024-01-02T03:04:05.678Z',
92-
updatedAt: '2024-01-03T03:04:05.678Z',
93-
});
94-
expect(errorMock).toHaveBeenCalledWith({
95-
msg: 'Creator list field type mismatch',
96-
fieldName: 'displayName',
97-
expectedType: 'string',
98-
receivedType: 'number',
99-
creatorId: 'creator-2',
100-
requestId: 'req-type-1',
101-
});
102-
});
103-
104-
it('logs an error when a Date field receives a string', () => {
105-
const input = {
106-
id: 'creator-3',
107-
handle: 'test-handle',
108-
displayName: 'Alice',
109-
avatarUrl: null,
110-
isVerified: true,
111-
createdAt: '2024-01-02T03:04:05.678Z', // string instead of Date
112-
updatedAt: new Date('2024-01-03T03:04:05.678Z'),
113-
} as any;
114-
115-
requestContextStorage.run(
116-
{ path: '/api/v1/creators', method: 'GET', requestId: 'req-type-2' },
117-
() => mapCreatorListItem(input)
118-
);
119-
120-
expect(errorMock).toHaveBeenCalledWith(
121-
expect.objectContaining({
122-
msg: 'Creator list field type mismatch',
123-
fieldName: 'createdAt',
124-
expectedType: 'Date',
125-
receivedType: 'string',
126-
creatorId: 'creator-3',
127-
requestId: 'req-type-2',
128-
})
129-
);
130-
});
131-
132-
it('does not log an error for correctly typed fields', () => {
133-
const input = {
134-
id: 'creator-4',
135-
handle: 'good-handle',
136-
displayName: 'Bob',
137-
avatarUrl: 'https://example.com/avatar.png',
138-
isVerified: false,
139-
createdAt: new Date('2024-01-02T03:04:05.678Z'),
140-
updatedAt: new Date('2024-01-03T03:04:05.678Z'),
141-
} as any;
142-
143-
mapCreatorListItem(input);
144-
145-
expect(errorMock).not.toHaveBeenCalled();
14616
});
14717
});
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { createSeededCreatorFixture } from './seeded-creator-fixtures.utils';
2+
3+
describe('createSeededCreatorFixture', () => {
4+
it('produces deterministic records for the same seed', () => {
5+
const first = createSeededCreatorFixture(5);
6+
const second = createSeededCreatorFixture(5);
7+
8+
expect(first).toEqual(second);
9+
});
10+
11+
it('produces distinct records for different seeds', () => {
12+
const seedOne = createSeededCreatorFixture(1);
13+
const seedTwo = createSeededCreatorFixture(2);
14+
15+
expect(seedOne).not.toEqual(seedTwo);
16+
expect(seedOne.id).toBe('creator-1');
17+
expect(seedTwo.id).toBe('creator-2');
18+
});
19+
20+
it('derives stable field values from the seed', () => {
21+
const fixture = createSeededCreatorFixture(3);
22+
23+
expect(fixture).toMatchObject({
24+
id: 'creator-3',
25+
userId: 'user-3',
26+
handle: 'creator-3',
27+
displayName: 'Creator 3',
28+
avatarUrl: 'https://example.com/avatar-3.png',
29+
bio: 'Bio for creator 3',
30+
perkSummary: 'Perks for creator 3',
31+
isVerified: false,
32+
});
33+
expect(fixture.updatedAt.getTime()).toBe(
34+
fixture.createdAt.getTime() + 1000
35+
);
36+
});
37+
38+
it('respects overridden createdAt when deriving updatedAt', () => {
39+
const customDate = new Date('2025-01-01T00:00:00.000Z');
40+
const fixture = createSeededCreatorFixture(4, { createdAt: customDate });
41+
42+
expect(fixture.createdAt).toEqual(customDate);
43+
expect(fixture.updatedAt.getTime()).toBe(customDate.getTime() + 1000);
44+
});
45+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { CreatorProfile } from '../../types/profile.types';
2+
3+
const CREATOR_FIXTURE_BASE_DATE = new Date(Date.UTC(2020, 0, 1));
4+
5+
/**
6+
* Generates a deterministic creator record from a numeric seed.
7+
*
8+
* Field mapping:
9+
* - id: `creator-${seed}`
10+
* - userId: `user-${seed}`
11+
* - handle: `creator-${seed}`
12+
* - displayName: `Creator ${seed}`
13+
* - avatarUrl: `https://example.com/avatar-${seed}.png`
14+
* - bio: `Bio for creator ${seed}`
15+
* - perkSummary: `Perks for creator ${seed}`
16+
* - perks: []
17+
* - isVerified: true for even seeds, false for odd seeds
18+
* - createdAt: 2020-01-01 UTC plus `seed` days
19+
* - updatedAt: `createdAt` plus 1 second
20+
*
21+
* This helper is intentionally stable: the same seed always returns the same object,
22+
* and different seeds produce distinct creator values.
23+
*/
24+
export function createSeededCreatorFixture(
25+
seed: number,
26+
overrides: Partial<CreatorProfile> = {}
27+
): CreatorProfile {
28+
const normalizedSeed = Math.max(0, Math.floor(seed));
29+
const createdAt = new Date(CREATOR_FIXTURE_BASE_DATE);
30+
createdAt.setUTCDate(createdAt.getUTCDate() + normalizedSeed);
31+
const finalCreatedAt = overrides.createdAt ?? createdAt;
32+
33+
return {
34+
id: `creator-${normalizedSeed}`,
35+
userId: `user-${normalizedSeed}`,
36+
handle: `creator-${normalizedSeed}`,
37+
displayName: `Creator ${normalizedSeed}`,
38+
bio: `Bio for creator ${normalizedSeed}`,
39+
avatarUrl: `https://example.com/avatar-${normalizedSeed}.png`,
40+
perkSummary: `Perks for creator ${normalizedSeed}`,
41+
perks: [],
42+
isVerified: normalizedSeed % 2 === 0,
43+
createdAt: finalCreatedAt,
44+
updatedAt:
45+
overrides.updatedAt ?? new Date(finalCreatedAt.getTime() + 1000),
46+
...overrides,
47+
};
48+
}

0 commit comments

Comments
 (0)