From 3d5e1796a81dc5f6bd441f3d62f10948b820666e Mon Sep 17 00:00:00 2001 From: Endika Iglesias Date: Thu, 28 May 2026 22:07:12 +0200 Subject: [PATCH] fix(profile): credit actual editor in profile-update history --- src/application/dtos/UpdateProfileDTO.ts | 1 + .../handlers/UpdateProfileHandler.ts | 9 +++-- .../features/event/ProfileEditor.tsx | 3 +- .../handlers/UpdateProfileHandler.test.ts | 35 +++++++++++++++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/application/dtos/UpdateProfileDTO.ts b/src/application/dtos/UpdateProfileDTO.ts index 886e7ae..835d3c5 100644 --- a/src/application/dtos/UpdateProfileDTO.ts +++ b/src/application/dtos/UpdateProfileDTO.ts @@ -5,6 +5,7 @@ import { USER_KINDS } from '@/domain/entities/User' export const UpdateProfileSchema = z.object({ eventId: z.string().regex(/^[a-z0-9]{7}$/), userId: z.string().uuid(), + actorId: z.string().uuid(), name: z.string().trim().min(2).max(50).optional(), alias: z.string().trim().max(50).nullable().optional(), email: z.string().trim().max(100).nullable().optional(), diff --git a/src/application/handlers/UpdateProfileHandler.ts b/src/application/handlers/UpdateProfileHandler.ts index d094ae1..ca421aa 100644 --- a/src/application/handlers/UpdateProfileHandler.ts +++ b/src/application/handlers/UpdateProfileHandler.ts @@ -32,12 +32,17 @@ export class UpdateProfileHandler { const nextUsers = row.snapshot.users.map((u) => u.id === parsed.userId ? updated.toSnapshot() : u, ) + const actorName = row.snapshot.users.find((u) => u.id === parsed.actorId)?.name ?? 'Someone' + const description = + parsed.actorId === parsed.userId + ? `${updated.displayName} updated their profile` + : `${actorName} updated ${updated.displayName}'s profile` const nextSnapshot: EventSnapshot = HistoryAppender.append( { ...row.snapshot, users: nextUsers }, { type: 'user_profile_updated', - userId: parsed.userId, - description: `${updated.displayName} updated their profile`, + userId: parsed.actorId, + description, }, ) diff --git a/src/presentation/components/features/event/ProfileEditor.tsx b/src/presentation/components/features/event/ProfileEditor.tsx index 2f6dec8..7995f1d 100644 --- a/src/presentation/components/features/event/ProfileEditor.tsx +++ b/src/presentation/components/features/event/ProfileEditor.tsx @@ -70,7 +70,7 @@ export function ProfileEditor({ userId, onClose }: { userId?: string; onClose: ( function save(e: FormEvent) { e.preventDefault() - if (!event || !targetUserId) return + if (!event || !targetUserId || !me?.id) return guardedExecute(async () => { setBusy(true) setError(null) @@ -79,6 +79,7 @@ export function ProfileEditor({ userId, onClose }: { userId?: string; onClose: ( const result = await handler.execute({ eventId: event.id, userId: targetUserId, + actorId: me.id, name: name.trim(), alias: alias.trim() || null, email: email.trim() || null, diff --git a/tests/application/handlers/UpdateProfileHandler.test.ts b/tests/application/handlers/UpdateProfileHandler.test.ts index 8a22928..deda503 100644 --- a/tests/application/handlers/UpdateProfileHandler.test.ts +++ b/tests/application/handlers/UpdateProfileHandler.test.ts @@ -11,6 +11,7 @@ describe('UpdateProfileHandler', () => { new UpdateProfileHandler(repo).execute({ eventId: create.event.id, userId: '018f4a8e-0000-7000-8000-000000000000', + actorId: create.creator.id, email: 'x@y.com', }), ).rejects.toThrow(/not in event/i) @@ -22,6 +23,7 @@ describe('UpdateProfileHandler', () => { const result = await new UpdateProfileHandler(repo).execute({ eventId: create.event.id, userId: create.creator.id, + actorId: create.creator.id, email: 'john@example.com', allergies: [{ name: 'gluten', severity: 'severe' }], }) @@ -39,10 +41,43 @@ describe('UpdateProfileHandler', () => { const result = await new UpdateProfileHandler(repo).execute({ eventId: create.event.id, userId: create.creator.id, + actorId: create.creator.id, email: 'x@y.com', }) const user = result.event.users.find((u) => u.id === create.creator.id)! expect(user.alias).toBe('cousin') expect(user.allergies).toEqual([]) }) + + it('records actor as the history userId and uses self description when editing own profile', async () => { + const repo = new InMemoryEventRepository() + const create = await new CreateEventHandler(repo).execute({ name: 'Trip', creatorName: 'John' }) + const result = await new UpdateProfileHandler(repo).execute({ + eventId: create.event.id, + userId: create.creator.id, + actorId: create.creator.id, + email: 'self@example.com', + }) + const entry = result.event.history.at(-1)! + expect(entry.userId).toBe(create.creator.id) + expect(entry.description).toMatch(/updated their profile/i) + }) + + it('records actor as the history userId and credits the actor when editing someone elses profile', async () => { + const repo = new InMemoryEventRepository() + const create = await new CreateEventHandler(repo).execute({ name: 'Trip', creatorName: 'John' }) + const joinHandler = new (await import('@/application/handlers/JoinAsNewUserHandler')).JoinAsNewUserHandler(repo) + const joined = await joinHandler.execute({ eventId: create.event.id, name: 'Maite' }) + const result = await new UpdateProfileHandler(repo).execute({ + eventId: create.event.id, + userId: joined.newUser.id, + actorId: create.creator.id, + email: 'edited@example.com', + }) + const entry = result.event.history.at(-1)! + expect(entry.userId).toBe(create.creator.id) + expect(entry.description).toContain('John') + expect(entry.description).toContain("Maite") + expect(entry.description).not.toMatch(/their profile/i) + }) })