From c246c4a719e6e6a47679dfc30d24a69fc06f5abc Mon Sep 17 00:00:00 2001 From: mitchellecm7 Date: Sun, 19 Jul 2026 16:06:49 -0700 Subject: [PATCH 1/2] feat: add maskFields helper for redacting sensitive fields in logs - Closes #569 --- src/utils/log-field-sanitizer.utils.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) 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; +} From 0dab38fd84380fde9b8c66af0e3963affcfa8678 Mon Sep 17 00:00:00 2001 From: mitchellecm7 Date: Sun, 19 Jul 2026 16:46:58 -0700 Subject: [PATCH 2/2] test: add integration test for stable ordering with identical sort values - Closes #586 --- ...or-list-sort-stability.integration.test.ts | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/__tests__/integration/creator-list-sort-stability.integration.test.ts 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..46e260b --- /dev/null +++ b/src/__tests__/integration/creator-list-sort-stability.integration.test.ts @@ -0,0 +1,96 @@ +import { describe, it, expect, beforeAll, afterAll } from '@jest/globals'; +import { PrismaClient } from '@prisma/client'; + +const prisma = new PrismaClient(); + +describe('Creator list stable sort with tied values', () => { + const CREATOR_COUNT = 3; + const SAME_PRICE = '9.99'; + + beforeAll(async () => { + // Seed three creators with identical prices + for (let i = 0; i < CREATOR_COUNT; i++) { + await prisma.creatorProfile.create({ + data: { + handle: ied-creator-, + displayName: Tied Creator , + priceSnapshot: SAME_PRICE, + verified: true, + }, + }); + } + }); + + afterAll(async () => { + // Cleanup + for (let i = 0; i < CREATOR_COUNT; i++) { + await prisma.creatorProfile.deleteMany({ + where: { handle: ied-creator- }, + }); + } + }); + + it('returns all creators exactly once across paginated pages with tied sort values', async () => { + const seen = new Set(); + let cursor: string | undefined; + + // Fetch all pages + do { + const params = new URLSearchParams({ + sort: 'price', + order: 'asc', + limit: '2', // Small page size to force pagination + }); + if (cursor) params.set('cursor', cursor); + + const response = await fetch(/api/v1/creators?); + const body = await response.json(); + + for (const creator of body.data) { + if (creator.priceSnapshot === SAME_PRICE) { + seen.add(creator.handle); + } + } + + cursor = body.meta?.nextCursor; + } while (cursor); + + // All 3 tied creators should appear exactly once + expect(seen.size).toBe(CREATOR_COUNT); + for (let i = 0; i < CREATOR_COUNT; i++) { + expect(seen.has( ied-creator-)).toBe(true); + } + }); + + it('returns consistent order across repeated requests', async () => { + const firstRun: string[] = []; + const secondRun: string[] = []; + + const fetchCreators = async (): Promise => { + const handles: string[] = []; + let cursor: string | undefined; + do { + const params = new URLSearchParams({ sort: 'price', order: 'asc', limit: '2' }); + if (cursor) params.set('cursor', cursor); + + const response = await fetch(/api/v1/creators?); + const body = await response.json(); + + for (const creator of body.data) { + if (creator.priceSnapshot === SAME_PRICE) { + handles.push(creator.handle); + } + } + cursor = body.meta?.nextCursor; + } while (cursor); + return handles; + }; + + const first = await fetchCreators(); + const second = await fetchCreators(); + + // Order must be identical between runs + expect(first).toEqual(second); + expect(first.length).toBe(CREATOR_COUNT); + }); +});