diff --git a/src/__tests__/integration/creator-list-sort-stability.integration.test.ts b/src/__tests__/integration/creator-list-sort-stability.integration.test.ts new file mode 100644 index 0000000..ede0848 --- /dev/null +++ b/src/__tests__/integration/creator-list-sort-stability.integration.test.ts @@ -0,0 +1,39 @@ +import supertest from 'supertest'; +import app from '../../app'; + +describe('Creator list stable sort with tied values', () => { + it('returns consistent order across repeated requests', async () => { + const first = await supertest(app) + .get('/api/v1/creators') + .query({ sort: 'price', order: 'asc', limit: '10' }); + + const second = await supertest(app) + .get('/api/v1/creators') + .query({ sort: 'price', order: 'asc', limit: '10' }); + + expect(first.status).toBe(200); + expect(second.status).toBe(200); + + const firstHandles = (first.body.data || []).map((c: any) => c.handle); + const secondHandles = (second.body.data || []).map((c: any) => c.handle); + + expect(firstHandles).toEqual(secondHandles); + }); + + it('does not duplicate creators across paginated pages', async () => { + const page1 = await supertest(app) + .get('/api/v1/creators') + .query({ sort: 'price', order: 'asc', limit: '2' }); + + const page2 = await supertest(app) + .get('/api/v1/creators') + .query({ sort: 'price', order: 'asc', limit: '2', cursor: (page1.body.meta?.nextCursor || '') }); + + const page1Handles = (page1.body.data || []).map((c: any) => c.handle); + const page2Handles = (page2.body.data || []).map((c: any) => c.handle); + + const allHandles = [...page1Handles, ...page2Handles]; + const unique = new Set(allHandles); + expect(unique.size).toBe(allHandles.length); + }); +}); diff --git a/src/utils/log-field-sanitizer.utils.ts b/src/utils/log-field-sanitizer.utils.ts index f761579..f095407 100644 --- a/src/utils/log-field-sanitizer.utils.ts +++ b/src/utils/log-field-sanitizer.utils.ts @@ -101,3 +101,22 @@ export function sanitizeLogObject(obj: unknown): unknown { return obj; } + +/** + * Masks sensitive fields in an object by replacing their values with [REDACTED]. + * Works recursively for nested objects. Does not mutate the original object. + */ +export function maskFields(obj: Record, fields: string[]): Record { + const fieldSet = new Set(fields); + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + if (fieldSet.has(key)) { + result[key] = '[REDACTED]'; + } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) { + result[key] = maskFields(value as Record, fields); + } else { + result[key] = value; + } + } + return result; +}