// SPDX-License-Identifier: AGPL-3.0-or-later import {createTestAccount, setUserACLs, type TestAccount} from '@app/api/auth/tests/AuthTestUtils'; import {VALID_GIF_BASE64, VALID_PNG_BASE64} from '@app/api/emoji/tests/EmojiTestUtils'; import {createGuild} from '@app/api/guild/tests/GuildTestUtils'; import {ensureSessionStarted} from '@app/api/message/tests/MessageTestUtils'; import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness'; import {HTTP_STATUS} from '@app/api/test/TestConstants'; import {createBuilder} from '@app/api/test/TestRequestBuilder'; import {AdminACLs} from '@fluxer/constants/src/AdminACLs'; import type {LimitKey} from '@fluxer/constants/src/LimitConfigMetadata'; import {AVATAR_MAX_SIZE, EMOJI_MAX_SIZE, STICKER_MAX_SIZE} from '@fluxer/constants/src/LimitConstants'; import {ValidationErrorCodes} from '@fluxer/constants/src/ValidationErrorCodes'; import type {LimitConfigSnapshot} from '@fluxer/limits/src/LimitTypes'; import {afterAll, beforeAll, beforeEach, describe, expect, it} from 'vitest'; const RAISED_EXPRESSION_SIZE = EMOJI_MAX_SIZE * 4; const RAISED_AVATAR_SIZE = AVATAR_MAX_SIZE + 2 * 1024 * 1024; interface LimitConfigReadResponse { limit_config: LimitConfigSnapshot; } interface ValidationErrorResponse { errors?: Array<{path?: string; code?: string; message?: string}>; } function paddedImage(mime: string, base64: string, byteLength: number): string { const image = Buffer.from(base64, 'base64'); const padded = Buffer.concat([image, Buffer.alloc(byteLength - image.length)]); return `data:${mime};base64,${padded.toString('base64')}`; } describe('configured image size limits', () => { let harness: ApiTestHarness; beforeAll(async () => { harness = await createApiTestHarness(); }); beforeEach(async () => { await harness.reset(); }); afterAll(async () => { await harness?.shutdown(); }); const raiseLimits = async (limits: Partial>): Promise => { const admin = await setUserACLs(harness, await createTestAccount(harness), [ AdminACLs.AUTHENTICATE, AdminACLs.INSTANCE_LIMIT_CONFIG_VIEW, AdminACLs.INSTANCE_LIMIT_CONFIG_UPDATE, ]); const current = await createBuilder(harness, admin.token) .get('/admin/limit-config') .expect(HTTP_STATUS.OK) .execute(); await createBuilder(harness, admin.token) .put('/admin/limit-config') .body({ limit_config: { traitDefinitions: current.limit_config.traitDefinitions, rules: current.limit_config.rules.map((rule) => ({ id: rule.id, filters: rule.filters, limits: rule.id === 'very_large_guild' ? rule.limits : {...rule.limits, ...limits}, })), }, }) .expect(HTTP_STATUS.OK) .execute(); }; const createOwner = async (): Promise => { const owner = await createTestAccount(harness); await ensureSessionStarted(harness, owner.token); return owner; }; it('accepts emoji and sticker uploads above the stock size once the limits are raised', async () => { await raiseLimits({emoji_max_size: RAISED_EXPRESSION_SIZE, sticker_max_size: RAISED_EXPRESSION_SIZE}); const owner = await createOwner(); const guild = await createGuild(harness, owner.token, 'Raised expression limits'); const image = paddedImage('image/gif', VALID_GIF_BASE64, Math.max(EMOJI_MAX_SIZE, STICKER_MAX_SIZE) * 2); await createBuilder(harness, owner.token) .post(`/guilds/${guild.id}/emojis`) .body({name: 'large_emoji', image}) .expect(HTTP_STATUS.OK) .execute(); await createBuilder(harness, owner.token) .post(`/guilds/${guild.id}/stickers`) .body({name: 'large_sticker', description: 'large sticker', tags: [], image}) .expect(HTTP_STATUS.OK) .execute(); }); it('rejects an emoji over a raised limit with the configured size', async () => { await raiseLimits({emoji_max_size: RAISED_EXPRESSION_SIZE}); const owner = await createOwner(); const guild = await createGuild(harness, owner.token, 'Raised emoji ceiling'); const json = await createBuilder(harness, owner.token) .post(`/guilds/${guild.id}/emojis`) .body({name: 'too_large', image: paddedImage('image/gif', VALID_GIF_BASE64, RAISED_EXPRESSION_SIZE + 1)}) .expect(HTTP_STATUS.BAD_REQUEST) .execute(); expect(json.errors?.[0]).toMatchObject({ path: 'image', code: ValidationErrorCodes.IMAGE_SIZE_EXCEEDS_LIMIT, message: `Image size exceeds ${RAISED_EXPRESSION_SIZE} bytes.`, }); }); it('accepts an avatar above the stock size once avatar_max_size is raised', async () => { await raiseLimits({avatar_max_size: RAISED_AVATAR_SIZE}); const account = await createOwner(); const json = await createBuilder<{avatar: string | null}>(harness, account.token) .patch('/users/@me') .body({avatar: paddedImage('image/png', VALID_PNG_BASE64, AVATAR_MAX_SIZE + 1024 * 1024)}) .expect(HTTP_STATUS.OK) .execute(); expect(json.avatar).toBeTruthy(); }); it('rejects an avatar over a raised limit with the configured size', async () => { await raiseLimits({avatar_max_size: RAISED_AVATAR_SIZE}); const account = await createOwner(); const json = await createBuilder(harness, account.token) .patch('/users/@me') .body({avatar: paddedImage('image/png', VALID_PNG_BASE64, RAISED_AVATAR_SIZE + 1)}) .expect(HTTP_STATUS.BAD_REQUEST) .execute(); expect(json.errors?.[0]).toMatchObject({ path: 'avatar', code: ValidationErrorCodes.IMAGE_SIZE_EXCEEDS_LIMIT, message: `Image size exceeds ${RAISED_AVATAR_SIZE} bytes.`, }); }); });