fix(api): enforce gdm call membership and ring recipient validation
This commit is contained in:
parent
68ae760fa8
commit
fcc8463cd8
@ -184,6 +184,18 @@ export class CallService {
|
||||
throw new InvalidChannelTypeForCallError();
|
||||
}
|
||||
|
||||
this.ensureRequesterIsGroupDmRecipient({
|
||||
channelType: channel.type,
|
||||
channelRecipientIds: channel.recipientIds,
|
||||
userId,
|
||||
});
|
||||
|
||||
this.validateExplicitRingRecipients({
|
||||
channelRecipientIds: channel.recipientIds,
|
||||
userId,
|
||||
recipients: ringing,
|
||||
});
|
||||
|
||||
const recipientIds = Array.from(channel.recipientIds);
|
||||
const channelRecipients = recipientIds.length > 0 ? await this.userRepository.listUsers(recipientIds) : [];
|
||||
|
||||
@ -323,6 +335,20 @@ export class CallService {
|
||||
throw new InvalidChannelTypeForCallError();
|
||||
}
|
||||
|
||||
this.ensureRequesterIsGroupDmRecipient({
|
||||
channelType: channel.type,
|
||||
channelRecipientIds: channel.recipientIds,
|
||||
userId,
|
||||
});
|
||||
|
||||
if (recipients !== undefined) {
|
||||
this.validateExplicitRingRecipients({
|
||||
channelRecipientIds: channel.recipientIds,
|
||||
userId,
|
||||
recipients,
|
||||
});
|
||||
}
|
||||
|
||||
if (channel.type === ChannelTypes.DM) {
|
||||
const channelRecipients = await this.userRepository.listUsers(Array.from(channel.recipientIds));
|
||||
await this.dmPermissionValidator.validate({recipients: channelRecipients, userId});
|
||||
@ -528,6 +554,42 @@ export class CallService {
|
||||
}
|
||||
}
|
||||
|
||||
private ensureRequesterIsGroupDmRecipient({
|
||||
channelType,
|
||||
channelRecipientIds,
|
||||
userId,
|
||||
}: {
|
||||
channelType: number;
|
||||
channelRecipientIds: Set<UserID>;
|
||||
userId: UserID;
|
||||
}): void {
|
||||
if (channelType !== ChannelTypes.GROUP_DM) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!channelRecipientIds.has(userId)) {
|
||||
throw new UnknownChannelError();
|
||||
}
|
||||
}
|
||||
|
||||
private validateExplicitRingRecipients({
|
||||
channelRecipientIds,
|
||||
userId,
|
||||
recipients,
|
||||
}: {
|
||||
channelRecipientIds: Set<UserID>;
|
||||
userId: UserID;
|
||||
recipients: Array<UserID>;
|
||||
}): void {
|
||||
const allowedRecipientIds = new Set(Array.from(channelRecipientIds).filter((id) => id !== userId));
|
||||
|
||||
for (const recipientId of recipients) {
|
||||
if (!allowedRecipientIds.has(recipientId)) {
|
||||
throw InputValidationError.fromCode('recipients', ValidationErrorCodes.USER_NOT_IN_CHANNEL);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async updateCallMessageEnded({
|
||||
channelId,
|
||||
messageId,
|
||||
|
||||
@ -18,12 +18,25 @@
|
||||
*/
|
||||
|
||||
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createDmChannel, createGuild} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {
|
||||
createDmChannel,
|
||||
createFriendship,
|
||||
createGroupDmChannel,
|
||||
createGuild,
|
||||
type MinimalChannelResponse,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import type {GuildInviteMetadataResponse} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
interface ErrorResponse {
|
||||
code: string;
|
||||
errors?: Array<{code?: string}>;
|
||||
}
|
||||
|
||||
interface PrivateChannelsResponse extends Array<MinimalChannelResponse> {}
|
||||
|
||||
describe('Call Endpoints', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
@ -55,6 +68,20 @@ describe('Call Endpoints', () => {
|
||||
return {user1, user2};
|
||||
}
|
||||
|
||||
async function setupGroupDmUsers() {
|
||||
const owner = await createTestAccount(harness);
|
||||
const memberOne = await createTestAccount(harness);
|
||||
const memberTwo = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, owner, memberOne);
|
||||
await createFriendship(harness, owner, memberTwo);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, owner.token, [memberOne.userId, memberTwo.userId]);
|
||||
|
||||
return {owner, memberOne, memberTwo, outsider, groupDm};
|
||||
}
|
||||
|
||||
describe('GET /channels/:channel_id/call', () => {
|
||||
it('returns call eligibility for DM channel when voice is disabled', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
@ -144,6 +171,70 @@ describe('Call Endpoints', () => {
|
||||
.expect(404)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('non-member cannot create or start a group DM call', async () => {
|
||||
const {outsider, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({})
|
||||
.expect(404, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('non-member cannot ring an existing group DM call', async () => {
|
||||
const {owner, memberOne, outsider, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [memberOne.userId]})
|
||||
.expect(204)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [memberOne.userId]})
|
||||
.expect(404, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects group DM ring recipients outside the channel recipient set', async () => {
|
||||
const {owner, memberOne, outsider, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
const error = await createBuilder<ErrorResponse>(harness, owner.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [memberOne.userId, outsider.userId]})
|
||||
.expect(400, 'INVALID_FORM_BODY')
|
||||
.execute();
|
||||
|
||||
expect(error.errors?.[0]?.code).toBe('USER_NOT_IN_CHANNEL');
|
||||
|
||||
const outsiderChannels = await createBuilder<PrivateChannelsResponse>(harness, outsider.token)
|
||||
.get('/users/@me/channels')
|
||||
.execute();
|
||||
|
||||
expect(outsiderChannels.some((channel) => channel.id === groupDm.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('group DM member can ring a valid recipient subset', async () => {
|
||||
const {memberOne, owner, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
await createBuilder(harness, memberOne.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [owner.userId]})
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('group DM member can ring default recipients when recipients are omitted', async () => {
|
||||
const {memberTwo, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
await createBuilder(harness, memberTwo.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({})
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /channels/:channel_id/call/stop-ringing', () => {
|
||||
|
||||
@ -23,6 +23,7 @@ import {
|
||||
createChannelInvite,
|
||||
createDmChannel,
|
||||
createFriendship,
|
||||
createGroupDmChannel,
|
||||
createGuild,
|
||||
getChannel,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
@ -30,7 +31,12 @@ import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUti
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
interface ErrorResponse {
|
||||
code: string;
|
||||
errors?: Array<{code?: string}>;
|
||||
}
|
||||
|
||||
describe('Voice Call Ringing', () => {
|
||||
let harness: ApiTestHarness;
|
||||
@ -61,6 +67,20 @@ describe('Voice Call Ringing', () => {
|
||||
return {user1, user2, guild};
|
||||
}
|
||||
|
||||
async function setupGroupDmUsers() {
|
||||
const owner = await createTestAccount(harness);
|
||||
const memberOne = await createTestAccount(harness);
|
||||
const memberTwo = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, owner, memberOne);
|
||||
await createFriendship(harness, owner, memberTwo);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, owner.token, [memberOne.userId, memberTwo.userId]);
|
||||
|
||||
return {owner, memberOne, memberTwo, outsider, groupDm};
|
||||
}
|
||||
|
||||
describe('Ring call', () => {
|
||||
it('rings call recipients in DM channel', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
@ -127,6 +147,50 @@ describe('Voice Call Ringing', () => {
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_CHANNEL_TYPE_FOR_CALL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('non-member cannot ring group DM call', async () => {
|
||||
const {owner, memberOne, outsider, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [memberOne.userId]})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [memberOne.userId]})
|
||||
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects non-recipient ids in group DM ring payload', async () => {
|
||||
const {owner, memberOne, outsider, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
const error = await createBuilder<ErrorResponse>(harness, owner.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [memberOne.userId, outsider.userId]})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY')
|
||||
.execute();
|
||||
|
||||
expect(error.errors?.[0]?.code).toBe('USER_NOT_IN_CHANNEL');
|
||||
});
|
||||
|
||||
it('group DM members can ring valid subset and default recipients', async () => {
|
||||
const {owner, memberOne, memberTwo, groupDm} = await setupGroupDmUsers();
|
||||
|
||||
await createBuilder(harness, memberOne.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({recipients: [owner.userId]})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, memberTwo.token)
|
||||
.post(`/channels/${groupDm.id}/call/ring`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stop ringing', () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user