From 5f5590a3c947818291c4078d9808430fef212999 Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Sat, 25 Apr 2026 19:14:31 +0200 Subject: [PATCH] fix(social): normalize username lookup on POST /api/social/requests Registration canonicalizes usernames to lowercase (auth.ts:32), but the friend-request endpoint compared with strict eq() against raw user input, so 'Bob' returned 404 even when 'bob' existed. Trim and lowercase before lookup, matching the rest of the auth boundary. Empty-after-trim now returns 400 (was: 404). Tests appended to social.test.ts as a second describe block sharing the harness from Task 1. --- packages/server/src/routes/social.test.ts | 54 +++++++++++++++++++++++ packages/server/src/routes/social.ts | 10 ++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/server/src/routes/social.test.ts b/packages/server/src/routes/social.test.ts index 06b270db..5381b5be 100644 --- a/packages/server/src/routes/social.test.ts +++ b/packages/server/src/routes/social.test.ts @@ -159,3 +159,57 @@ describe('GET /api/social/search — filter hygiene', () => { expect(out.map(u => u.id)).toContain('u1'); }); }); + +describe('POST /api/social/requests — case-insensitive username lookup', () => { + let app: FastifyInstance; + + beforeEach(async () => { + sqlite = new Database(':memory:'); + testDb = drizzle(sqlite, { schema }); + applyMigrations(sqlite); + seedUser({ id: CALLER_ID, username: 'caller' }); + // Target stored canonically lowercase, as registration would write it. + seedUser({ id: 'target-id', username: 'bob' }); + app = await buildApp(); + }); + + async function sendRequest(username: string) { + return app.inject({ + method: 'POST', + url: '/api/social/requests', + payload: { username }, + }); + } + + it('finds the target when the caller types the exact stored handle', async () => { + const res = await sendRequest('bob'); + expect(res.statusCode).toBe(201); + const inserted = testDb.select().from(schema.friendRequests) + .where(eq(schema.friendRequests.toId, 'target-id')).get(); + expect(inserted).toBeTruthy(); + }); + + it('finds the target when the caller types a mixed-case handle', async () => { + const res = await sendRequest('Bob'); + expect(res.statusCode).toBe(201); + const inserted = testDb.select().from(schema.friendRequests) + .where(eq(schema.friendRequests.toId, 'target-id')).get(); + expect(inserted).toBeTruthy(); + }); + + it('finds the target when the caller types an all-uppercase handle', async () => { + const res = await sendRequest('BOB'); + expect(res.statusCode).toBe(201); + }); + + it('trims surrounding whitespace before lookup', async () => { + const res = await sendRequest(' bob '); + expect(res.statusCode).toBe(201); + }); + + it('returns 404 when the handle does not exist', async () => { + const res = await sendRequest('nobody'); + expect(res.statusCode).toBe(404); + expect(JSON.parse(res.body).error).toBe('User not found'); + }); +}); diff --git a/packages/server/src/routes/social.ts b/packages/server/src/routes/social.ts index faa18956..bb8f769f 100644 --- a/packages/server/src/routes/social.ts +++ b/packages/server/src/routes/social.ts @@ -119,12 +119,18 @@ export async function socialRoutes(app: FastifyInstance): Promise { const { username } = request.body; const db = getDb(); - if (!username) { + if (!username || typeof username !== 'string') { + return reply.code(400).send({ error: 'Username is required', statusCode: 400 }); + } + + // Match the canonical-lowercase form used by auth (auth.ts:32, 211, 256). + const lookupUsername = username.trim().toLowerCase(); + if (!lookupUsername) { return reply.code(400).send({ error: 'Username is required', statusCode: 400 }); } // Find the target user - const targetUser = db.select().from(schema.users).where(eq(schema.users.username, username)).get(); + const targetUser = db.select().from(schema.users).where(eq(schema.users.username, lookupUsername)).get(); if (!targetUser) { return reply.code(404).send({ error: 'User not found', statusCode: 404 }); }