450 lines
16 KiB
TypeScript
450 lines
16 KiB
TypeScript
/*
|
|
* Copyright (C) 2026 Fluxer Contributors
|
|
*
|
|
* This file is part of Fluxer.
|
|
*
|
|
* Fluxer is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* Fluxer is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import {createTestAccount, unclaimAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
|
import {authorizeBot, createTestBotAccount} from '@fluxer/api/src/bot/tests/BotTestUtils';
|
|
import {
|
|
acceptInvite,
|
|
blockUser,
|
|
createChannelInvite,
|
|
createDmChannel,
|
|
createFriendship,
|
|
createGroupDmChannel,
|
|
createGuild,
|
|
getChannel,
|
|
type MinimalChannelResponse,
|
|
sendChannelMessage,
|
|
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
|
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
|
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 {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
|
import {afterAll, beforeAll, beforeEach, describe, expect, test} from 'vitest';
|
|
|
|
interface PrivateChannelsResponse extends Array<MinimalChannelResponse> {}
|
|
|
|
describe('UserChannelService', () => {
|
|
let harness: ApiTestHarness;
|
|
|
|
beforeAll(async () => {
|
|
harness = await createApiTestHarness();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
await harness.reset();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await harness?.shutdown();
|
|
});
|
|
|
|
describe('DM channel creation', () => {
|
|
test('can create DM with a friend', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
|
|
const channel = await createDmChannel(harness, user1.token, user2.userId);
|
|
|
|
expect(channel.id).toBeDefined();
|
|
expect(channel.type).toBe(ChannelTypes.DM);
|
|
});
|
|
|
|
test('bot can create DM when it shares a guild with the recipient', async () => {
|
|
const botAccount = await createTestBotAccount(harness);
|
|
const recipient = await createTestAccount(harness);
|
|
const guild = await createGuild(harness, botAccount.ownerToken, 'Bot DM mutual guild');
|
|
const systemChannel = await getChannel(harness, botAccount.ownerToken, guild.system_channel_id!);
|
|
const invite = await createChannelInvite(harness, botAccount.ownerToken, systemChannel.id);
|
|
await acceptInvite(harness, recipient.token, invite.code);
|
|
await authorizeBot(harness, botAccount.ownerToken, botAccount.appId, ['bot'], guild.id, '0');
|
|
|
|
const channel = await createBuilder<MinimalChannelResponse>(harness, `Bot ${botAccount.botToken}`)
|
|
.post('/users/@me/channels')
|
|
.body({recipient_id: recipient.userId})
|
|
.execute();
|
|
|
|
expect(channel.id).toBeDefined();
|
|
expect(channel.type).toBe(ChannelTypes.DM);
|
|
});
|
|
|
|
test('cannot create DM with yourself', async () => {
|
|
const user = await createTestAccount(harness);
|
|
|
|
await createBuilder(harness, user.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipient_id: user.userId})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY')
|
|
.execute();
|
|
});
|
|
|
|
test('cannot create DM with unknown user', async () => {
|
|
const user = await createTestAccount(harness);
|
|
|
|
await createBuilder(harness, user.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipient_id: '999999999999999999'})
|
|
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_USER')
|
|
.execute();
|
|
});
|
|
|
|
test('cannot create DM with blocked user', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
const guild = await createGuild(harness, user1.token, 'Test Community');
|
|
const systemChannel = await getChannel(harness, user1.token, guild.system_channel_id!);
|
|
const invite = await createChannelInvite(harness, user1.token, systemChannel.id);
|
|
await acceptInvite(harness, user2.token, invite.code);
|
|
|
|
await blockUser(harness, user1, user2.userId);
|
|
|
|
await createBuilder(harness, user1.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipient_id: user2.userId})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'CANNOT_SEND_MESSAGES_TO_USER')
|
|
.execute();
|
|
});
|
|
|
|
test('unclaimed account cannot create DM', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
const guild = await createGuild(harness, user1.token, 'Test Community');
|
|
const systemChannel = await getChannel(harness, user1.token, guild.system_channel_id!);
|
|
const invite = await createChannelInvite(harness, user1.token, systemChannel.id);
|
|
await acceptInvite(harness, user2.token, invite.code);
|
|
await unclaimAccount(harness, user1.userId);
|
|
|
|
await createBuilder(harness, user1.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipient_id: user2.userId})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'UNCLAIMED_ACCOUNT_CANNOT_SEND_DIRECT_MESSAGES')
|
|
.execute();
|
|
});
|
|
|
|
test('reopening existing DM returns same channel', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
|
|
const channel1 = await createDmChannel(harness, user1.token, user2.userId);
|
|
const channel2 = await createDmChannel(harness, user1.token, user2.userId);
|
|
|
|
expect(channel1.id).toBe(channel2.id);
|
|
});
|
|
|
|
test('reopening existing DM works after blocking user', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
|
|
const channel1 = await createDmChannel(harness, user1.token, user2.userId);
|
|
|
|
await blockUser(harness, user1, user2.userId);
|
|
|
|
const channel2 = await createDmChannel(harness, user1.token, user2.userId);
|
|
|
|
expect(channel2.id).toBe(channel1.id);
|
|
});
|
|
});
|
|
|
|
describe('Group DM creation', () => {
|
|
test('can create group DM with friends', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend1 = await createTestAccount(harness);
|
|
const friend2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend1);
|
|
await createFriendship(harness, owner, friend2);
|
|
|
|
const channel = await createGroupDmChannel(harness, owner.token, [friend1.userId, friend2.userId]);
|
|
|
|
expect(channel.id).toBeDefined();
|
|
expect(channel.type).toBe(ChannelTypes.GROUP_DM);
|
|
expect(channel.owner_id).toBe(owner.userId);
|
|
expect(channel.recipients.length).toBe(2);
|
|
});
|
|
|
|
test('cannot create group DM with non-friend', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend = await createTestAccount(harness);
|
|
const stranger = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipients: [friend.userId, stranger.userId]})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'NOT_FRIENDS_WITH_USER')
|
|
.execute();
|
|
});
|
|
|
|
test('cannot add yourself to group DM recipients', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipients: [friend.userId, owner.userId]})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY')
|
|
.execute();
|
|
});
|
|
|
|
test('cannot add duplicate recipients to group DM', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipients: [friend.userId, friend.userId]})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY')
|
|
.execute();
|
|
});
|
|
|
|
test('cannot create group DM with unknown user', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.post('/users/@me/channels')
|
|
.body({recipients: [friend.userId, '999999999999999999']})
|
|
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_USER')
|
|
.execute();
|
|
});
|
|
});
|
|
|
|
describe('private channel listing', () => {
|
|
test('lists all private channels for user', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
const user3 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
await createFriendship(harness, user1, user3);
|
|
|
|
await createDmChannel(harness, user1.token, user2.userId);
|
|
await createDmChannel(harness, user1.token, user3.userId);
|
|
|
|
const channels = await createBuilder<PrivateChannelsResponse>(harness, user1.token)
|
|
.get('/users/@me/channels')
|
|
.execute();
|
|
|
|
expect(channels.length).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('DM pinning', () => {
|
|
test('can pin and unpin DM channel', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
const channel = await createDmChannel(harness, user1.token, user2.userId);
|
|
|
|
await createBuilder(harness, user1.token)
|
|
.put(`/users/@me/channels/${channel.id}/pin`)
|
|
.body(null)
|
|
.expect(HTTP_STATUS.NO_CONTENT)
|
|
.execute();
|
|
|
|
await createBuilder(harness, user1.token)
|
|
.delete(`/users/@me/channels/${channel.id}/pin`)
|
|
.expect(HTTP_STATUS.NO_CONTENT)
|
|
.execute();
|
|
});
|
|
|
|
test('cannot pin non-DM channel', async () => {
|
|
const user = await createTestAccount(harness);
|
|
|
|
await createBuilder(harness, user.token)
|
|
.put('/users/@me/channels/999999999999999999/pin')
|
|
.body(null)
|
|
.expect(HTTP_STATUS.NOT_FOUND)
|
|
.execute();
|
|
});
|
|
});
|
|
|
|
describe('preload DM messages', () => {
|
|
test('can preload messages for multiple DM channels', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
const user3 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
await createFriendship(harness, user1, user3);
|
|
|
|
await ensureSessionStarted(harness, user1.token);
|
|
|
|
const channel1 = await createDmChannel(harness, user1.token, user2.userId);
|
|
const channel2 = await createDmChannel(harness, user1.token, user3.userId);
|
|
|
|
await sendChannelMessage(harness, user1.token, channel1.id, 'Hello user2');
|
|
await sendChannelMessage(harness, user1.token, channel2.id, 'Hello user3');
|
|
|
|
const result = await createBuilder<Record<string, unknown>>(harness, user1.token)
|
|
.post('/users/@me/preload-messages')
|
|
.body({channels: [channel1.id, channel2.id]})
|
|
.execute();
|
|
|
|
expect(result).toBeDefined();
|
|
});
|
|
|
|
test('cannot preload more than 100 channels', async () => {
|
|
const user = await createTestAccount(harness);
|
|
|
|
const tooManyChannels = Array.from({length: 101}, (_, i) => i.toString());
|
|
|
|
await createBuilder(harness, user.token)
|
|
.post('/users/@me/preload-messages')
|
|
.body({channels: tooManyChannels})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'INVALID_FORM_BODY')
|
|
.execute();
|
|
});
|
|
});
|
|
|
|
describe('sending messages in DMs', () => {
|
|
test('can send message in DM to friend', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
await ensureSessionStarted(harness, user1.token);
|
|
|
|
const channel = await createDmChannel(harness, user1.token, user2.userId);
|
|
const message = await sendChannelMessage(harness, user1.token, channel.id, 'Hello!');
|
|
|
|
expect(message.id).toBeDefined();
|
|
expect(message.content).toBe('Hello!');
|
|
});
|
|
|
|
test('cannot send message to user who blocked you', async () => {
|
|
const user1 = await createTestAccount(harness);
|
|
const user2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, user1, user2);
|
|
await ensureSessionStarted(harness, user1.token);
|
|
|
|
const channel = await createDmChannel(harness, user1.token, user2.userId);
|
|
|
|
await createBuilder(harness, user2.token)
|
|
.put(`/users/@me/relationships/${user1.userId}`)
|
|
.body({type: 2})
|
|
.execute();
|
|
|
|
await createBuilder(harness, user1.token)
|
|
.post(`/channels/${channel.id}/messages`)
|
|
.body({content: 'Hello!'})
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'CANNOT_SEND_MESSAGES_TO_USER')
|
|
.execute();
|
|
});
|
|
});
|
|
|
|
describe('group DM recipient management', () => {
|
|
test('owner can add friend to group DM', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend1 = await createTestAccount(harness);
|
|
const friend2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend1);
|
|
await createFriendship(harness, owner, friend2);
|
|
|
|
const channel = await createGroupDmChannel(harness, owner.token, [friend1.userId]);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.put(`/channels/${channel.id}/recipients/${friend2.userId}`)
|
|
.body(null)
|
|
.expect(HTTP_STATUS.NO_CONTENT)
|
|
.execute();
|
|
});
|
|
|
|
test('owner can remove recipient from group DM', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend1 = await createTestAccount(harness);
|
|
const friend2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend1);
|
|
await createFriendship(harness, owner, friend2);
|
|
|
|
const channel = await createGroupDmChannel(harness, owner.token, [friend1.userId, friend2.userId]);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.delete(`/channels/${channel.id}/recipients/${friend2.userId}`)
|
|
.expect(HTTP_STATUS.NO_CONTENT)
|
|
.execute();
|
|
});
|
|
|
|
test('cannot add non-friend to group DM', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const friend = await createTestAccount(harness);
|
|
const stranger = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, friend);
|
|
|
|
const channel = await createGroupDmChannel(harness, owner.token, [friend.userId]);
|
|
|
|
await createBuilder(harness, owner.token)
|
|
.put(`/channels/${channel.id}/recipients/${stranger.userId}`)
|
|
.body(null)
|
|
.expect(HTTP_STATUS.BAD_REQUEST, 'NOT_FRIENDS_WITH_USER')
|
|
.execute();
|
|
});
|
|
|
|
test('non-owner cannot remove other recipients', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const member1 = await createTestAccount(harness);
|
|
const member2 = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, member1);
|
|
await createFriendship(harness, owner, member2);
|
|
|
|
const channel = await createGroupDmChannel(harness, owner.token, [member1.userId, member2.userId]);
|
|
|
|
await createBuilder(harness, member1.token)
|
|
.delete(`/channels/${channel.id}/recipients/${member2.userId}`)
|
|
.expect(HTTP_STATUS.FORBIDDEN)
|
|
.execute();
|
|
});
|
|
|
|
test('member can leave group DM', async () => {
|
|
const owner = await createTestAccount(harness);
|
|
const member = await createTestAccount(harness);
|
|
|
|
await createFriendship(harness, owner, member);
|
|
|
|
const channel = await createGroupDmChannel(harness, owner.token, [member.userId]);
|
|
|
|
await createBuilder(harness, member.token)
|
|
.delete(`/channels/${channel.id}/recipients/${member.userId}`)
|
|
.expect(HTTP_STATUS.NO_CONTENT)
|
|
.execute();
|
|
});
|
|
});
|
|
});
|