fluxer/packages/api/src/message/tests/MessageEditAttachmentDescription.test.tsx
2026-02-17 12:22:36 +00:00

484 lines
17 KiB
TypeScript

/*
* Copyright (C) 2026 Fluxer Contributors
*
* This file is part of Fluxer.
*
* Fluxer is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fluxer is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Fluxer. If not, see <https://www.gnu.org/licenses/>.
*/
import {createTestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {createGuild, loadFixture, sendMessageWithAttachments} from '@fluxer/api/src/channel/tests/AttachmentTestUtils';
import {addMemberRole, createRole} from '@fluxer/api/src/guild/tests/GuildTestUtils';
import {
acceptInvite,
createChannelInvite,
createDMChannel,
editMessageWithAttachments,
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 {MessageFlags, Permissions} from '@fluxer/constants/src/ChannelConstants';
import {afterEach, beforeEach, describe, expect, test} from 'vitest';
describe('Message edit attachment description', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createApiTestHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
test('author can edit own attachment description', async () => {
const author = await createTestAccount(harness);
await ensureSessionStarted(harness, author.token);
const guild = await createGuild(harness, author.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
author.token,
channelId,
{
content: 'Message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const edited = await editMessageWithAttachments(harness, author.token, channelId, msg.id, {
content: 'Message with edited attachment',
attachments: [{id: 0, description: 'A cool image'}],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].description).toBe('A cool image');
expect(edited.content).toBe('Message with edited attachment');
expect(edited.edited_timestamp).toBeDefined();
expect(edited.edited_timestamp).not.toBeNull();
});
test('MANAGE_MESSAGES holder can edit others attachment descriptions in guild', async () => {
const owner = await createTestAccount(harness);
const moderator = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, moderator.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const moderatorRole = await createRole(harness, owner.token, guild.id, {
name: 'Moderator',
permissions: Permissions.MANAGE_MESSAGES.toString(),
});
const invite = await createChannelInvite(harness, owner.token, channelId);
await acceptInvite(harness, moderator.token, invite.code);
await acceptInvite(harness, member.token, invite.code);
await addMemberRole(harness, owner.token, guild.id, moderator.userId, moderatorRole.id);
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
member.token,
channelId,
{
content: 'Member message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const edited = await editMessageWithAttachments(harness, moderator.token, channelId, msg.id, {
attachments: [{id: 0, description: 'Moderator edited description'}],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].description).toBe('Moderator edited description');
});
test('MANAGE_MESSAGES holder can edit both title and description', async () => {
const owner = await createTestAccount(harness);
const moderator = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, moderator.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const moderatorRole = await createRole(harness, owner.token, guild.id, {
name: 'Moderator',
permissions: Permissions.MANAGE_MESSAGES.toString(),
});
const invite = await createChannelInvite(harness, owner.token, channelId);
await acceptInvite(harness, moderator.token, invite.code);
await addMemberRole(harness, owner.token, guild.id, moderator.userId, moderatorRole.id);
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
owner.token,
channelId,
{
content: 'Owner message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const edited = await editMessageWithAttachments(harness, moderator.token, channelId, msg.id, {
attachments: [{id: 0, title: 'Image Title', description: 'Image description'}],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].title).toBe('Image Title');
expect(edited.attachments?.[0].description).toBe('Image description');
});
test('unauthorized user without MANAGE_MESSAGES is rejected with 403', async () => {
const owner = await createTestAccount(harness);
const member = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, member.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const invite = await createChannelInvite(harness, owner.token, channelId);
await acceptInvite(harness, member.token, invite.code);
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
owner.token,
channelId,
{
content: 'Owner message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
await createBuilder(harness, member.token)
.patch(`/channels/${channelId}/messages/${msg.id}`)
.body({
attachments: [{id: 0, description: 'Unauthorized edit'}],
})
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
});
test('description validation enforces max 4096 characters', async () => {
const author = await createTestAccount(harness);
await ensureSessionStarted(harness, author.token);
const guild = await createGuild(harness, author.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
author.token,
channelId,
{
content: 'Message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const longDescription = 'x'.repeat(4097);
await createBuilder(harness, author.token)
.patch(`/channels/${channelId}/messages/${msg.id}`)
.body({
content: 'Message with attachment',
attachments: [{id: 0, description: longDescription}],
})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
});
test('empty or null description clears alt text', async () => {
const author = await createTestAccount(harness);
await ensureSessionStarted(harness, author.token);
const guild = await createGuild(harness, author.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
author.token,
channelId,
{
content: 'Message with attachment',
attachments: [{id: 0, filename: 'yeah.png', description: 'Initial description'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
expect(msg.attachments![0].description).toBe('Initial description');
const edited = await editMessageWithAttachments(harness, author.token, channelId, msg.id, {
content: 'Message with attachment',
attachments: [{id: 0, description: null}],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].description).toBeNull();
});
test('non-existent attachment ID is gracefully ignored', async () => {
const author = await createTestAccount(harness);
await ensureSessionStarted(harness, author.token);
const guild = await createGuild(harness, author.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
author.token,
channelId,
{
content: 'Message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const edited = await editMessageWithAttachments(harness, author.token, channelId, msg.id, {
content: 'Message with attachment',
attachments: [
{id: 0, description: 'Real attachment'},
{id: 999, description: 'Fake'},
],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].description).toBe('Real attachment');
});
test('DM channel: only author can edit, recipient is rejected', async () => {
const user1 = await createTestAccount(harness);
const user2 = await createTestAccount(harness);
await ensureSessionStarted(harness, user1.token);
await ensureSessionStarted(harness, user2.token);
await createBuilder(harness, user1.token).post(`/users/@me/relationships/${user2.userId}`).body({}).execute();
await createBuilder(harness, user2.token).put(`/users/@me/relationships/${user1.userId}`).body({}).execute();
const dmChannel = await createDMChannel(harness, user1.token, user2.userId);
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
user1.token,
dmChannel.id,
{
content: 'DM with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const editedByAuthor = await editMessageWithAttachments(harness, user1.token, dmChannel.id, msg.id, {
content: 'DM with attachment',
attachments: [{id: 0, description: 'Author edit'}],
});
expect(editedByAuthor.attachments).toBeDefined();
expect(editedByAuthor.attachments?.length).toBe(1);
expect(editedByAuthor.attachments?.[0].description).toBe('Author edit');
await createBuilder(harness, user2.token)
.patch(`/channels/${dmChannel.id}/messages/${msg.id}`)
.body({
content: 'DM with attachment',
attachments: [{id: 0, description: 'Recipient edit'}],
})
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
});
test('multiple attachments: edit specific one', async () => {
const author = await createTestAccount(harness);
await ensureSessionStarted(harness, author.token);
const guild = await createGuild(harness, author.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const fileData1 = loadFixture('yeah.png');
const fileData2 = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
author.token,
channelId,
{
content: 'Message with multiple attachments',
attachments: [
{id: 0, filename: 'first.png'},
{id: 1, filename: 'second.png'},
],
},
[
{index: 0, filename: 'first.png', data: fileData1},
{index: 1, filename: 'second.png', data: fileData2},
],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(2);
const edited = await editMessageWithAttachments(harness, author.token, channelId, msg.id, {
content: 'Message with multiple attachments',
attachments: [{id: 0, description: 'First image description'}, {id: 1}],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(2);
expect(edited.attachments?.[0].description).toBe('First image description');
expect(edited.attachments?.[1].description).toBeNull();
});
test('combined edit: SUPPRESS_EMBEDS flag and attachment description', async () => {
const author = await createTestAccount(harness);
await ensureSessionStarted(harness, author.token);
const guild = await createGuild(harness, author.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
author.token,
channelId,
{
content: 'Message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const edited = await editMessageWithAttachments(harness, author.token, channelId, msg.id, {
content: 'Message with attachment',
attachments: [{id: 0, description: 'Edited description'}],
flags: MessageFlags.SUPPRESS_EMBEDS,
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].description).toBe('Edited description');
});
test('verify edited_timestamp NOT set for non-author edits with MANAGE_MESSAGES', async () => {
const owner = await createTestAccount(harness);
const moderator = await createTestAccount(harness);
await ensureSessionStarted(harness, owner.token);
await ensureSessionStarted(harness, moderator.token);
const guild = await createGuild(harness, owner.token, 'Test Guild');
const channelId = guild.system_channel_id!;
const moderatorRole = await createRole(harness, owner.token, guild.id, {
name: 'Moderator',
permissions: Permissions.MANAGE_MESSAGES.toString(),
});
const invite = await createChannelInvite(harness, owner.token, channelId);
await acceptInvite(harness, moderator.token, invite.code);
await addMemberRole(harness, owner.token, guild.id, moderator.userId, moderatorRole.id);
const fileData = loadFixture('yeah.png');
const {json: msg} = await sendMessageWithAttachments(
harness,
owner.token,
channelId,
{
content: 'Owner message with attachment',
attachments: [{id: 0, filename: 'yeah.png'}],
},
[{index: 0, filename: 'yeah.png', data: fileData}],
);
expect(msg.attachments).toBeDefined();
expect(msg.attachments).not.toBeNull();
expect(msg.attachments!.length).toBe(1);
const originalEditedTimestamp = msg.edited_timestamp;
const edited = await editMessageWithAttachments(harness, moderator.token, channelId, msg.id, {
attachments: [{id: 0, description: 'Moderator edited description'}],
});
expect(edited.attachments).toBeDefined();
expect(edited.attachments?.length).toBe(1);
expect(edited.attachments?.[0].description).toBe('Moderator edited description');
expect(edited.edited_timestamp).toBe(originalEditedTimestamp);
});
});