fix(social): friend_request_sent local broadcast must carry target profile

The local branch was reusing friendRequestPayload (built for
friend_request_received with user=sender) for the sender-side
friend_request_sent broadcast. Tab B on the sender would render
the sender's own avatar where the target's should appear.
Match the federated branch — sent broadcast carries target.
This commit is contained in:
Jannis Braun
2026-04-25 22:18:47 +02:00
parent 1ad1fd1b0c
commit d88d369a82
3 changed files with 28 additions and 11 deletions
@@ -161,14 +161,17 @@ describe('POST /api/social/requests — federated branch (happy path)', () => {
.get(); .get();
expect(mutationRow).toBeTruthy(); expect(mutationRow).toBeTruthy();
// WS broadcast to sender // WS broadcast to sender — the 'user' field must carry the TARGET's profile (alice),
expect(sendToUser).toHaveBeenCalledWith( // not the sender. This is the federated analogue of the local-branch invariant.
CALLER_ID, const sentEvent = sendToUser.mock.calls.find(
expect.objectContaining({ (c: unknown[]) => (c[1] as { type?: string })?.type === 'friend_request_sent',
type: 'friend_request_sent',
request: expect.objectContaining({ id: body.requestId }),
}),
); );
expect(sentEvent).toBeDefined();
expect(sentEvent![0]).toBe(CALLER_ID);
expect(sentEvent![1].request.id).toBe(body.requestId);
// homeUserId identifies the target; username is the canonical stub form (<homeUserId>@<host>).
expect(sentEvent![1].request.user.homeUserId).toBe('remote-alice');
expect(sentEvent![1].request.user.username).toBe('remote-alice@orbit.test');
}); });
}); });
@@ -236,5 +236,9 @@ describe('POST /api/social/requests — case-insensitive username lookup', () =>
const sent = sendToUser.mock.calls.find(c => c[1]?.type === 'friend_request_sent'); const sent = sendToUser.mock.calls.find(c => c[1]?.type === 'friend_request_sent');
expect(sent).toBeDefined(); expect(sent).toBeDefined();
expect(sent![0]).toBe(CALLER_ID); expect(sent![0]).toBe(CALLER_ID);
// The 'user' field on the sent payload must be the TARGET (alice),
// not the sender — symmetric with how the federated branch builds it.
expect(sent![1].request.user.id).toBe('u1');
expect(sent![1].request.user.username).toBe('alice');
}); });
}); });
+14 -4
View File
@@ -88,8 +88,8 @@ async function handleLocalFriendRequest(
createdAt: now, createdAt: now,
}).run(); }).run();
// Broadcast friend_request_received to the target user // Broadcast to the target: they need to know who sent the request (sender profile).
const friendRequestPayload: FriendRequest = { const receivedPayload: FriendRequest = {
id, id,
fromId: request.userId, fromId: request.userId,
toId: targetUser.id, toId: targetUser.id,
@@ -98,14 +98,24 @@ async function handleLocalFriendRequest(
user: sanitizeUser(sender), user: sanitizeUser(sender),
}; };
// Broadcast to the sender's other tabs: they need to know who they added (target profile).
const sentPayload: FriendRequest = {
id,
fromId: request.userId,
toId: targetUser.id,
status: 'pending',
createdAt: now,
user: sanitizeUser(targetUser),
};
connectionManager.sendToUser(targetUser.id, { connectionManager.sendToUser(targetUser.id, {
type: 'friend_request_received', type: 'friend_request_received',
request: friendRequestPayload, request: receivedPayload,
}); });
connectionManager.sendToUser(request.userId, { connectionManager.sendToUser(request.userId, {
type: 'friend_request_sent', type: 'friend_request_sent',
request: friendRequestPayload, request: sentPayload,
}); });
// Federation relay: notify the target user's home instance // Federation relay: notify the target user's home instance