refactor progress
This commit is contained in:
320
packages/api/src/channel/tests/AttachmentDecay.test.tsx
Normal file
320
packages/api/src/channel/tests/AttachmentDecay.test.tsx
Normal file
@@ -0,0 +1,320 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannel,
|
||||
createGuild,
|
||||
loadFixture,
|
||||
sendMessageWithAttachments,
|
||||
} from '@fluxer/api/src/channel/tests/AttachmentTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {getExpiryBucket} from '@fluxer/api/src/utils/AttachmentDecay';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
interface AttachmentDecayRow {
|
||||
attachment_id: string;
|
||||
channel_id: string;
|
||||
message_id: string;
|
||||
filename: string;
|
||||
size_bytes: string;
|
||||
expires_at: string;
|
||||
expiry_bucket: number;
|
||||
status: string | null;
|
||||
}
|
||||
|
||||
interface AttachmentDecayQueryResponse {
|
||||
rows: Array<AttachmentDecayRow>;
|
||||
has_more: boolean;
|
||||
count: number;
|
||||
}
|
||||
|
||||
describe('Attachment Decay', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('should create decay metadata when uploading attachment', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Decay Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(
|
||||
harness,
|
||||
account.token,
|
||||
channelId,
|
||||
{
|
||||
content: 'Attachment with decay tracking',
|
||||
attachments: [{id: 0, filename: 'test.png'}],
|
||||
},
|
||||
[{index: 0, filename: 'test.png', data: fileData}],
|
||||
);
|
||||
|
||||
expect(response.status).toBe(HTTP_STATUS.OK);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!.length).toBe(1);
|
||||
|
||||
const attachmentId = json.attachments![0].id;
|
||||
|
||||
const decayResponse = await createBuilderWithoutAuth<{row: AttachmentDecayRow | null}>(harness)
|
||||
.get(`/test/attachment-decay/${attachmentId}`)
|
||||
.execute();
|
||||
|
||||
expect(decayResponse.row).not.toBeNull();
|
||||
expect(decayResponse.row?.attachment_id).toBe(attachmentId);
|
||||
expect(decayResponse.row?.channel_id).toBe(channelId);
|
||||
expect(decayResponse.row?.message_id).toBe(json.id);
|
||||
});
|
||||
|
||||
test('should track multiple attachments in single message', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Multi Attachment Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
const file1Data = loadFixture('yeah.png');
|
||||
const file2Data = loadFixture('thisisfine.gif');
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(
|
||||
harness,
|
||||
account.token,
|
||||
channelId,
|
||||
{
|
||||
content: 'Multiple attachments',
|
||||
attachments: [
|
||||
{id: 0, filename: 'first.png'},
|
||||
{id: 1, filename: 'second.gif'},
|
||||
],
|
||||
},
|
||||
[
|
||||
{index: 0, filename: 'first.png', data: file1Data},
|
||||
{index: 1, filename: 'second.gif', data: file2Data},
|
||||
],
|
||||
);
|
||||
|
||||
expect(response.status).toBe(HTTP_STATUS.OK);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!.length).toBe(2);
|
||||
|
||||
for (const attachment of json.attachments!) {
|
||||
const decayResponse = await createBuilderWithoutAuth<{row: AttachmentDecayRow | null}>(harness)
|
||||
.get(`/test/attachment-decay/${attachment.id}`)
|
||||
.execute();
|
||||
|
||||
expect(decayResponse.row).not.toBeNull();
|
||||
expect(decayResponse.row?.message_id).toBe(json.id);
|
||||
}
|
||||
});
|
||||
|
||||
test('should seed attachment decay rows via test endpoint', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Seed Test Guild');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
const futureExpiryDate = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000);
|
||||
const futureExpiry = futureExpiryDate.toISOString();
|
||||
const bucket = getExpiryBucket(futureExpiryDate);
|
||||
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/test/attachment-decay/rows')
|
||||
.body({
|
||||
rows: [
|
||||
{
|
||||
attachment_id: '123456789012345678',
|
||||
channel_id: channelId,
|
||||
message_id: '234567890123456789',
|
||||
filename: 'seeded.png',
|
||||
size_bytes: '1024',
|
||||
expires_at: futureExpiry,
|
||||
},
|
||||
],
|
||||
})
|
||||
.execute();
|
||||
|
||||
const queryTimeAfterExpiry = new Date(futureExpiryDate.getTime() + 1000).toISOString();
|
||||
const queryResponse = await createBuilderWithoutAuth<AttachmentDecayQueryResponse>(harness)
|
||||
.post('/test/attachment-decay/query')
|
||||
.body({
|
||||
bucket,
|
||||
limit: 100,
|
||||
current_time: queryTimeAfterExpiry,
|
||||
})
|
||||
.execute();
|
||||
|
||||
expect(queryResponse.rows.some((r) => r.attachment_id === '123456789012345678')).toBe(true);
|
||||
});
|
||||
|
||||
test('should clear attachment decay data', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Clear Test Guild');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
const expiry = new Date(Date.now() + 1000).toISOString();
|
||||
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post('/test/attachment-decay/rows')
|
||||
.body({
|
||||
rows: [
|
||||
{
|
||||
attachment_id: '111111111111111111',
|
||||
channel_id: channelId,
|
||||
message_id: '222222222222222222',
|
||||
filename: 'clear-test.png',
|
||||
size_bytes: '512',
|
||||
expires_at: expiry,
|
||||
},
|
||||
],
|
||||
})
|
||||
.execute();
|
||||
|
||||
await createBuilderWithoutAuth(harness).post('/test/attachment-decay/clear').body({}).execute();
|
||||
|
||||
const checkResponse = await createBuilderWithoutAuth<{row: AttachmentDecayRow | null}>(harness)
|
||||
.get('/test/attachment-decay/111111111111111111')
|
||||
.execute();
|
||||
|
||||
expect(checkResponse.row).toBeNull();
|
||||
});
|
||||
|
||||
test('should include decay metadata in message response', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Metadata Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(
|
||||
harness,
|
||||
account.token,
|
||||
channelId,
|
||||
{
|
||||
content: 'Decay metadata test',
|
||||
attachments: [{id: 0, filename: 'metadata.png'}],
|
||||
},
|
||||
[{index: 0, filename: 'metadata.png', data: fileData}],
|
||||
);
|
||||
|
||||
expect(response.status).toBe(HTTP_STATUS.OK);
|
||||
|
||||
const messageResponse = await createBuilder<{
|
||||
id: string;
|
||||
attachments: Array<{id: string; filename: string; url: string}>;
|
||||
}>(harness, account.token)
|
||||
.get(`/channels/${channelId}/messages/${json.id}`)
|
||||
.execute();
|
||||
|
||||
expect(messageResponse.attachments).toBeDefined();
|
||||
expect(messageResponse.attachments).not.toBeNull();
|
||||
expect(messageResponse.attachments!.length).toBe(1);
|
||||
expect(messageResponse.attachments![0].url).toBeTruthy();
|
||||
});
|
||||
|
||||
test('should handle attachment with different sizes', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Size Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
const smallFile = loadFixture('yeah.png');
|
||||
const largeFile = loadFixture('thisisfine.gif');
|
||||
|
||||
const smallResult = await sendMessageWithAttachments(
|
||||
harness,
|
||||
account.token,
|
||||
channelId,
|
||||
{
|
||||
content: 'Small file',
|
||||
attachments: [{id: 0, filename: 'small.png'}],
|
||||
},
|
||||
[{index: 0, filename: 'small.png', data: smallFile}],
|
||||
);
|
||||
|
||||
const largeResult = await sendMessageWithAttachments(
|
||||
harness,
|
||||
account.token,
|
||||
channelId,
|
||||
{
|
||||
content: 'Large file',
|
||||
attachments: [{id: 0, filename: 'large.gif'}],
|
||||
},
|
||||
[{index: 0, filename: 'large.gif', data: largeFile}],
|
||||
);
|
||||
|
||||
expect(smallResult.response.status).toBe(HTTP_STATUS.OK);
|
||||
expect(largeResult.response.status).toBe(HTTP_STATUS.OK);
|
||||
|
||||
const smallDecay = await createBuilderWithoutAuth<{row: AttachmentDecayRow | null}>(harness)
|
||||
.get(`/test/attachment-decay/${smallResult.json.attachments![0].id}`)
|
||||
.execute();
|
||||
|
||||
const largeDecay = await createBuilderWithoutAuth<{row: AttachmentDecayRow | null}>(harness)
|
||||
.get(`/test/attachment-decay/${largeResult.json.attachments![0].id}`)
|
||||
.execute();
|
||||
|
||||
expect(smallDecay.row).not.toBeNull();
|
||||
expect(largeDecay.row).not.toBeNull();
|
||||
|
||||
const smallSize = BigInt(smallDecay.row?.size_bytes ?? '0');
|
||||
const largeSize = BigInt(largeDecay.row?.size_bytes ?? '0');
|
||||
expect(largeSize).toBeGreaterThan(smallSize);
|
||||
});
|
||||
|
||||
test('should track filename in decay metadata', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Filename Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(
|
||||
harness,
|
||||
account.token,
|
||||
channelId,
|
||||
{
|
||||
content: 'Filename tracking test',
|
||||
attachments: [{id: 0, filename: 'unique-filename-test.png'}],
|
||||
},
|
||||
[{index: 0, filename: 'unique-filename-test.png', data: fileData}],
|
||||
);
|
||||
|
||||
expect(response.status).toBe(HTTP_STATUS.OK);
|
||||
|
||||
const decayResponse = await createBuilderWithoutAuth<{row: AttachmentDecayRow | null}>(harness)
|
||||
.get(`/test/attachment-decay/${json.attachments![0].id}`)
|
||||
.execute();
|
||||
|
||||
expect(decayResponse.row).not.toBeNull();
|
||||
expect(decayResponse.row?.filename).toBe('unique-filename-test.png');
|
||||
});
|
||||
});
|
||||
165
packages/api/src/channel/tests/AttachmentTestUtils.tsx
Normal file
165
packages/api/src/channel/tests/AttachmentTestUtils.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
/*
|
||||
* 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 {readFileSync} from 'node:fs';
|
||||
import {join} from 'node:path';
|
||||
import {createTestAccount, type TestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {Config} from '@fluxer/api/src/Config';
|
||||
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
|
||||
export interface AttachmentMetadata {
|
||||
id: number;
|
||||
filename: string;
|
||||
title?: string;
|
||||
description?: string;
|
||||
flags?: number;
|
||||
}
|
||||
|
||||
export function createMultipartFormData(
|
||||
payload: Record<string, unknown>,
|
||||
files: Array<{index: number; filename: string; data: Buffer}>,
|
||||
): {body: Buffer; contentType: string} {
|
||||
const boundary = `----FormBoundary${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
const chunks: Array<Buffer> = [];
|
||||
|
||||
const payloadJson = JSON.stringify(payload);
|
||||
chunks.push(Buffer.from(`--${boundary}\r\n`));
|
||||
chunks.push(Buffer.from(`Content-Disposition: form-data; name="payload_json"\r\n`));
|
||||
chunks.push(Buffer.from(`Content-Type: application/json\r\n\r\n`));
|
||||
chunks.push(Buffer.from(`${payloadJson}\r\n`));
|
||||
|
||||
for (const file of files) {
|
||||
chunks.push(Buffer.from(`--${boundary}\r\n`));
|
||||
chunks.push(
|
||||
Buffer.from(`Content-Disposition: form-data; name="files[${file.index}]"; filename="${file.filename}"\r\n`),
|
||||
);
|
||||
chunks.push(Buffer.from(`Content-Type: application/octet-stream\r\n\r\n`));
|
||||
chunks.push(file.data);
|
||||
chunks.push(Buffer.from(`\r\n`));
|
||||
}
|
||||
|
||||
chunks.push(Buffer.from(`--${boundary}--\r\n`));
|
||||
|
||||
return {
|
||||
body: Buffer.concat(chunks),
|
||||
contentType: `multipart/form-data; boundary=${boundary}`,
|
||||
};
|
||||
}
|
||||
|
||||
export function loadFixture(filename: string): Buffer {
|
||||
const fixturesPath = join(import.meta.dirname, '..', '..', 'test', 'fixtures', filename);
|
||||
return readFileSync(fixturesPath);
|
||||
}
|
||||
|
||||
export async function createGuild(harness: ApiTestHarness, token: string, name: string): Promise<GuildResponse> {
|
||||
return createBuilder<GuildResponse>(harness, token).post('/guilds').body({name}).execute();
|
||||
}
|
||||
|
||||
export async function createChannel(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
name: string,
|
||||
type = 0,
|
||||
): Promise<ChannelResponse> {
|
||||
return createBuilder<ChannelResponse>(harness, token)
|
||||
.post(`/guilds/${guildId}/channels`)
|
||||
.body({name, type})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function sendMessageWithAttachments(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
payload: Record<string, unknown>,
|
||||
files: Array<{index: number; filename: string; data: Buffer}>,
|
||||
): Promise<{response: Response; text: string; json: MessageResponse}> {
|
||||
await ensureSessionStarted(harness, token);
|
||||
|
||||
const {body, contentType} = createMultipartFormData(payload, files);
|
||||
|
||||
const mergedHeaders = new Headers();
|
||||
mergedHeaders.set('Content-Type', contentType);
|
||||
mergedHeaders.set('Authorization', token);
|
||||
if (!mergedHeaders.has('x-forwarded-for')) {
|
||||
mergedHeaders.set('x-forwarded-for', '127.0.0.1');
|
||||
}
|
||||
|
||||
const response = await harness.app.request(`/channels/${channelId}/messages`, {
|
||||
method: 'POST',
|
||||
headers: mergedHeaders,
|
||||
body,
|
||||
});
|
||||
|
||||
const text = await response.text();
|
||||
let json: unknown = null;
|
||||
try {
|
||||
json = text.length > 0 ? (JSON.parse(text) as unknown) : null;
|
||||
} catch {
|
||||
json = null;
|
||||
}
|
||||
|
||||
return {response, text, json: json as MessageResponse};
|
||||
}
|
||||
|
||||
export async function getMessage(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
): Promise<{response: Response; json: MessageResponse}> {
|
||||
return createBuilder<MessageResponse>(harness, token)
|
||||
.get(`/channels/${channelId}/messages/${messageId}`)
|
||||
.executeWithResponse();
|
||||
}
|
||||
|
||||
export async function setupTestGuildAndChannel(
|
||||
harness: ApiTestHarness,
|
||||
account?: TestAccount,
|
||||
): Promise<{account: TestAccount; guild: GuildResponse; channel: ChannelResponse}> {
|
||||
const testAccount = account ?? (await createTestAccount(harness));
|
||||
|
||||
const guild = await createGuild(harness, testAccount.token, 'Test Guild');
|
||||
|
||||
const channel = await createChannel(harness, testAccount.token, guild.id, 'test-channel');
|
||||
|
||||
return {account: testAccount, guild, channel};
|
||||
}
|
||||
|
||||
export async function createTestAccountForAttachmentTests(harness: ApiTestHarness): Promise<TestAccount> {
|
||||
const account = await createTestAccount(harness);
|
||||
await ensureSessionStarted(harness, account.token);
|
||||
await createBuilder<{type: 'session'}>(harness, `Bearer ${Config.gateway.rpcSecret}`)
|
||||
.post('/_rpc')
|
||||
.body({
|
||||
type: 'session',
|
||||
token: account.token,
|
||||
version: 1,
|
||||
ip: '127.0.0.1',
|
||||
})
|
||||
.execute();
|
||||
return account;
|
||||
}
|
||||
@@ -0,0 +1,561 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannel,
|
||||
createGuild,
|
||||
loadFixture,
|
||||
sendMessageWithAttachments,
|
||||
} from '@fluxer/api/src/channel/tests/AttachmentTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Attachment Upload Validation', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
describe('File Validation', () => {
|
||||
describe('size limits', () => {
|
||||
it('should reject files exceeding size limits', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Too Large File Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const largeFileData = Buffer.alloc(26 * 1024 * 1024);
|
||||
|
||||
const payload = {
|
||||
content: 'Large file test',
|
||||
attachments: [{id: 0, filename: 'large.bin'}],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'large.bin', data: largeFileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('filename validation', () => {
|
||||
it('should reject empty filenames', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Empty Filename Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: 'Empty filename test',
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'actualname.txt', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should reject empty filename in metadata and upload', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Invalid Filename Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: 'Invalid filename test',
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: '',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: '', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).not.toBe(200);
|
||||
});
|
||||
|
||||
it('should reject excessively long filename', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Invalid Filename Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
const longFilename = 'a'.repeat(300);
|
||||
|
||||
const payload = {
|
||||
content: 'Invalid filename test',
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: longFilename,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: longFilename, data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).not.toBe(200);
|
||||
});
|
||||
|
||||
it('should use metadata filename when different from upload filename', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Filename Mismatch Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: 'Filename mismatch test',
|
||||
attachments: [{id: 0, filename: 'expected.txt'}],
|
||||
};
|
||||
|
||||
const {response, text, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'different.txt', data: fileData},
|
||||
]);
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log('Error response:', text);
|
||||
}
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
expect(json.attachments![0].filename).toBe('expected.txt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('special characters', () => {
|
||||
let channelId: string;
|
||||
let accountToken: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
accountToken = account.token;
|
||||
const guild = await createGuild(harness, account.token, 'Special Chars Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
channelId = guild.system_channel_id ?? channel.id;
|
||||
});
|
||||
|
||||
const testCases = [
|
||||
{filename: 'file<script>.txt', shouldSanitize: true, description: 'HTML tag in filename'},
|
||||
{filename: 'file>output.txt', shouldSanitize: true, description: 'redirect operator'},
|
||||
{filename: 'file|pipe.txt', shouldSanitize: true, description: 'pipe character'},
|
||||
{filename: 'file:colon.txt', shouldSanitize: true, description: 'colon (Windows reserved)'},
|
||||
{filename: 'file*star.txt', shouldSanitize: true, description: 'asterisk (wildcard)'},
|
||||
{filename: 'file?question.txt', shouldSanitize: true, description: 'question mark'},
|
||||
{filename: 'file"quote.txt', shouldSanitize: true, description: 'double quote'},
|
||||
{filename: 'COM1.txt', shouldSanitize: true, description: 'Windows reserved name'},
|
||||
{filename: 'LPT1.txt', shouldSanitize: true, description: 'Windows reserved name'},
|
||||
{filename: 'file with spaces.txt', shouldSanitize: false, description: 'spaces should be OK'},
|
||||
{
|
||||
filename: 'file-dash_underscore.txt',
|
||||
shouldSanitize: false,
|
||||
description: 'dash and underscore OK',
|
||||
},
|
||||
{filename: 'file.multiple.dots.txt', shouldSanitize: false, description: 'multiple dots OK'},
|
||||
{filename: 'файл.txt', shouldSanitize: false, description: 'unicode characters OK'},
|
||||
{filename: '文件.txt', shouldSanitize: false, description: 'CJK characters OK'},
|
||||
{filename: '😀.txt', shouldSanitize: false, description: 'emoji OK'},
|
||||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
it(`should handle ${tc.description}`, async () => {
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: `Special char test: ${tc.description}`,
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: tc.filename,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, accountToken, channelId, payload, [
|
||||
{index: 0, filename: 'test.txt', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
|
||||
const sanitized = json.attachments![0].filename;
|
||||
|
||||
if (tc.shouldSanitize) {
|
||||
const dangerousChars = ['<', '>', ':', '"', '|', '?', '*'];
|
||||
for (const char of dangerousChars) {
|
||||
expect(sanitized).not.toContain(char);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('path traversal', () => {
|
||||
const testCases = [
|
||||
{input: '../../../etc/passwd', expectedSuffix: 'passwd'},
|
||||
{input: '..\\\\..\\\\..\\\\windows\\\\system32\\\\config\\\\sam', expectedSuffix: 'sam'},
|
||||
{input: '....//....//....//etc/passwd', expectedSuffix: 'passwd'},
|
||||
{input: '..\\\\..\\\\..\\\\', expectedSuffix: ''},
|
||||
{input: '../../sensitive.txt', expectedSuffix: 'sensitive.txt'},
|
||||
{input: './../../etc/hosts', expectedSuffix: 'hosts'},
|
||||
];
|
||||
|
||||
for (const tc of testCases) {
|
||||
it(`should sanitize path traversal in filename: ${tc.input}`, async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Path Traversal Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: 'Path traversal test',
|
||||
attachments: [{id: 0, filename: tc.input}],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: tc.input, data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
|
||||
const sanitized = json.attachments![0].filename;
|
||||
expect(sanitized).not.toContain('..');
|
||||
expect(sanitized).not.toContain('/');
|
||||
expect(sanitized).not.toContain('\\');
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Metadata Validation', () => {
|
||||
describe('id matching', () => {
|
||||
it('should reject negative IDs in metadata', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Negative ID Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: 'Negative ID test',
|
||||
attachments: [{id: -1, filename: 'test.txt'}],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'test.txt', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should reject mismatched IDs between metadata and files', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'ID Mismatch Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = Buffer.from('test content');
|
||||
|
||||
const payload = {
|
||||
content: 'ID mismatch test',
|
||||
attachments: [{id: 5, filename: 'test.txt'}],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'test.txt', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should correctly match files with metadata when sent in natural order', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'ID Matching Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const file1Data = loadFixture('yeah.png');
|
||||
const file2Data = loadFixture('thisisfine.gif');
|
||||
|
||||
const payload = {
|
||||
content: 'Ordered files test',
|
||||
attachments: [
|
||||
{id: 0, filename: 'yeah.png', description: 'First file', title: 'First'},
|
||||
{id: 1, filename: 'thisisfine.gif', description: 'Second file', title: 'Second'},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: file1Data},
|
||||
{index: 1, filename: 'thisisfine.gif', data: file2Data},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(2);
|
||||
|
||||
expect(json.attachments![0].filename).toBe('yeah.png');
|
||||
expect(json.attachments![0].description).toBe('First file');
|
||||
expect(json.attachments![0].title).toBe('First');
|
||||
|
||||
expect(json.attachments![1].filename).toBe('thisisfine.gif');
|
||||
expect(json.attachments![1].description).toBe('Second file');
|
||||
expect(json.attachments![1].title).toBe('Second');
|
||||
});
|
||||
|
||||
it('should handle non-sequential IDs like 2, 5', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Sparse IDs Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const file1Data = loadFixture('yeah.png');
|
||||
const file2Data = loadFixture('thisisfine.gif');
|
||||
|
||||
const payload = {
|
||||
content: 'Sparse IDs test',
|
||||
attachments: [
|
||||
{id: 2, filename: 'yeah.png', description: 'ID is 2', title: 'Two'},
|
||||
{id: 5, filename: 'thisisfine.gif', description: 'ID is 5', title: 'Five'},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 2, filename: 'yeah.png', data: file1Data},
|
||||
{index: 5, filename: 'thisisfine.gif', data: file2Data},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(2);
|
||||
|
||||
expect(json.attachments![0].description).toBe('ID is 2');
|
||||
expect(json.attachments![0].title).toBe('Two');
|
||||
|
||||
expect(json.attachments![1].description).toBe('ID is 5');
|
||||
expect(json.attachments![1].title).toBe('Five');
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata requirements', () => {
|
||||
it('should reject file upload without attachment metadata', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Without Metadata Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'File without metadata',
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should reject attachment metadata without corresponding file', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Metadata Without File Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const payload = {
|
||||
content: 'Metadata without file',
|
||||
attachments: [{id: 0, filename: 'missing.png', description: 'This file does not exist'}],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, []);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('title and description', () => {
|
||||
it('should preserve title and description fields through upload flow', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Title Description Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Testing title and description',
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'yeah.png',
|
||||
title: 'My Awesome Title',
|
||||
description: 'This is a detailed description of the attachment with special chars: émoji 🎉',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
|
||||
const att = json.attachments![0];
|
||||
expect(att.title).toBe('My Awesome Title');
|
||||
|
||||
const expectedDesc = 'This is a detailed description of the attachment with special chars: émoji 🎉';
|
||||
expect(att.description).toBe(expectedDesc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Flag Preservation', () => {
|
||||
it('should preserve attachment flags through upload flow', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Flags Preserved Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Flags preservation test',
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'spoiler.png',
|
||||
flags: 8,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'spoiler.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
expect(json.attachments![0].flags).toBe(8);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple File Upload', () => {
|
||||
it('should handle multiple files with mixed metadata quality', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Mixed Metadata Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const file1Data = loadFixture('yeah.png');
|
||||
const file2Data = loadFixture('thisisfine.gif');
|
||||
|
||||
const payload = {
|
||||
content: 'Mixed metadata test',
|
||||
attachments: [
|
||||
{
|
||||
id: 0,
|
||||
filename: 'yeah.png',
|
||||
title: 'Full Metadata',
|
||||
description: 'Complete description',
|
||||
flags: 0,
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
filename: 'thisisfine.gif',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: file1Data},
|
||||
{index: 1, filename: 'thisisfine.gif', data: file2Data},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(2);
|
||||
|
||||
expect(json.attachments![0].title).toBe('Full Metadata');
|
||||
expect(json.attachments![0].description).toBe('Complete description');
|
||||
|
||||
expect(json.attachments![1].title).toBeNull();
|
||||
expect(json.attachments![1].description).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
116
packages/api/src/channel/tests/BulkDeleteMessages.test.tsx
Normal file
116
packages/api/src/channel/tests/BulkDeleteMessages.test.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createGuild} 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 {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
|
||||
|
||||
describe('Bulk Delete Messages', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('rejects empty message_ids array', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Validation Test Guild');
|
||||
|
||||
if (!guild.system_channel_id) {
|
||||
throw new Error('Guild should have a system channel');
|
||||
}
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/channels/${guild.system_channel_id}/messages/bulk-delete`)
|
||||
.body({message_ids: []})
|
||||
.expect(400)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects more than 100 messages', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Validation Test Guild');
|
||||
|
||||
if (!guild.system_channel_id) {
|
||||
throw new Error('Guild should have a system channel');
|
||||
}
|
||||
|
||||
const tooManyIds: Array<string> = [];
|
||||
for (let i = 0; i < 101; i++) {
|
||||
tooManyIds.push(`${1000000000000000000n + BigInt(i)}`);
|
||||
}
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/channels/${guild.system_channel_id}/messages/bulk-delete`)
|
||||
.body({message_ids: tooManyIds})
|
||||
.expect(400)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects bulk delete without MANAGE_MESSAGES permission when channel does not exist', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Permissions Test Guild');
|
||||
|
||||
if (!guild.system_channel_id) {
|
||||
throw new Error('Guild should have a system channel');
|
||||
}
|
||||
|
||||
const messageIds: Array<string> = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
messageIds.push(`${1000000000000000000n + BigInt(i)}`);
|
||||
}
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/channels/${guild.system_channel_id}/messages/bulk-delete`)
|
||||
.body({message_ids: messageIds})
|
||||
.expect(403)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('accepts bulk delete request with valid message IDs (does not require messages to exist)', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'Bulk Delete Test Guild');
|
||||
|
||||
if (!guild.system_channel_id) {
|
||||
throw new Error('Guild should have a system channel');
|
||||
}
|
||||
|
||||
const messageIds: Array<string> = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
messageIds.push(`${1000000000000000000n + BigInt(i)}`);
|
||||
}
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/channels/${guild.system_channel_id}/messages/bulk-delete`)
|
||||
.body({message_ids: messageIds})
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
188
packages/api/src/channel/tests/CallEndpoints.test.tsx
Normal file
188
packages/api/src/channel/tests/CallEndpoints.test.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createDmChannel, createGuild} 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';
|
||||
|
||||
describe('Call Endpoints', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
async function setupUsersWithMutualGuild() {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, user1.token, 'Test Guild');
|
||||
|
||||
const invite = await createBuilder<GuildInviteMetadataResponse>(harness, user1.token)
|
||||
.post(`/channels/${guild.system_channel_id}/invites`)
|
||||
.body({})
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, user2.token).post(`/invites/${invite.code}`).body(null).execute();
|
||||
|
||||
return {user1, user2};
|
||||
}
|
||||
|
||||
describe('GET /channels/:channel_id/call', () => {
|
||||
it('returns call eligibility for DM channel when voice is disabled', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
const callData = await createBuilder<{ringable: boolean}>(harness, user1.token)
|
||||
.get(`/channels/${dmChannel.id}/call`)
|
||||
.execute();
|
||||
|
||||
expect(callData).toHaveProperty('ringable');
|
||||
expect(typeof callData.ringable).toBe('boolean');
|
||||
});
|
||||
|
||||
it('returns call eligibility for non-existent channel', async () => {
|
||||
const user = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, user.token).get('/channels/999999999999999999/call').expect(404).execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /channels/:channel_id/call', () => {
|
||||
it('updates call region for DM channel', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.patch(`/channels/${dmChannel.id}/call`)
|
||||
.body({region: 'us-west'})
|
||||
.expect(404)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('returns error when updating non-existent call', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token).patch(`/channels/${dmChannel.id}/call`).body({}).expect(404).execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /channels/:channel_id/call/ring', () => {
|
||||
it('rings call recipients for DM channel', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post(`/channels/${dmChannel.id}/call/ring`)
|
||||
.body({recipients: [user2.userId]})
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('creates silent call without ringing when recipients is empty array (shift-click)', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post(`/channels/${dmChannel.id}/call/ring`)
|
||||
.body({recipients: []})
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rings call without specifying recipients for DM channel', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post(`/channels/${dmChannel.id}/call/ring`)
|
||||
.body({})
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects ringing for non-existent channel', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post('/channels/123456789/call/ring')
|
||||
.body({recipients: [user2.userId]})
|
||||
.expect(404)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /channels/:channel_id/call/stop-ringing', () => {
|
||||
it('returns error when stopping ringing for non-existent call', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post(`/channels/${dmChannel.id}/call/stop-ringing`)
|
||||
.body({recipients: [user2.userId]})
|
||||
.expect(404)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('returns error when stopping ringing all for non-existent call', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post(`/channels/${dmChannel.id}/call/stop-ringing`)
|
||||
.body({})
|
||||
.expect(404)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /channels/:channel_id/call/end', () => {
|
||||
it('ends call for DM channel', async () => {
|
||||
const {user1, user2} = await setupUsersWithMutualGuild();
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post(`/channels/${dmChannel.id}/call/end`)
|
||||
.body(null)
|
||||
.expect(204)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,137 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
acceptInvite,
|
||||
createChannel,
|
||||
createChannelInvite,
|
||||
createGuild,
|
||||
deleteChannel,
|
||||
getChannel,
|
||||
updateChannel,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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 {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Channel Operation Permissions', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('should allow member to get channel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Channel Perms Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
const channel = await getChannel(harness, member.token, systemChannel.id);
|
||||
|
||||
expect(channel.id).toBe(systemChannel.id);
|
||||
});
|
||||
|
||||
it('should reject nonmember from getting channel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const nonmember = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Channel Perms Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
await createBuilder(harness, nonmember.token)
|
||||
.get(`/channels/${systemChannel.id}`)
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should reject member from updating channel without MANAGE_CHANNELS', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Channel Perms Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.patch(`/channels/${systemChannel.id}`)
|
||||
.body({name: 'hacked'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should reject member from deleting channel without MANAGE_CHANNELS', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Channel Perms Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.delete(`/channels/${systemChannel.id}`)
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should allow owner to update channel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Channel Perms Guild');
|
||||
|
||||
const testChannel = await createChannel(harness, owner.token, guild.id, 'test-channel');
|
||||
|
||||
const updated = await updateChannel(harness, owner.token, testChannel.id, {name: 'owner-updated'});
|
||||
|
||||
expect(updated.name).toBe('owner-updated');
|
||||
});
|
||||
|
||||
it('should allow owner to delete channel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Channel Perms Guild');
|
||||
|
||||
const testChannel = await createChannel(harness, owner.token, guild.id, 'test-channel');
|
||||
|
||||
await deleteChannel(harness, owner.token, testChannel.id);
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.get(`/channels/${testChannel.id}`)
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannel,
|
||||
createGuild,
|
||||
deleteChannel,
|
||||
getChannel,
|
||||
updateChannel,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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 {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Channel Operation Validation', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('should reject getting nonexistent channel', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.get('/channels/999999999999999999')
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should reject updating nonexistent channel', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.patch('/channels/999999999999999999')
|
||||
.body({name: 'new-name'})
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should reject deleting nonexistent channel', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.delete('/channels/999999999999999999')
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should get channel successfully', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Channel Operation Guild');
|
||||
const channel = await getChannel(harness, account.token, guild.system_channel_id!);
|
||||
|
||||
expect(channel.id).toBe(guild.system_channel_id);
|
||||
});
|
||||
|
||||
it('should update channel name successfully', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Channel Operation Guild');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
const updated = await updateChannel(harness, account.token, channelId, {name: 'updated-name'});
|
||||
|
||||
expect(updated.name).toBe('updated-name');
|
||||
});
|
||||
|
||||
it('should update channel topic successfully', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Channel Operation Guild');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
const updated = await updateChannel(harness, account.token, channelId, {topic: 'New topic'});
|
||||
|
||||
expect(updated.topic).toBe('New topic');
|
||||
});
|
||||
|
||||
it('should delete channel successfully', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Channel Operation Guild');
|
||||
|
||||
const newChannel = await createChannel(harness, account.token, guild.id, 'to-delete');
|
||||
|
||||
await deleteChannel(harness, account.token, newChannel.id);
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.get(`/channels/${newChannel.id}`)
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,252 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannel,
|
||||
createGuild,
|
||||
createPermissionOverwrite,
|
||||
createRole,
|
||||
getChannel,
|
||||
setupTestGuildWithMembers,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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 {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
|
||||
|
||||
describe('Channel Permission Overwrites', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('should create permission overwrite for role', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
const role = await createRole(harness, account.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
const overwrite = await createPermissionOverwrite(harness, account.token, channel.id, role.id, {
|
||||
type: 0,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
expect(overwrite.id).toBe(role.id);
|
||||
expect(overwrite.type).toBe(0);
|
||||
});
|
||||
|
||||
test('should create permission overwrite for member', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
|
||||
const overwrite = await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: Permissions.VIEW_CHANNEL.toString(),
|
||||
deny: Permissions.SEND_MESSAGES.toString(),
|
||||
});
|
||||
|
||||
expect(overwrite.id).toBe(member.userId);
|
||||
expect(overwrite.type).toBe(1);
|
||||
});
|
||||
|
||||
test('should deny permission via overwrite', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: '0',
|
||||
deny: Permissions.SEND_MESSAGES.toString(),
|
||||
});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'Test message'})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should allow permission via overwrite', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'Test message'})
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should update existing permission overwrite', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
const role = await createRole(harness, account.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
await createPermissionOverwrite(harness, account.token, channel.id, role.id, {
|
||||
type: 0,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
const updated = await createPermissionOverwrite(harness, account.token, channel.id, role.id, {
|
||||
type: 0,
|
||||
allow: (Permissions.SEND_MESSAGES | Permissions.EMBED_LINKS).toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
expect(BigInt(updated.allow)).toBe(Permissions.SEND_MESSAGES | Permissions.EMBED_LINKS);
|
||||
});
|
||||
|
||||
test('should delete permission overwrite', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
const role = await createRole(harness, account.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
await createPermissionOverwrite(harness, account.token, channel.id, role.id, {
|
||||
type: 0,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.delete(`/channels/${channel.id}/permissions/${role.id}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
const channelData = await getChannel(harness, account.token, channel.id);
|
||||
const overwrite = channelData.permission_overwrites?.find((o) => o.id === role.id);
|
||||
expect(overwrite).toBeUndefined();
|
||||
});
|
||||
|
||||
test('should require MANAGE_ROLES to create overwrites', async () => {
|
||||
const {owner, members, guild, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
const role = await createRole(harness, owner.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.put(`/channels/${systemChannel.id}/permissions/${role.id}`)
|
||||
.body({
|
||||
type: 0,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should show overwrites in channel response', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
const role = await createRole(harness, account.token, guild.id, {name: 'Test Role'});
|
||||
|
||||
await createPermissionOverwrite(harness, account.token, channel.id, role.id, {
|
||||
type: 0,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
const channelData = await getChannel(harness, account.token, channel.id);
|
||||
expect(channelData.permission_overwrites).toBeDefined();
|
||||
expect(channelData.permission_overwrites?.some((o) => o.id === role.id)).toBe(true);
|
||||
});
|
||||
|
||||
test('should prioritize member overwrite over role overwrite', async () => {
|
||||
const {owner, members, guild, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
const role = await createRole(harness, owner.token, guild.id, {name: 'Deny Role'});
|
||||
|
||||
await createBuilder<void>(harness, owner.token)
|
||||
.put(`/guilds/${guild.id}/members/${member.userId}/roles/${role.id}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, role.id, {
|
||||
type: 0,
|
||||
allow: '0',
|
||||
deny: Permissions.SEND_MESSAGES.toString(),
|
||||
});
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/messages`)
|
||||
.body({content: 'Test message'})
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should reject invalid overwrite type', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.put(`/channels/${channel.id}/permissions/123456789`)
|
||||
.body({
|
||||
type: 999,
|
||||
allow: '0',
|
||||
deny: '0',
|
||||
})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should handle multiple overlapping role overwrites', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const role1 = await createRole(harness, account.token, guild.id, {name: 'Role 1'});
|
||||
const role2 = await createRole(harness, account.token, guild.id, {name: 'Role 2'});
|
||||
|
||||
await createPermissionOverwrite(harness, account.token, channel.id, role1.id, {
|
||||
type: 0,
|
||||
allow: Permissions.SEND_MESSAGES.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
await createPermissionOverwrite(harness, account.token, channel.id, role2.id, {
|
||||
type: 0,
|
||||
allow: Permissions.EMBED_LINKS.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
const channelData = await getChannel(harness, account.token, channel.id);
|
||||
expect(channelData.permission_overwrites?.length).toBeGreaterThanOrEqual(2);
|
||||
});
|
||||
});
|
||||
415
packages/api/src/channel/tests/ChannelTestUtils.tsx
Normal file
415
packages/api/src/channel/tests/ChannelTestUtils.tsx
Normal file
@@ -0,0 +1,415 @@
|
||||
/*
|
||||
* 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, type TestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import type {ChannelOverwriteResponse, ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import type {GuildMemberResponse} from '@fluxer/schema/src/domains/guild/GuildMemberSchemas';
|
||||
import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas';
|
||||
import type {GuildRoleResponse} from '@fluxer/schema/src/domains/guild/GuildRoleSchemas';
|
||||
import type {GuildInviteMetadataResponse} from '@fluxer/schema/src/domains/invite/InviteSchemas';
|
||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
|
||||
export async function createGuild(harness: ApiTestHarness, token: string, name: string): Promise<GuildResponse> {
|
||||
return createBuilder<GuildResponse>(harness, token).post('/guilds').body({name}).execute();
|
||||
}
|
||||
|
||||
export async function createChannel(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
name: string,
|
||||
type = 0,
|
||||
): Promise<ChannelResponse> {
|
||||
return createBuilder<ChannelResponse>(harness, token)
|
||||
.post(`/guilds/${guildId}/channels`)
|
||||
.body({name, type})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function getChannel(harness: ApiTestHarness, token: string, channelId: string): Promise<ChannelResponse> {
|
||||
return createBuilder<ChannelResponse>(harness, token).get(`/channels/${channelId}`).execute();
|
||||
}
|
||||
|
||||
export async function updateChannel(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
updates: Partial<
|
||||
Pick<ChannelResponse, 'name' | 'type' | 'topic' | 'parent_id' | 'position' | 'rate_limit_per_user' | 'nsfw'>
|
||||
>,
|
||||
): Promise<ChannelResponse> {
|
||||
return createBuilder<ChannelResponse>(harness, token).patch(`/channels/${channelId}`).body(updates).execute();
|
||||
}
|
||||
|
||||
export async function deleteChannel(harness: ApiTestHarness, token: string, channelId: string): Promise<void> {
|
||||
return createBuilder<void>(harness, token).delete(`/channels/${channelId}`).expect(204).execute();
|
||||
}
|
||||
|
||||
export async function createChannelInvite(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
): Promise<GuildInviteMetadataResponse> {
|
||||
return createBuilder<GuildInviteMetadataResponse>(harness, token)
|
||||
.post(`/channels/${channelId}/invites`)
|
||||
.body({})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function acceptInvite(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
inviteCode: string,
|
||||
): Promise<{guild: GuildResponse}> {
|
||||
return createBuilder<{guild: GuildResponse}>(harness, token).post(`/invites/${inviteCode}`).body(null).execute();
|
||||
}
|
||||
|
||||
export async function createRole(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
role: Partial<Omit<GuildRoleResponse, 'id' | 'position'>>,
|
||||
): Promise<GuildRoleResponse> {
|
||||
return createBuilder<GuildRoleResponse>(harness, token).post(`/guilds/${guildId}/roles`).body(role).execute();
|
||||
}
|
||||
|
||||
export async function updateRole(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
roleId: string,
|
||||
updates: Partial<GuildRoleResponse>,
|
||||
): Promise<GuildRoleResponse> {
|
||||
return createBuilder<GuildRoleResponse>(harness, token)
|
||||
.patch(`/guilds/${guildId}/roles/${roleId}`)
|
||||
.body(updates)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function deleteRole(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
roleId: string,
|
||||
): Promise<void> {
|
||||
return createBuilder<void>(harness, token).delete(`/guilds/${guildId}/roles/${roleId}`).expect(204).execute();
|
||||
}
|
||||
|
||||
export async function addMemberRole(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
roleId: string,
|
||||
): Promise<void> {
|
||||
return createBuilder<void>(harness, token)
|
||||
.put(`/guilds/${guildId}/members/${userId}/roles/${roleId}`)
|
||||
.expect(204)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function removeMemberRole(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
roleId: string,
|
||||
): Promise<void> {
|
||||
return createBuilder<void>(harness, token)
|
||||
.delete(`/guilds/${guildId}/members/${userId}/roles/${roleId}`)
|
||||
.expect(204)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function updateMember(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
updates: {roles?: Array<string>; nick?: string},
|
||||
): Promise<GuildMemberResponse> {
|
||||
return createBuilder<GuildMemberResponse>(harness, token)
|
||||
.patch(`/guilds/${guildId}/members/${userId}`)
|
||||
.body(updates)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function getMember(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
): Promise<GuildMemberResponse> {
|
||||
return createBuilder<GuildMemberResponse>(harness, token).get(`/guilds/${guildId}/members/${userId}`).execute();
|
||||
}
|
||||
|
||||
export async function getGuild(harness: ApiTestHarness, token: string, guildId: string): Promise<GuildResponse> {
|
||||
return createBuilder<GuildResponse>(harness, token).get(`/guilds/${guildId}`).execute();
|
||||
}
|
||||
|
||||
export async function updateGuild(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
updates: Partial<GuildResponse>,
|
||||
): Promise<GuildResponse> {
|
||||
return createBuilder<GuildResponse>(harness, token).patch(`/guilds/${guildId}`).body(updates).execute();
|
||||
}
|
||||
|
||||
export async function leaveGuild(harness: ApiTestHarness, token: string, guildId: string): Promise<void> {
|
||||
return createBuilder<void>(harness, token).delete(`/users/@me/guilds/${guildId}`).expect(204).execute();
|
||||
}
|
||||
|
||||
export async function getUserGuilds(harness: ApiTestHarness, token: string): Promise<Array<GuildResponse>> {
|
||||
return createBuilder<Array<GuildResponse>>(harness, token).get('/users/@me/guilds').execute();
|
||||
}
|
||||
|
||||
export async function createPermissionOverwrite(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
overwriteId: string,
|
||||
overwrite: Omit<ChannelOverwriteResponse, 'id'>,
|
||||
): Promise<ChannelOverwriteResponse> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.put(`/channels/${channelId}/permissions/${overwriteId}`)
|
||||
.body({
|
||||
type: overwrite.type,
|
||||
allow: overwrite.allow,
|
||||
deny: overwrite.deny,
|
||||
})
|
||||
.expect(204)
|
||||
.execute();
|
||||
|
||||
return {
|
||||
id: overwriteId,
|
||||
type: overwrite.type,
|
||||
allow: overwrite.allow,
|
||||
deny: overwrite.deny,
|
||||
};
|
||||
}
|
||||
|
||||
export async function setupTestGuildWithChannels(
|
||||
harness: ApiTestHarness,
|
||||
account?: TestAccount,
|
||||
): Promise<{
|
||||
account: TestAccount;
|
||||
guild: GuildResponse;
|
||||
systemChannel: ChannelResponse;
|
||||
}> {
|
||||
const testAccount = account ?? (await createTestAccount(harness));
|
||||
|
||||
const guild = await createGuild(harness, testAccount.token, 'Test Guild');
|
||||
|
||||
const systemChannel = await getChannel(harness, testAccount.token, guild.system_channel_id!);
|
||||
|
||||
return {account: testAccount, guild, systemChannel};
|
||||
}
|
||||
|
||||
export async function setupTestGuildWithMembers(
|
||||
harness: ApiTestHarness,
|
||||
memberCount = 2,
|
||||
): Promise<{
|
||||
owner: TestAccount;
|
||||
members: Array<TestAccount>;
|
||||
guild: GuildResponse;
|
||||
systemChannel: ChannelResponse;
|
||||
}> {
|
||||
const owner = await createTestAccount(harness);
|
||||
const members: Array<TestAccount> = [];
|
||||
|
||||
for (let i = 0; i < memberCount; i++) {
|
||||
members.push(await createTestAccount(harness));
|
||||
}
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
const systemChannel = await getChannel(harness, owner.token, guild.system_channel_id!);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, systemChannel.id);
|
||||
|
||||
for (const member of members) {
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
}
|
||||
|
||||
return {owner, members, guild, systemChannel};
|
||||
}
|
||||
|
||||
export interface MinimalChannelResponse {
|
||||
id: string;
|
||||
type: number;
|
||||
recipients?: Array<{id: string; username: string}>;
|
||||
}
|
||||
|
||||
export async function createFriendship(harness: ApiTestHarness, user1: TestAccount, user2: TestAccount): Promise<void> {
|
||||
await createBuilder<unknown>(harness, user1.token)
|
||||
.post(`/users/@me/relationships/${user2.userId}`)
|
||||
.body(null)
|
||||
.execute();
|
||||
|
||||
await createBuilder<unknown>(harness, user2.token).put(`/users/@me/relationships/${user1.userId}`).body({}).execute();
|
||||
}
|
||||
|
||||
export async function createDmChannel(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
recipientId: string,
|
||||
): Promise<MinimalChannelResponse> {
|
||||
const channel = await createBuilder<MinimalChannelResponse>(harness, token)
|
||||
.post('/users/@me/channels')
|
||||
.body({
|
||||
recipient_id: recipientId,
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (!channel.id) {
|
||||
throw new Error('DM channel response missing id');
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
export async function sendChannelMessage(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
content: string,
|
||||
): Promise<MessageResponse> {
|
||||
await ensureSessionStarted(harness, token);
|
||||
|
||||
const msg = await createBuilder<MessageResponse>(harness, token)
|
||||
.post(`/channels/${channelId}/messages`)
|
||||
.body({
|
||||
content,
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (!msg.id) {
|
||||
throw new Error('Message response missing id');
|
||||
}
|
||||
return msg;
|
||||
}
|
||||
|
||||
export async function blockUser(harness: ApiTestHarness, user: TestAccount, targetUserId: string): Promise<void> {
|
||||
await createBuilder<unknown>(harness, user.token)
|
||||
.put(`/users/@me/relationships/${targetUserId}`)
|
||||
.body({
|
||||
type: 2,
|
||||
})
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function initiateCall(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
recipients: Array<string>,
|
||||
expectedStatus: number = 204,
|
||||
): Promise<{response: Response; json: unknown}> {
|
||||
return createBuilder(harness, token)
|
||||
.post(`/channels/${channelId}/call/ring`)
|
||||
.body({recipients})
|
||||
.expect(expectedStatus)
|
||||
.executeWithResponse();
|
||||
}
|
||||
|
||||
export async function pinMessage(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
messageId: string,
|
||||
expectedStatus: number = 204,
|
||||
): Promise<{response: Response; json: unknown}> {
|
||||
return createBuilder(harness, token)
|
||||
.put(`/channels/${channelId}/pins/${messageId}`)
|
||||
.body(null)
|
||||
.expect(expectedStatus)
|
||||
.executeWithResponse();
|
||||
}
|
||||
|
||||
export interface GroupDmChannelResponse {
|
||||
id: string;
|
||||
type: number;
|
||||
name: string | null;
|
||||
owner_id: string;
|
||||
recipients: Array<{id: string; username: string}>;
|
||||
}
|
||||
|
||||
export async function createGroupDmChannel(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
recipientUserIds: Array<string>,
|
||||
): Promise<GroupDmChannelResponse> {
|
||||
const channel = await createBuilder<GroupDmChannelResponse>(harness, token)
|
||||
.post('/users/@me/channels')
|
||||
.body({
|
||||
recipients: recipientUserIds,
|
||||
})
|
||||
.execute();
|
||||
|
||||
if (!channel.id) {
|
||||
throw new Error('Group DM channel response missing id');
|
||||
}
|
||||
return channel;
|
||||
}
|
||||
|
||||
export async function addRecipientToGroupDm(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder(harness, token)
|
||||
.put(`/channels/${channelId}/recipients/${userId}`)
|
||||
.body(null)
|
||||
.expect(204)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function removeRecipientFromGroupDm(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder(harness, token).delete(`/channels/${channelId}/recipients/${userId}`).expect(204).execute();
|
||||
}
|
||||
|
||||
export interface SeedPrivateChannelsResult {
|
||||
group_dms: Array<{channel_id: string; last_message_id: string}>;
|
||||
dms: Array<{channel_id: string; last_message_id: string}>;
|
||||
}
|
||||
|
||||
export async function seedPrivateChannels(
|
||||
harness: ApiTestHarness,
|
||||
_token: string,
|
||||
userId: string,
|
||||
params: {
|
||||
group_dm_count?: number;
|
||||
dm_count?: number;
|
||||
recipients?: Array<string>;
|
||||
clear_existing?: boolean;
|
||||
},
|
||||
): Promise<SeedPrivateChannelsResult> {
|
||||
return createBuilder<SeedPrivateChannelsResult>(harness, '')
|
||||
.post(`/test/users/${userId}/private-channels`)
|
||||
.body(params)
|
||||
.execute();
|
||||
}
|
||||
188
packages/api/src/channel/tests/DMBlockingBehaviors.test.tsx
Normal file
188
packages/api/src/channel/tests/DMBlockingBehaviors.test.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
acceptInvite,
|
||||
blockUser,
|
||||
createChannelInvite,
|
||||
createDmChannel,
|
||||
createFriendship,
|
||||
createGuild,
|
||||
getChannel,
|
||||
initiateCall,
|
||||
pinMessage,
|
||||
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 {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('DM Blocking Behaviors', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
describe('DM Creation Blocking', () => {
|
||||
it('prevents DM creation when the other user has blocked you, even with mutual guild', 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, user2.token)
|
||||
.post('/users/@me/channels')
|
||||
.body({recipient_id: user1.userId})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('prevents DM creation with someone you have blocked, even with mutual guild', 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)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Voice Call Blocking', () => {
|
||||
it('prevents the user who blocked someone from initiating calls in the DM', 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 blockUser(harness, user1, user2.userId);
|
||||
|
||||
const {response, json} = await initiateCall(harness, user1.token, channel.id, [user2.userId], 400);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect((json as {code: string}).code).toBe('CANNOT_SEND_MESSAGES_TO_USER');
|
||||
});
|
||||
|
||||
it('prevents calls in a DM after one user blocks the other', 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 blockUser(harness, user1, user2.userId);
|
||||
|
||||
const {response, json} = await initiateCall(harness, user2.token, channel.id, [user1.userId], 400);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect((json as {code: string}).code).toBe('CANNOT_SEND_MESSAGES_TO_USER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pin Operation Blocking', () => {
|
||||
it('prevents the user who blocked someone from pinning messages in the DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const channel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
const msg = await sendChannelMessage(harness, user2.token, channel.id, 'message to pin');
|
||||
|
||||
await blockUser(harness, user1, user2.userId);
|
||||
|
||||
const {response, json} = await pinMessage(harness, user1.token, channel.id, msg.id, 400);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect((json as {code: string}).code).toBe('CANNOT_SEND_MESSAGES_TO_USER');
|
||||
});
|
||||
|
||||
it('prevents pinning messages in a DM after one user blocks the other', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const channel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
const msg = await sendChannelMessage(harness, user1.token, channel.id, 'message to pin');
|
||||
|
||||
await blockUser(harness, user1, user2.userId);
|
||||
|
||||
const {response, json} = await pinMessage(harness, user2.token, channel.id, msg.id, 400);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
expect((json as {code: string}).code).toBe('CANNOT_SEND_MESSAGES_TO_USER');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Message Blocking', () => {
|
||||
it('prevents messages in a DM after one user blocks the other', 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 blockUser(harness, user1, user2.userId);
|
||||
|
||||
await createBuilder(harness, user2.token)
|
||||
.post(`/channels/${channel.id}/messages`)
|
||||
.body({content: 'hello'})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, 'CANNOT_SEND_MESSAGES_TO_USER')
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
});
|
||||
74
packages/api/src/channel/tests/DMChannelManagement.test.tsx
Normal file
74
packages/api/src/channel/tests/DMChannelManagement.test.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createDmChannel, createFriendship, deleteChannel} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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, expect, it} from 'vitest';
|
||||
|
||||
describe('DM channel management', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('can create DM channel', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dm = await createDmChannel(harness, user1.token, user2.userId);
|
||||
expect(dm.id).toBeTruthy();
|
||||
expect(dm.id.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('can get DM channel', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dm = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user1.token).get(`/channels/${dm.id}`).expect(HTTP_STATUS.OK).execute();
|
||||
});
|
||||
|
||||
it('can close DM channel', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dm = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await deleteChannel(harness, user1.token, dm.id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createDmChannel, createFriendship} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('DM creation allowed with friendship', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('allows friends to create DMs with each other', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dm = await createDmChannel(harness, user1.token, user2.userId);
|
||||
expect(dm.id).toBeTruthy();
|
||||
expect(dm.id.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
acceptInvite,
|
||||
createChannelInvite,
|
||||
createDmChannel,
|
||||
createGuild,
|
||||
getChannel,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('DM creation allowed with mutual guild', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('allows users sharing a guild to create DMs without being friends', 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);
|
||||
|
||||
const dm = await createDmChannel(harness, user1.token, user2.userId);
|
||||
expect(dm.id).toBeTruthy();
|
||||
expect(dm.id.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {afterAll, beforeAll, beforeEach, describe, it} from 'vitest';
|
||||
|
||||
describe('DM creation requires friendship or mutual guild', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('prevents DM creation with strangers who you do not share a guild with and are not friends with', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.post('/users/@me/channels')
|
||||
.body({recipient_id: user2.userId})
|
||||
.expect(400)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,845 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannel,
|
||||
createGuild,
|
||||
loadFixture,
|
||||
sendMessageWithAttachments,
|
||||
} from '@fluxer/api/src/channel/tests/AttachmentTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {MessageAttachmentFlags} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Embed Attachment URL Resolution', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
describe('Basic URL Resolution', () => {
|
||||
it('should resolve attachment:// URLs in embed image field', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Embed Attachment Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with embed',
|
||||
attachments: [{id: 0, filename: 'yeah.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Test Embed',
|
||||
description: 'This embed uses an attached image',
|
||||
image: {url: 'attachment://yeah.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.content).toBe('Test message with embed');
|
||||
expect(json.embeds).toBeDefined();
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.title).toBe('Test Embed');
|
||||
expect(embed.image?.url).toBeTruthy();
|
||||
expect(embed.image?.url).not.toContain('attachment://');
|
||||
});
|
||||
|
||||
it('should resolve attachment:// URLs in embed thumbnail field', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Thumbnail Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Test with thumbnail',
|
||||
attachments: [{id: 0, filename: 'yeah.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Thumbnail Test',
|
||||
description: 'This embed uses a thumbnail',
|
||||
thumbnail: {url: 'attachment://yeah.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toBeDefined();
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.title).toBe('Thumbnail Test');
|
||||
expect(embed.thumbnail?.url).toBeTruthy();
|
||||
expect(embed.thumbnail?.url).not.toContain('attachment://');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Image and Thumbnail Fields', () => {
|
||||
it('should handle single embed using attachment:// for both image and thumbnail', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Both Fields Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const file1Data = loadFixture('yeah.png');
|
||||
const file2Data = loadFixture('thisisfine.gif');
|
||||
|
||||
const payload = {
|
||||
content: 'Both image and thumbnail',
|
||||
attachments: [
|
||||
{id: 0, filename: 'yeah.png'},
|
||||
{id: 1, filename: 'thisisfine.gif'},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Complete Embed',
|
||||
description: 'This embed uses both image and thumbnail',
|
||||
image: {url: 'attachment://thisisfine.gif'},
|
||||
thumbnail: {url: 'attachment://yeah.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: file1Data},
|
||||
{index: 1, filename: 'thisisfine.gif', data: file2Data},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toBeDefined();
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.image?.url).toBeTruthy();
|
||||
expect(embed.thumbnail?.url).toBeTruthy();
|
||||
expect(embed.image?.url).not.toContain('attachment://');
|
||||
expect(embed.thumbnail?.url).not.toContain('attachment://');
|
||||
});
|
||||
|
||||
it('should handle image and thumbnail in same embed from different files', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Image And Thumbnail Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Image and thumbnail from different attachments',
|
||||
attachments: [
|
||||
{id: 0, filename: 'main-image.png'},
|
||||
{id: 1, filename: 'thumb-image.png'},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Dual Image Embed',
|
||||
image: {url: 'attachment://main-image.png'},
|
||||
thumbnail: {url: 'attachment://thumb-image.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'main-image.png', data: fileData},
|
||||
{index: 1, filename: 'thumb-image.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(2);
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.image?.url).not.toContain('attachment://');
|
||||
expect(embed.thumbnail?.url).not.toContain('attachment://');
|
||||
expect(embed.image?.url).not.toBe(embed.thumbnail?.url);
|
||||
});
|
||||
|
||||
it('should handle mixed attachment:// and https:// URLs in embeds', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Mixed URLs Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with mixed embed URLs',
|
||||
attachments: [{id: 0, filename: 'local-image.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Mixed URL Embed',
|
||||
description: 'This embed uses both attachment and external URLs',
|
||||
image: {url: 'attachment://local-image.png'},
|
||||
thumbnail: {url: 'https://example.com/external-image.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'local-image.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toBeDefined();
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.image?.url).toBeTruthy();
|
||||
expect(embed.image?.url).not.toContain('attachment://');
|
||||
expect(embed.thumbnail?.url).toBe('https://example.com/external-image.png');
|
||||
});
|
||||
|
||||
it('should resolve attachment:// URL while preserving external thumbnail URL', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Preserve External URL Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Test with external thumbnail',
|
||||
attachments: [{id: 0, filename: 'attached.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'External Thumbnail Test',
|
||||
image: {url: 'attachment://attached.png'},
|
||||
thumbnail: {url: 'https://cdn.example.com/thumb.jpg'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'attached.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.image?.url).not.toContain('attachment://');
|
||||
expect(embed.thumbnail?.url).toBe('https://cdn.example.com/thumb.jpg');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Filename Matching', () => {
|
||||
it('should require exact filename matching', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Filename Matching Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload1 = {
|
||||
content: 'Case-sensitive filename test',
|
||||
attachments: [{id: 0, filename: 'yeah.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Exact Match Required',
|
||||
image: {url: 'attachment://yeah.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response: response1} = await sendMessageWithAttachments(harness, account.token, channelId, payload1, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response1.status).toBe(200);
|
||||
|
||||
const payload2 = {
|
||||
content: 'Case mismatch test',
|
||||
attachments: [{id: 0, filename: 'yeah.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Wrong Case',
|
||||
image: {url: 'attachment://Yeah.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response: response2} = await sendMessageWithAttachments(harness, account.token, channelId, payload2, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response2.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should match correct file by filename', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Filename Match Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Testing filename matching',
|
||||
attachments: [
|
||||
{id: 0, filename: 'alpha.png'},
|
||||
{id: 1, filename: 'beta.png'},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Beta Image',
|
||||
image: {url: 'attachment://beta.png'},
|
||||
},
|
||||
{
|
||||
title: 'Alpha Image',
|
||||
image: {url: 'attachment://alpha.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'alpha.png', data: fileData},
|
||||
{index: 1, filename: 'beta.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toHaveLength(2);
|
||||
expect(json.embeds).toHaveLength(2);
|
||||
|
||||
expect(json.embeds![0].title).toBe('Beta Image');
|
||||
expect(json.embeds![0].image?.url).not.toContain('attachment://');
|
||||
|
||||
expect(json.embeds![1].title).toBe('Alpha Image');
|
||||
expect(json.embeds![1].image?.url).not.toContain('attachment://');
|
||||
});
|
||||
|
||||
it('should resolve spoiler attachment in embed', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Spoiler Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with spoiler attachment in embed',
|
||||
attachments: [{id: 0, filename: 'SPOILER_secret.png', flags: MessageAttachmentFlags.IS_SPOILER}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Spoiler Embed',
|
||||
description: 'This embed uses a spoiler attachment',
|
||||
image: {url: 'attachment://SPOILER_secret.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'SPOILER_secret.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
expect(json.attachments![0].filename).toBe('SPOILER_secret.png');
|
||||
|
||||
expect(json.embeds).toBeDefined();
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
|
||||
const embed = json.embeds![0];
|
||||
expect(embed.image?.url).toBeTruthy();
|
||||
expect(embed.image?.url).not.toContain('attachment://');
|
||||
});
|
||||
|
||||
it('should preserve spoiler flag on attachment when referenced by embed', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Spoiler Flag Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Spoiler flag preservation test',
|
||||
attachments: [{id: 0, filename: 'SPOILER_hidden.png', flags: MessageAttachmentFlags.IS_SPOILER}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Hidden Content',
|
||||
image: {url: 'attachment://SPOILER_hidden.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'SPOILER_hidden.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(1);
|
||||
|
||||
const attachment = json.attachments![0];
|
||||
expect(attachment.flags).toBeDefined();
|
||||
expect(attachment.flags! & MessageAttachmentFlags.IS_SPOILER).toBe(MessageAttachmentFlags.IS_SPOILER);
|
||||
});
|
||||
|
||||
it('should handle spoiler attachment alongside non-spoiler in embed', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Mixed Spoiler Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Mixed spoiler and non-spoiler attachments',
|
||||
attachments: [
|
||||
{id: 0, filename: 'SPOILER_hidden.png', flags: MessageAttachmentFlags.IS_SPOILER},
|
||||
{id: 1, filename: 'visible.png'},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Spoiler Image',
|
||||
image: {url: 'attachment://SPOILER_hidden.png'},
|
||||
},
|
||||
{
|
||||
title: 'Visible Image',
|
||||
thumbnail: {url: 'attachment://visible.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'SPOILER_hidden.png', data: fileData},
|
||||
{index: 1, filename: 'visible.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toBeDefined();
|
||||
expect(json.attachments).not.toBeNull();
|
||||
expect(json.attachments!).toHaveLength(2);
|
||||
|
||||
const spoilerAttachment = json.attachments!.find((a) => a.filename === 'SPOILER_hidden.png');
|
||||
const visibleAttachment = json.attachments!.find((a) => a.filename === 'visible.png');
|
||||
|
||||
expect(spoilerAttachment).toBeDefined();
|
||||
expect(spoilerAttachment!.flags! & MessageAttachmentFlags.IS_SPOILER).toBe(MessageAttachmentFlags.IS_SPOILER);
|
||||
|
||||
expect(visibleAttachment).toBeDefined();
|
||||
expect(visibleAttachment!.flags ?? 0).not.toBe(MessageAttachmentFlags.IS_SPOILER);
|
||||
|
||||
expect(json.embeds).toHaveLength(2);
|
||||
expect(json.embeds![0].image?.url).not.toContain('attachment://');
|
||||
expect(json.embeds![1].thumbnail?.url).not.toContain('attachment://');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should reject embed with attachment:// URL when no files are uploaded', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'No Attachments Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const payload = {
|
||||
content: 'Embed references attachment but none provided',
|
||||
embeds: [
|
||||
{
|
||||
title: 'Invalid Reference',
|
||||
description: 'No attachment uploaded',
|
||||
image: {url: 'attachment://image.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.post(`/channels/${channelId}/messages`)
|
||||
.body(payload)
|
||||
.expect(400)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('should reject embed referencing non-existent filename', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Missing Attachment Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Embed references missing file',
|
||||
attachments: [{id: 0, filename: 'yeah.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Invalid Reference',
|
||||
description: 'This embed references a non-existent file',
|
||||
image: {url: 'attachment://nonexistent.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Validation Rules', () => {
|
||||
it('should reject non-image attachment in embed image field', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Non-Image Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const textFileData = Buffer.from('This is a text file, not an image.');
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with non-image in embed',
|
||||
attachments: [{id: 0, filename: 'document.txt'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Invalid Embed',
|
||||
image: {url: 'attachment://document.txt'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'document.txt', data: textFileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should reject PDF attachment in embed image field', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'PDF Rejection Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const pdfHeader = Buffer.from('%PDF-1.4\n');
|
||||
const pdfData = Buffer.concat([pdfHeader, Buffer.alloc(100)]);
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with PDF in embed',
|
||||
attachments: [{id: 0, filename: 'document.pdf'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'PDF Embed Attempt',
|
||||
image: {url: 'attachment://document.pdf'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'document.pdf', data: pdfData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should reject JSON attachment in embed thumbnail field', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'JSON Rejection Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const jsonData = Buffer.from(JSON.stringify({key: 'value'}));
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with JSON in embed thumbnail',
|
||||
attachments: [{id: 0, filename: 'data.json'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'JSON Embed Attempt',
|
||||
thumbnail: {url: 'attachment://data.json'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'data.json', data: jsonData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should reject executable attachment in embed image field', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Executable Rejection Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const exeData = Buffer.from('MZ');
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with executable in embed',
|
||||
attachments: [{id: 0, filename: 'program.exe'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Executable Embed Attempt',
|
||||
image: {url: 'attachment://program.exe'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'program.exe', data: exeData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(400);
|
||||
});
|
||||
|
||||
it('should accept file with image extension but corrupted content for embed reference', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Content Type Mismatch Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const textData = Buffer.from('Not an image, just text.');
|
||||
|
||||
const payload = {
|
||||
content: 'Test with fake PNG extension',
|
||||
attachments: [{id: 0, filename: 'fake.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'Fake Image Embed',
|
||||
image: {url: 'attachment://fake.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'fake.png', data: textData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toHaveLength(1);
|
||||
expect(json.embeds![0].image?.url).not.toContain('attachment://');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple Embeds and Files', () => {
|
||||
it('should handle multiple embeds with different URL types', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Multiple Embeds Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Multiple embeds with different URL types',
|
||||
attachments: [{id: 0, filename: 'image1.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'First Embed - Attachment',
|
||||
image: {url: 'attachment://image1.png'},
|
||||
},
|
||||
{
|
||||
title: 'Second Embed - External',
|
||||
image: {url: 'https://example.com/external.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'image1.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toHaveLength(2);
|
||||
|
||||
expect(json.embeds![0].image?.url).not.toContain('attachment://');
|
||||
expect(json.embeds![1].image?.url).toBe('https://example.com/external.png');
|
||||
});
|
||||
|
||||
it('should handle multiple embeds each referencing different attachments', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Multiple Embeds Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const file1Data = loadFixture('yeah.png');
|
||||
const file2Data = loadFixture('thisisfine.gif');
|
||||
|
||||
const payload = {
|
||||
content: 'Multiple embeds with different attachments',
|
||||
attachments: [
|
||||
{id: 0, filename: 'yeah.png'},
|
||||
{id: 1, filename: 'thisisfine.gif'},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
title: 'First Embed',
|
||||
description: 'Uses PNG',
|
||||
image: {url: 'attachment://yeah.png'},
|
||||
},
|
||||
{
|
||||
title: 'Second Embed',
|
||||
description: 'Uses GIF',
|
||||
image: {url: 'attachment://thisisfine.gif'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'yeah.png', data: file1Data},
|
||||
{index: 1, filename: 'thisisfine.gif', data: file2Data},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.embeds).toBeDefined();
|
||||
expect(json.embeds).toHaveLength(2);
|
||||
|
||||
expect(json.embeds![0].image?.url).toContain('yeah.png');
|
||||
expect(json.embeds![1].image?.url).toContain('thisisfine.gif');
|
||||
});
|
||||
|
||||
it('should resolve multiple files referenced by embeds', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Multiple Files Test Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Test message with multiple attachments in embeds',
|
||||
attachments: [
|
||||
{id: 0, filename: 'image1.png'},
|
||||
{id: 1, filename: 'image2.png'},
|
||||
{id: 2, filename: 'image3.png'},
|
||||
],
|
||||
embeds: [
|
||||
{
|
||||
title: 'First Image',
|
||||
image: {url: 'attachment://image1.png'},
|
||||
},
|
||||
{
|
||||
title: 'Second Image',
|
||||
image: {url: 'attachment://image2.png'},
|
||||
},
|
||||
{
|
||||
title: 'Third Image',
|
||||
thumbnail: {url: 'attachment://image3.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'image1.png', data: fileData},
|
||||
{index: 1, filename: 'image2.png', data: fileData},
|
||||
{index: 2, filename: 'image3.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toHaveLength(3);
|
||||
expect(json.embeds).toHaveLength(3);
|
||||
|
||||
for (const embed of json.embeds!) {
|
||||
if (embed.image) {
|
||||
expect(embed.image.url).not.toContain('attachment://');
|
||||
}
|
||||
if (embed.thumbnail) {
|
||||
expect(embed.thumbnail.url).not.toContain('attachment://');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should allow same attachment to be used in multiple embeds', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Reused Attachment Guild');
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'test-channel');
|
||||
|
||||
const channelId = guild.system_channel_id ?? channel.id;
|
||||
|
||||
const fileData = loadFixture('yeah.png');
|
||||
|
||||
const payload = {
|
||||
content: 'Same attachment in multiple embeds',
|
||||
attachments: [{id: 0, filename: 'shared.png'}],
|
||||
embeds: [
|
||||
{
|
||||
title: 'First Use',
|
||||
image: {url: 'attachment://shared.png'},
|
||||
},
|
||||
{
|
||||
title: 'Second Use',
|
||||
thumbnail: {url: 'attachment://shared.png'},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const {response, json} = await sendMessageWithAttachments(harness, account.token, channelId, payload, [
|
||||
{index: 0, filename: 'shared.png', data: fileData},
|
||||
]);
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(json.attachments).toHaveLength(1);
|
||||
expect(json.embeds).toHaveLength(2);
|
||||
|
||||
expect(json.embeds![0].image?.url).not.toContain('attachment://');
|
||||
expect(json.embeds![1].thumbnail?.url).not.toContain('attachment://');
|
||||
});
|
||||
});
|
||||
});
|
||||
69
packages/api/src/channel/tests/GroupDMNameUpdate.test.tsx
Normal file
69
packages/api/src/channel/tests/GroupDMNameUpdate.test.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createFriendship,
|
||||
createGroupDmChannel,
|
||||
type GroupDmChannelResponse,
|
||||
} 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 {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Group DM name update', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('updates group DM name correctly', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
const updated = await createBuilder<GroupDmChannelResponse>(harness, user1.token)
|
||||
.patch(`/channels/${groupDm.id}`)
|
||||
.body({name: 'Cool Group Chat'})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(updated.name).toBe('Cool Group Chat');
|
||||
});
|
||||
});
|
||||
154
packages/api/src/channel/tests/GroupDMNicknameUpdate.test.tsx
Normal file
154
packages/api/src/channel/tests/GroupDMNicknameUpdate.test.tsx
Normal file
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createFriendship, createGroupDmChannel, getChannel} 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 type {ChannelResponse} from '@fluxer/schema/src/domains/channel/ChannelSchemas';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Group DM nickname update', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('user can update their own nickname in a group DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
const updatedChannel = await createBuilder<ChannelResponse>(harness, user2.token)
|
||||
.patch(`/channels/${groupDm.id}`)
|
||||
.body({
|
||||
nicks: {
|
||||
[user2.userId]: 'User 2 Nick',
|
||||
},
|
||||
})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(updatedChannel.nicks).toBeDefined();
|
||||
expect(updatedChannel.nicks?.[user2.userId]).toBe('User 2 Nick');
|
||||
});
|
||||
|
||||
it('nickname is returned correctly in channel response after update', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
await createBuilder<ChannelResponse>(harness, user2.token)
|
||||
.patch(`/channels/${groupDm.id}`)
|
||||
.body({
|
||||
nicks: {
|
||||
[user2.userId]: 'My Custom Nick',
|
||||
},
|
||||
})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
const fetchedChannel = await getChannel(harness, user2.token, groupDm.id);
|
||||
|
||||
expect(fetchedChannel.nicks).toBeDefined();
|
||||
expect(fetchedChannel.nicks?.[user2.userId]).toBe('My Custom Nick');
|
||||
});
|
||||
|
||||
it('non-owner cannot update another users nickname', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
await createBuilder(harness, user2.token)
|
||||
.patch(`/channels/${groupDm.id}`)
|
||||
.body({
|
||||
nicks: {
|
||||
[user3.userId]: 'User 3 Nick by User 2',
|
||||
},
|
||||
})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('owner can update another users nickname', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
const updatedChannel = await createBuilder<ChannelResponse>(harness, user1.token)
|
||||
.patch(`/channels/${groupDm.id}`)
|
||||
.body({
|
||||
nicks: {
|
||||
[user3.userId]: 'User 3 Nick by Owner',
|
||||
},
|
||||
})
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.execute();
|
||||
|
||||
expect(updatedChannel.nicks).toBeDefined();
|
||||
expect(updatedChannel.nicks?.[user3.userId]).toBe('User 3 Nick by Owner');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createFriendship, createGroupDmChannel} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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';
|
||||
|
||||
describe('Group DM Security Boundaries', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('non-member cannot read messages from group DM', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
const attacker = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, creator.token, [recipient.userId]);
|
||||
|
||||
await createBuilder(harness, attacker.token)
|
||||
.get(`/channels/${groupDm.id}/messages`)
|
||||
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('non-member cannot send messages to group DM', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
const attacker = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, creator.token, [recipient.userId]);
|
||||
|
||||
await createBuilder(harness, attacker.token)
|
||||
.post(`/channels/${groupDm.id}/messages`)
|
||||
.body({content: 'Unauthorized message'})
|
||||
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('non-member cannot add themselves to group DM', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
const attacker = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, creator.token, [recipient.userId]);
|
||||
|
||||
await createBuilder(harness, attacker.token)
|
||||
.put(`/channels/${groupDm.id}/recipients/${attacker.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_ACCESS')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('recipient can leave group DM', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, creator.token, [recipient.userId]);
|
||||
|
||||
await createBuilder(harness, recipient.token)
|
||||
.delete(`/channels/${groupDm.id}/recipients/${recipient.userId}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('after leaving, former member cannot read messages', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, creator.token, [recipient.userId]);
|
||||
|
||||
await createBuilder(harness, recipient.token)
|
||||
.delete(`/channels/${groupDm.id}/recipients/${recipient.userId}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, recipient.token)
|
||||
.get(`/channels/${groupDm.id}/messages`)
|
||||
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('after leaving, former member cannot send messages', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, creator.token, [recipient.userId]);
|
||||
|
||||
await createBuilder(harness, recipient.token)
|
||||
.delete(`/channels/${groupDm.id}/recipients/${recipient.userId}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, recipient.token)
|
||||
.post(`/channels/${groupDm.id}/messages`)
|
||||
.body({content: 'Message after leaving'})
|
||||
.expect(HTTP_STATUS.NOT_FOUND, 'UNKNOWN_CHANNEL')
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createFriendship, createGroupDmChannel} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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';
|
||||
|
||||
describe('Group DM Add Recipient Permissions', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('rejects adding non-friend to group DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
const user4 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDmChannel = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.put(`/channels/${groupDmChannel.id}/recipients/${user4.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, 'NOT_FRIENDS_WITH_USER')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('allows adding friend to group DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
const user4 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
await createFriendship(harness, user1, user4);
|
||||
|
||||
const groupDmChannel = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
await createBuilder(harness, user1.token)
|
||||
.put(`/channels/${groupDmChannel.id}/recipients/${user4.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('allows member to add their own friend to group DM', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const otherMember = await createTestAccount(harness);
|
||||
const newUser = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, owner, member);
|
||||
await createFriendship(harness, owner, otherMember);
|
||||
await createFriendship(harness, member, newUser);
|
||||
|
||||
const groupDmChannel = await createGroupDmChannel(harness, owner.token, [member.userId, otherMember.userId]);
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.put(`/channels/${groupDmChannel.id}/recipients/${newUser.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects non-participant from adding recipients to group DM', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const third = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, owner, member);
|
||||
await createFriendship(harness, owner, third);
|
||||
await createFriendship(harness, member, third);
|
||||
await createFriendship(harness, outsider, third);
|
||||
|
||||
const groupDmChannel = await createGroupDmChannel(harness, owner.token, [member.userId, third.userId]);
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.put(`/channels/${groupDmChannel.id}/recipients/${third.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.FORBIDDEN, 'MISSING_ACCESS')
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects member from adding non-friend to group DM', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const otherMember = await createTestAccount(harness);
|
||||
const ownerFriend = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, owner, member);
|
||||
await createFriendship(harness, owner, otherMember);
|
||||
await createFriendship(harness, owner, ownerFriend);
|
||||
await createFriendship(harness, outsider, otherMember);
|
||||
|
||||
const groupDmChannel = await createGroupDmChannel(harness, owner.token, [member.userId, otherMember.userId]);
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.put(`/channels/${groupDmChannel.id}/recipients/${ownerFriend.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.put(`/channels/${groupDmChannel.id}/recipients/${outsider.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, 'NOT_FRIENDS_WITH_USER')
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
76
packages/api/src/channel/tests/GroupDmLimit.test.tsx
Normal file
76
packages/api/src/channel/tests/GroupDmLimit.test.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createFriendship, seedPrivateChannels} 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 {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
const MAX_GROUP_DM_LIMIT = 150;
|
||||
const MAX_GROUP_DMS_ERROR_CODE = 'MAX_GROUP_DMS';
|
||||
|
||||
describe('Group DM recipient limit', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('rejects creating group DM when user has reached the limit', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const target = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
const helper = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, creator.token);
|
||||
await ensureSessionStarted(harness, target.token);
|
||||
await ensureSessionStarted(harness, recipient.token);
|
||||
await ensureSessionStarted(harness, helper.token);
|
||||
|
||||
await createFriendship(harness, creator, target);
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const seedResult = await seedPrivateChannels(harness, target.token, target.userId, {
|
||||
group_dm_count: MAX_GROUP_DM_LIMIT,
|
||||
recipients: [helper.userId, recipient.userId],
|
||||
clear_existing: true,
|
||||
});
|
||||
|
||||
expect(seedResult.group_dms.length).toBe(MAX_GROUP_DM_LIMIT);
|
||||
|
||||
await createBuilder(harness, creator.token)
|
||||
.post('/users/@me/channels')
|
||||
.body({
|
||||
recipients: [helper.userId, target.userId],
|
||||
})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, MAX_GROUP_DMS_ERROR_CODE)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
156
packages/api/src/channel/tests/GroupDmManagement.test.tsx
Normal file
156
packages/api/src/channel/tests/GroupDmManagement.test.tsx
Normal file
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
addRecipientToGroupDm,
|
||||
createFriendship,
|
||||
createGroupDmChannel,
|
||||
getChannel,
|
||||
removeRecipientFromGroupDm,
|
||||
updateChannel,
|
||||
} 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 {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Group DM management', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('can create group DM with multiple recipients', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
expect(groupDm.id).toBeTruthy();
|
||||
expect(groupDm.type).toBe(3);
|
||||
expect(groupDm.owner_id).toBe(user1.userId);
|
||||
expect(groupDm.recipients.length).toBe(2);
|
||||
});
|
||||
|
||||
it('can update group DM name', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
const updated = await updateChannel(harness, user1.token, groupDm.id, {name: 'Cool Group Chat'});
|
||||
|
||||
expect(updated.name).toBe('Cool Group Chat');
|
||||
});
|
||||
|
||||
it('can add recipient to existing group DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
const user4 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
await ensureSessionStarted(harness, user4.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
await createFriendship(harness, user1, user4);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
expect(groupDm.recipients.length).toBe(2);
|
||||
|
||||
await addRecipientToGroupDm(harness, user1.token, groupDm.id, user4.userId);
|
||||
|
||||
const updatedChannel = await getChannel(harness, user1.token, groupDm.id);
|
||||
expect(updatedChannel.type).toBe(3);
|
||||
});
|
||||
|
||||
it('can remove recipient from group DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
await removeRecipientFromGroupDm(harness, user1.token, groupDm.id, user3.userId);
|
||||
|
||||
const updatedChannel = await getChannel(harness, user1.token, groupDm.id);
|
||||
expect(updatedChannel.type).toBe(3);
|
||||
});
|
||||
|
||||
it('rejects non-member adding recipient to group DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
await ensureSessionStarted(harness, user1.token);
|
||||
await ensureSessionStarted(harness, user2.token);
|
||||
await ensureSessionStarted(harness, user3.token);
|
||||
await ensureSessionStarted(harness, outsider.token);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
await createFriendship(harness, user1, user3);
|
||||
await createFriendship(harness, outsider, user3);
|
||||
|
||||
const groupDm = await createGroupDmChannel(harness, user1.token, [user2.userId, user3.userId]);
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.put(`/channels/${groupDm.id}/recipients/${user3.userId}`)
|
||||
.body(null)
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createFriendship, seedPrivateChannels} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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, expect, it} from 'vitest';
|
||||
|
||||
const MAX_GROUP_DM_LIMIT = 150;
|
||||
const MAX_GROUP_DM_ERROR_CODE = 'MAX_GROUP_DMS';
|
||||
|
||||
describe('Group DM Recipient Limit', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('rejects creating group DM when user has reached limit', async () => {
|
||||
const creator = await createTestAccount(harness);
|
||||
const target = await createTestAccount(harness);
|
||||
const recipient = await createTestAccount(harness);
|
||||
const helper = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, creator, target);
|
||||
await createFriendship(harness, creator, recipient);
|
||||
|
||||
const seedResult = await seedPrivateChannels(harness, target.token, target.userId, {
|
||||
group_dm_count: MAX_GROUP_DM_LIMIT,
|
||||
recipients: [helper.userId, recipient.userId],
|
||||
clear_existing: true,
|
||||
});
|
||||
|
||||
expect(seedResult.group_dms).toHaveLength(MAX_GROUP_DM_LIMIT);
|
||||
|
||||
await createBuilder(harness, creator.token)
|
||||
.post('/users/@me/channels')
|
||||
.body({
|
||||
recipients: [helper.userId, target.userId],
|
||||
})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST, MAX_GROUP_DM_ERROR_CODE)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {createGuild, getChannel, sendChannelMessage} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {createEmoji, getGifDataUrl, getPngDataUrl} from '@fluxer/api/src/emoji/tests/EmojiTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Message custom emoji sanitization', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('preserves guild-local static and animated custom emojis for free users', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Local emoji guild');
|
||||
if (!guild.system_channel_id) {
|
||||
throw new Error('Guild system channel id is missing');
|
||||
}
|
||||
|
||||
const channel = await getChannel(harness, account.token, guild.system_channel_id);
|
||||
const staticEmoji = await createEmoji(harness, account.token, guild.id, {
|
||||
name: 'local_static',
|
||||
image: getPngDataUrl(),
|
||||
});
|
||||
const animatedEmoji = await createEmoji(harness, account.token, guild.id, {
|
||||
name: 'local_animated',
|
||||
image: getGifDataUrl(),
|
||||
});
|
||||
|
||||
const content = `Static <:${staticEmoji.name}:${staticEmoji.id}> animated <a:${animatedEmoji.name}:${animatedEmoji.id}>`;
|
||||
const message = await sendChannelMessage(harness, account.token, channel.id, content);
|
||||
|
||||
expect(message.content).toBe(content);
|
||||
});
|
||||
|
||||
it('strips external custom emojis for free users', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const destinationGuild = await createGuild(harness, account.token, 'Destination guild');
|
||||
if (!destinationGuild.system_channel_id) {
|
||||
throw new Error('Destination guild system channel id is missing');
|
||||
}
|
||||
|
||||
const destinationChannel = await getChannel(harness, account.token, destinationGuild.system_channel_id);
|
||||
const sourceGuild = await createGuild(harness, account.token, 'External emoji guild');
|
||||
const externalEmoji = await createEmoji(harness, account.token, sourceGuild.id, {
|
||||
name: 'external_emoji',
|
||||
image: getPngDataUrl(),
|
||||
});
|
||||
|
||||
const content = `External <:${externalEmoji.name}:${externalEmoji.id}>`;
|
||||
const message = await sendChannelMessage(harness, account.token, destinationChannel.id, content);
|
||||
|
||||
expect(message.content).toBe('External :external_emoji:');
|
||||
});
|
||||
});
|
||||
206
packages/api/src/channel/tests/ScheduledMessageTestUtils.tsx
Normal file
206
packages/api/src/channel/tests/ScheduledMessageTestUtils.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/*
|
||||
* 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 {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
||||
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {ManagedTraits} from '@fluxer/constants/src/ManagedTraits';
|
||||
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
|
||||
import type {ScheduledMessageResponseSchema} from '@fluxer/schema/src/domains/message/ScheduledMessageSchemas';
|
||||
import type {z} from 'zod';
|
||||
|
||||
export type ScheduledMessageResponse = z.infer<typeof ScheduledMessageResponseSchema>;
|
||||
|
||||
export async function createGuildChannel(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
name: string,
|
||||
): Promise<{id: string}> {
|
||||
const {response, json} = await createBuilder<{id: string}>(harness, token)
|
||||
.post(`/guilds/${guildId}/channels`)
|
||||
.body({name, type: 0})
|
||||
.executeWithResponse();
|
||||
|
||||
if (response.status !== 200 && response.status !== 201) {
|
||||
throw new Error(`Failed to create guild channel: ${response.status}`);
|
||||
}
|
||||
|
||||
return json as {id: string};
|
||||
}
|
||||
|
||||
export async function enableMessageSchedulingForGuild(harness: ApiTestHarness, guildId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, '')
|
||||
.post(`/test/guilds/${guildId}/features`)
|
||||
.body({
|
||||
add_features: [ManagedTraits.MESSAGE_SCHEDULING],
|
||||
})
|
||||
.expect(200)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function scheduleMessage(
|
||||
harness: ApiTestHarness,
|
||||
channelId: string,
|
||||
token: string,
|
||||
content: string,
|
||||
scheduledAt?: Date,
|
||||
): Promise<ScheduledMessageResponse> {
|
||||
const scheduledTime = scheduledAt ?? new Date(Date.now() + 60 * 1000);
|
||||
|
||||
await ensureSessionStarted(harness, token);
|
||||
|
||||
const {json} = await createBuilder<ScheduledMessageResponse>(harness, token)
|
||||
.post(`/channels/${channelId}/messages/schedule`)
|
||||
.body({
|
||||
content,
|
||||
scheduled_local_at: scheduledTime.toISOString(),
|
||||
timezone: 'UTC',
|
||||
})
|
||||
.expect(201)
|
||||
.executeWithResponse();
|
||||
|
||||
return json as ScheduledMessageResponse;
|
||||
}
|
||||
|
||||
export async function updateScheduledMessage(
|
||||
harness: ApiTestHarness,
|
||||
scheduledMessageId: string,
|
||||
token: string,
|
||||
updates: {
|
||||
content?: string;
|
||||
scheduled_local_at?: string;
|
||||
timezone?: string;
|
||||
},
|
||||
): Promise<ScheduledMessageResponse> {
|
||||
const {json} = await createBuilder<ScheduledMessageResponse>(harness, token)
|
||||
.patch(`/users/@me/scheduled-messages/${scheduledMessageId}`)
|
||||
.body(updates)
|
||||
.expect(200)
|
||||
.executeWithResponse();
|
||||
|
||||
return json as ScheduledMessageResponse;
|
||||
}
|
||||
|
||||
export async function getScheduledMessages(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
): Promise<Array<ScheduledMessageResponse>> {
|
||||
const {json} = await createBuilder<Array<ScheduledMessageResponse>>(harness, token)
|
||||
.get('/users/@me/scheduled-messages')
|
||||
.expect(200)
|
||||
.executeWithResponse();
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export async function getScheduledMessage(
|
||||
harness: ApiTestHarness,
|
||||
scheduledMessageId: string,
|
||||
token: string,
|
||||
expectedStatus: 200 | 404 = 200,
|
||||
): Promise<ScheduledMessageResponse | null> {
|
||||
const {response, json} = await createBuilder<ScheduledMessageResponse>(harness, token)
|
||||
.get(`/users/@me/scheduled-messages/${scheduledMessageId}`)
|
||||
.expect(expectedStatus)
|
||||
.executeWithResponse();
|
||||
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json as ScheduledMessageResponse;
|
||||
}
|
||||
|
||||
export async function cancelScheduledMessage(
|
||||
harness: ApiTestHarness,
|
||||
scheduledMessageId: string,
|
||||
token: string,
|
||||
): Promise<void> {
|
||||
await createBuilder<void>(harness, token)
|
||||
.delete(`/users/@me/scheduled-messages/${scheduledMessageId}`)
|
||||
.expect(204)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function triggerScheduledMessageWorker(
|
||||
harness: ApiTestHarness,
|
||||
userId: string,
|
||||
scheduledMessageId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder<void>(harness, '')
|
||||
.post(`/test/worker/send-scheduled-message/${userId}/${scheduledMessageId}`)
|
||||
.expect(200)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function getChannelMessages(
|
||||
harness: ApiTestHarness,
|
||||
channelId: string,
|
||||
token: string,
|
||||
limit = 20,
|
||||
): Promise<Array<MessageResponse>> {
|
||||
const {json} = await createBuilder<Array<MessageResponse>>(harness, token)
|
||||
.get(`/channels/${channelId}/messages?limit=${limit}`)
|
||||
.expect(200)
|
||||
.executeWithResponse();
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export function messageFromAuthorContains(
|
||||
messages: Array<MessageResponse>,
|
||||
authorId: string,
|
||||
content: string,
|
||||
): boolean {
|
||||
return messages.some((msg) => msg.author.id === authorId && msg.content === content);
|
||||
}
|
||||
|
||||
export function containsMessageContent(messages: Array<MessageResponse>, content: string): boolean {
|
||||
return messages.some((msg) => msg.content === content);
|
||||
}
|
||||
|
||||
export async function createChannelInvite(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
channelId: string,
|
||||
): Promise<{code: string}> {
|
||||
const {json} = await createBuilder<{code: string}>(harness, token)
|
||||
.post(`/channels/${channelId}/invites`)
|
||||
.body({
|
||||
max_age: 86400,
|
||||
})
|
||||
.expect(200)
|
||||
.executeWithResponse();
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
export async function joinGuild(harness: ApiTestHarness, token: string, inviteCode: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token).post(`/invites/${inviteCode}`).body({}).expect(200).execute();
|
||||
}
|
||||
|
||||
export async function removeGuildMember(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
guildId: string,
|
||||
userId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder<void>(harness, token).delete(`/guilds/${guildId}/members/${userId}`).expect(204).execute();
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
scheduleMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Scheduled message trait gating', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('rejects scheduling message before trait enabled', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-flag');
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-channel');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
await createBuilder(harness, owner.token)
|
||||
.post(`/channels/${channel.id}/messages/schedule`)
|
||||
.body({
|
||||
content: 'trying to schedule',
|
||||
scheduled_local_at: new Date(Date.now() + 60 * 1000).toISOString(),
|
||||
timezone: 'UTC',
|
||||
})
|
||||
.expect(403)
|
||||
.execute();
|
||||
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
|
||||
const scheduled = await scheduleMessage(harness, channel.id, owner.token, 'enabled now');
|
||||
expect(scheduled.id).toBeDefined();
|
||||
expect(scheduled.id).not.toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
scheduleMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
import {ensureSessionStarted} from '@fluxer/api/src/message/tests/MessageTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {
|
||||
generateFutureTimestamp,
|
||||
generatePastTimestamp,
|
||||
HTTP_STATUS,
|
||||
TEST_LIMITS,
|
||||
} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Scheduled message validation', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('rejects scheduling message with past time', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-past');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
const pastTime = generatePastTimestamp(1);
|
||||
|
||||
const {json} = await createBuilder<{errors: Array<{path: string; message: string; code: string}>}>(
|
||||
harness,
|
||||
owner.token,
|
||||
)
|
||||
.post(`/channels/${channel.id}/messages/schedule`)
|
||||
.body({
|
||||
content: 'should fail - past time',
|
||||
scheduled_local_at: pastTime,
|
||||
timezone: 'UTC',
|
||||
})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.executeWithResponse();
|
||||
|
||||
const errorJson = json as {errors: Array<{path: string; message: string; code: string}>};
|
||||
const hasScheduledAtError = errorJson.errors.some((e) => e.path === 'scheduled_local_at');
|
||||
expect(hasScheduledAtError).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects scheduling message exceeding 30 days', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-30day');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
const futureTime = new Date(Date.now() + TEST_LIMITS.SCHEDULED_MESSAGE_MAX_DELAY_MS).toISOString();
|
||||
|
||||
const {json} = await createBuilder<{errors: Array<{path: string; message: string; code: string}>}>(
|
||||
harness,
|
||||
owner.token,
|
||||
)
|
||||
.post(`/channels/${channel.id}/messages/schedule`)
|
||||
.body({
|
||||
content: 'should fail - exceeds 30 days',
|
||||
scheduled_local_at: futureTime,
|
||||
timezone: 'UTC',
|
||||
})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.executeWithResponse();
|
||||
|
||||
const errorJson = json as {errors: Array<{path: string; message: string; code: string}>};
|
||||
const hasScheduledAtError = errorJson.errors.some((e) => e.path === 'scheduled_local_at');
|
||||
expect(hasScheduledAtError).toBe(true);
|
||||
});
|
||||
|
||||
it('rejects scheduling message with invalid timezone', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-tz');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
await ensureSessionStarted(harness, owner.token);
|
||||
|
||||
const futureTime = generateFutureTimestamp(5);
|
||||
|
||||
const {json} = await createBuilder<{errors: Array<{path: string; message: string; code: string}>}>(
|
||||
harness,
|
||||
owner.token,
|
||||
)
|
||||
.post(`/channels/${channel.id}/messages/schedule`)
|
||||
.body({
|
||||
content: 'should fail - invalid timezone',
|
||||
scheduled_local_at: futureTime,
|
||||
timezone: 'Invalid/NotATimezone',
|
||||
})
|
||||
.expect(HTTP_STATUS.BAD_REQUEST)
|
||||
.executeWithResponse();
|
||||
|
||||
const errorJson = json as {errors: Array<{path: string; message: string; code: string}>};
|
||||
const hasTimezoneError = errorJson.errors.some((e) => e.path === 'timezone');
|
||||
expect(hasTimezoneError).toBe(true);
|
||||
});
|
||||
|
||||
it('accepts scheduling message at 30 day boundary', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'sched-validation-boundary');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'test');
|
||||
|
||||
const futureTime = new Date(Date.now() + 29 * 24 * 60 * 60 * 1000 + 23 * 60 * 60 * 1000).toISOString();
|
||||
|
||||
const scheduled = await scheduleMessage(
|
||||
harness,
|
||||
channel.id,
|
||||
owner.token,
|
||||
'should succeed - within 30 days',
|
||||
new Date(futureTime),
|
||||
);
|
||||
|
||||
expect(scheduled.status).toBe('pending');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannelInvite,
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
getChannelMessages,
|
||||
getScheduledMessage,
|
||||
joinGuild,
|
||||
messageFromAuthorContains,
|
||||
removeGuildMember,
|
||||
scheduleMessage,
|
||||
triggerScheduledMessageWorker,
|
||||
updateScheduledMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Scheduled message worker lifecycle', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('delivers scheduled message when permissions remain', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-messages');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled');
|
||||
|
||||
const content = 'scheduled message goes through';
|
||||
const scheduled = await scheduleMessage(harness, channel.id, owner.token, content);
|
||||
|
||||
await triggerScheduledMessageWorker(harness, owner.userId, scheduled.id);
|
||||
|
||||
const fetched = await getScheduledMessage(harness, scheduled.id, owner.token, 404);
|
||||
expect(fetched).toBeNull();
|
||||
|
||||
const messages = await getChannelMessages(harness, channel.id, owner.token);
|
||||
expect(messageFromAuthorContains(messages, owner.userId, content)).toBe(true);
|
||||
});
|
||||
|
||||
it('reschedules pending message before worker execution', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-messages');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled');
|
||||
|
||||
const content = 'scheduled message initial content';
|
||||
const scheduled = await scheduleMessage(harness, channel.id, owner.token, content);
|
||||
|
||||
const oldScheduledAt = new Date(scheduled.scheduled_at);
|
||||
|
||||
const updatedContent = 'scheduled message updated content';
|
||||
const newLocalTime = new Date(Date.now() + 5 * 60 * 1000);
|
||||
const newLocalStr = newLocalTime.toISOString();
|
||||
|
||||
const updated = await updateScheduledMessage(harness, scheduled.id, owner.token, {
|
||||
content: updatedContent,
|
||||
scheduled_local_at: newLocalStr,
|
||||
timezone: 'America/Los_Angeles',
|
||||
});
|
||||
|
||||
expect(updated.status).toBe('pending');
|
||||
expect(updated.scheduled_local_at).toBe(newLocalStr);
|
||||
expect(updated.timezone).toBe('America/Los_Angeles');
|
||||
|
||||
const updatedScheduledAt = new Date(updated.scheduled_at);
|
||||
expect(updatedScheduledAt.getTime()).toBeGreaterThan(oldScheduledAt.getTime());
|
||||
|
||||
await triggerScheduledMessageWorker(harness, owner.userId, updated.id);
|
||||
|
||||
const respMessages = await getChannelMessages(harness, channel.id, owner.token);
|
||||
expect(messageFromAuthorContains(respMessages, owner.userId, updatedContent)).toBe(true);
|
||||
expect(messageFromAuthorContains(respMessages, owner.userId, content)).toBe(false);
|
||||
|
||||
const fetched = await getScheduledMessage(harness, updated.id, owner.token, 404);
|
||||
expect(fetched).toBeNull();
|
||||
});
|
||||
|
||||
it('marks scheduled message invalid when access lost', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-messages');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled');
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, channel.id);
|
||||
await joinGuild(harness, member.token, invite.code);
|
||||
|
||||
const content = 'scheduled message invalidation';
|
||||
const scheduled = await scheduleMessage(harness, channel.id, member.token, content);
|
||||
|
||||
await removeGuildMember(harness, owner.token, guild.id, member.userId);
|
||||
|
||||
await triggerScheduledMessageWorker(harness, member.userId, scheduled.id);
|
||||
|
||||
const fetched = await getScheduledMessage(harness, scheduled.id, member.token);
|
||||
expect(fetched).not.toBeNull();
|
||||
expect(fetched!.status).toBe('invalid');
|
||||
expect(fetched!.status_reason).not.toBeNull();
|
||||
expect(fetched!.status_reason).not.toBe('');
|
||||
|
||||
const messages = await getChannelMessages(harness, channel.id, owner.token);
|
||||
const hasContent = messages.some((msg) => msg.content === content);
|
||||
expect(hasContent).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannelInvite,
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
getScheduledMessages,
|
||||
joinGuild,
|
||||
removeGuildMember,
|
||||
scheduleMessage,
|
||||
triggerScheduledMessageWorker,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Scheduled messages list invalid entry', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('shows invalid scheduled message in list', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-invalid');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-invalid');
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, channel.id);
|
||||
await joinGuild(harness, member.token, invite.code);
|
||||
|
||||
const content = 'invalid scheduled';
|
||||
const scheduled = await scheduleMessage(harness, channel.id, member.token, content);
|
||||
|
||||
await removeGuildMember(harness, owner.token, guild.id, member.userId);
|
||||
|
||||
await triggerScheduledMessageWorker(harness, member.userId, scheduled.id);
|
||||
|
||||
const list = await getScheduledMessages(harness, member.token);
|
||||
const entry = list.find((e) => e.id === scheduled.id);
|
||||
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry!.status).toBe('invalid');
|
||||
expect(entry!.status_reason).not.toBeNull();
|
||||
expect(entry!.status_reason).not.toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
cancelScheduledMessage,
|
||||
createGuildChannel,
|
||||
enableMessageSchedulingForGuild,
|
||||
getScheduledMessages,
|
||||
scheduleMessage,
|
||||
} from '@fluxer/api/src/channel/tests/ScheduledMessageTestUtils';
|
||||
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
describe('Scheduled messages list lifecycle', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
it('lists scheduled messages and removes after cancel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, owner.token, 'scheduled-list');
|
||||
await enableMessageSchedulingForGuild(harness, guild.id);
|
||||
const channel = await createGuildChannel(harness, owner.token, guild.id, 'scheduled-list');
|
||||
|
||||
const content = 'list scheduled';
|
||||
const scheduled = await scheduleMessage(harness, channel.id, owner.token, content);
|
||||
|
||||
const list = await getScheduledMessages(harness, owner.token);
|
||||
const found = list.some((entry) => entry.id === scheduled.id);
|
||||
expect(found).toBe(true);
|
||||
|
||||
await cancelScheduledMessage(harness, scheduled.id, owner.token);
|
||||
|
||||
const listAfterCancel = await getScheduledMessages(harness, owner.token);
|
||||
const foundAfterCancel = listAfterCancel.some((entry) => entry.id === scheduled.id);
|
||||
expect(foundAfterCancel).toBe(false);
|
||||
});
|
||||
});
|
||||
146
packages/api/src/channel/tests/StreamPreviewAuth.test.tsx
Normal file
146
packages/api/src/channel/tests/StreamPreviewAuth.test.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
acceptInvite,
|
||||
createChannel,
|
||||
createChannelInvite,
|
||||
createGuild,
|
||||
createPermissionOverwrite,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
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 {APIErrorCodes} from '@fluxer/constants/src/ApiErrorCodes';
|
||||
import {ChannelTypes, Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest';
|
||||
|
||||
const STREAM_PREVIEW_BASE64 = 'dGVzdC1zdHJlYW0tcHJldmlldw==';
|
||||
const STREAM_PREVIEW_TEXT = 'test-stream-preview';
|
||||
|
||||
function createGuildStreamKey(guildId: string, channelId: string): string {
|
||||
return `${guildId}:${channelId}:test-connection`;
|
||||
}
|
||||
|
||||
function createStreamPreviewPath(streamKey: string): string {
|
||||
return `/streams/${streamKey}/preview`;
|
||||
}
|
||||
|
||||
async function uploadStreamPreview(
|
||||
harness: ApiTestHarness,
|
||||
token: string,
|
||||
streamKey: string,
|
||||
channelId: string,
|
||||
): Promise<void> {
|
||||
await createBuilder(harness, token)
|
||||
.post(createStreamPreviewPath(streamKey))
|
||||
.body({
|
||||
channel_id: channelId,
|
||||
thumbnail: STREAM_PREVIEW_BASE64,
|
||||
content_type: 'image/jpeg',
|
||||
})
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe('Stream preview auth', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeAll(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await harness.reset();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
it('allows preview access for a guild member with CONNECT permission', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'stream-voice', ChannelTypes.GUILD_VOICE);
|
||||
const streamKey = createGuildStreamKey(guild.id, voiceChannel.id);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, voiceChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: Permissions.CONNECT.toString(),
|
||||
deny: '0',
|
||||
});
|
||||
|
||||
await uploadStreamPreview(harness, owner.token, streamKey, voiceChannel.id);
|
||||
|
||||
const {response, text} = await createBuilder(harness, member.token)
|
||||
.get(createStreamPreviewPath(streamKey))
|
||||
.expect(HTTP_STATUS.OK)
|
||||
.executeRaw();
|
||||
|
||||
expect(response.headers.get('content-type')).toBe('image/jpeg');
|
||||
expect(text).toBe(STREAM_PREVIEW_TEXT);
|
||||
});
|
||||
|
||||
it('rejects preview access for a user outside the guild', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const outsider = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'stream-voice', ChannelTypes.GUILD_VOICE);
|
||||
const streamKey = createGuildStreamKey(guild.id, voiceChannel.id);
|
||||
|
||||
await uploadStreamPreview(harness, owner.token, streamKey, voiceChannel.id);
|
||||
|
||||
await createBuilder(harness, outsider.token)
|
||||
.get(createStreamPreviewPath(streamKey))
|
||||
.expect(HTTP_STATUS.FORBIDDEN, APIErrorCodes.ACCESS_DENIED)
|
||||
.execute();
|
||||
});
|
||||
|
||||
it('rejects preview access when CONNECT is denied in the stream channel', async () => {
|
||||
const owner = await createTestAccount(harness);
|
||||
const member = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, owner.token, 'Test Guild');
|
||||
const voiceChannel = await createChannel(harness, owner.token, guild.id, 'stream-voice', ChannelTypes.GUILD_VOICE);
|
||||
const streamKey = createGuildStreamKey(guild.id, voiceChannel.id);
|
||||
|
||||
const invite = await createChannelInvite(harness, owner.token, guild.system_channel_id!);
|
||||
await acceptInvite(harness, member.token, invite.code);
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, voiceChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: '0',
|
||||
deny: Permissions.CONNECT.toString(),
|
||||
});
|
||||
|
||||
await uploadStreamPreview(harness, owner.token, streamKey, voiceChannel.id);
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.get(createStreamPreviewPath(streamKey))
|
||||
.expect(HTTP_STATUS.FORBIDDEN, APIErrorCodes.MISSING_PERMISSIONS)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
189
packages/api/src/channel/tests/TypingIndicators.test.tsx
Normal file
189
packages/api/src/channel/tests/TypingIndicators.test.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
* 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} from '@fluxer/api/src/auth/tests/AuthTestUtils';
|
||||
import {
|
||||
createChannel,
|
||||
createFriendship,
|
||||
createGuild,
|
||||
createPermissionOverwrite,
|
||||
createRole,
|
||||
setupTestGuildWithMembers,
|
||||
} from '@fluxer/api/src/channel/tests/ChannelTestUtils';
|
||||
import {type ApiTestHarness, createApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
|
||||
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
|
||||
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
|
||||
import {Permissions} from '@fluxer/constants/src/ChannelConstants';
|
||||
import {afterEach, beforeEach, describe, test} from 'vitest';
|
||||
|
||||
async function sendTypingIndicator(harness: ApiTestHarness, token: string, channelId: string): Promise<void> {
|
||||
await createBuilder<void>(harness, token).post(`/channels/${channelId}/typing`).body({}).expect(204).execute();
|
||||
}
|
||||
|
||||
async function createDmChannel(harness: ApiTestHarness, token: string, recipientId: string): Promise<{id: string}> {
|
||||
return createBuilder<{id: string}>(harness, token)
|
||||
.post('/users/@me/channels')
|
||||
.body({recipient_id: recipientId})
|
||||
.execute();
|
||||
}
|
||||
|
||||
describe('Typing Indicators', () => {
|
||||
let harness: ApiTestHarness;
|
||||
|
||||
beforeEach(async () => {
|
||||
harness = await createApiTestHarness();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await harness?.shutdown();
|
||||
});
|
||||
|
||||
test('should send typing indicator in DM channel', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await sendTypingIndicator(harness, user1.token, dmChannel.id);
|
||||
});
|
||||
|
||||
test('should send typing indicator in guild channel as member', async () => {
|
||||
const {members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
|
||||
await sendTypingIndicator(harness, member.token, systemChannel.id);
|
||||
});
|
||||
|
||||
test('should reject typing indicator from user not in DM channel', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
const user3 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await createBuilder(harness, user3.token)
|
||||
.post(`/channels/${dmChannel.id}/typing`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should reject typing indicator from non-member in guild channel', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const nonMember = await createTestAccount(harness);
|
||||
|
||||
const guild = await createGuild(harness, account.token, 'Typing Test Guild');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
await createBuilder(harness, nonMember.token)
|
||||
.post(`/channels/${channelId}/typing`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should require SEND_MESSAGES permission for typing indicator', async () => {
|
||||
const {owner, members, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
|
||||
await createPermissionOverwrite(harness, owner.token, systemChannel.id, member.userId, {
|
||||
type: 1,
|
||||
allow: '0',
|
||||
deny: Permissions.SEND_MESSAGES.toString(),
|
||||
});
|
||||
|
||||
await createBuilder(harness, member.token)
|
||||
.post(`/channels/${systemChannel.id}/typing`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.FORBIDDEN)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('should allow typing with SEND_MESSAGES permission from role', async () => {
|
||||
const {owner, members, guild, systemChannel} = await setupTestGuildWithMembers(harness, 1);
|
||||
const member = members[0];
|
||||
|
||||
const typingRole = await createRole(harness, owner.token, guild.id, {
|
||||
name: 'Can Type',
|
||||
permissions: Permissions.SEND_MESSAGES.toString(),
|
||||
});
|
||||
|
||||
await createBuilder<void>(harness, owner.token)
|
||||
.put(`/guilds/${guild.id}/members/${member.userId}/roles/${typingRole.id}`)
|
||||
.expect(HTTP_STATUS.NO_CONTENT)
|
||||
.execute();
|
||||
|
||||
await sendTypingIndicator(harness, member.token, systemChannel.id);
|
||||
});
|
||||
|
||||
test('should reject typing indicator for unknown channel', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
|
||||
await createBuilder(harness, account.token)
|
||||
.post('/channels/999999999999999999/typing')
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.NOT_FOUND)
|
||||
.execute();
|
||||
});
|
||||
|
||||
test('owner can always send typing indicator', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Owner Typing Test');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
await sendTypingIndicator(harness, account.token, channelId);
|
||||
});
|
||||
|
||||
test('both participants can send typing indicator in DM', async () => {
|
||||
const user1 = await createTestAccount(harness);
|
||||
const user2 = await createTestAccount(harness);
|
||||
|
||||
await createFriendship(harness, user1, user2);
|
||||
|
||||
const dmChannel = await createDmChannel(harness, user1.token, user2.userId);
|
||||
|
||||
await sendTypingIndicator(harness, user1.token, dmChannel.id);
|
||||
await sendTypingIndicator(harness, user2.token, dmChannel.id);
|
||||
});
|
||||
|
||||
test('should send typing indicator in created channel', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'Channel Typing Test');
|
||||
|
||||
const channel = await createChannel(harness, account.token, guild.id, 'typing-test');
|
||||
|
||||
await sendTypingIndicator(harness, account.token, channel.id);
|
||||
});
|
||||
|
||||
test('should reject typing indicator without authorization', async () => {
|
||||
const account = await createTestAccount(harness);
|
||||
const guild = await createGuild(harness, account.token, 'No Auth Test');
|
||||
const channelId = guild.system_channel_id!;
|
||||
|
||||
await createBuilderWithoutAuth(harness)
|
||||
.post(`/channels/${channelId}/typing`)
|
||||
.body({})
|
||||
.expect(HTTP_STATUS.UNAUTHORIZED)
|
||||
.execute();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user