/* * 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 . */ import {createTestAccount as authCreateTestAccount, type TestAccount} from '@fluxer/api/src/auth/tests/AuthTestUtils'; import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness'; import {createBuilder} from '@fluxer/api/src/test/TestRequestBuilder'; import type {GuildResponse} from '@fluxer/schema/src/domains/guild/GuildResponseSchemas'; import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas'; export interface AckResponse { token: string; } export async function createTestAccount(harness: ApiTestHarness): Promise { return authCreateTestAccount(harness); } export async function createMessageHarness(): Promise { const {createApiTestHarness} = await import('@fluxer/api/src/test/ApiTestHarness'); return await createApiTestHarness(); } export async function sendMessage( harness: ApiTestHarness, token: string, channelId: string, content: string, ): Promise { await ensureSessionStarted(harness, token); const msg = await createBuilder(harness, token) .post(`/channels/${channelId}/messages`) .body({ content, }) .execute(); if (!msg.id) { throw new Error('Message response missing id'); } return msg; } export async function getMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, ): Promise { const msg = await createBuilder(harness, token) .get(`/channels/${channelId}/messages/${messageId}`) .execute(); if (!msg.id) { throw new Error('Message response missing id'); } return msg; } export async function getMessages( harness: ApiTestHarness, token: string, channelId: string, queryParams?: Record, ): Promise> { const queryString = queryParams ? `?${new URLSearchParams(Object.entries(queryParams).map(([k, v]) => [k, v] as [string, string])).toString()}` : ''; return createBuilder>(harness, token) .get(`/channels/${channelId}/messages${queryString}`) .execute(); } export async function editMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, content: string, ): Promise { const msg = await createBuilder(harness, token) .patch(`/channels/${channelId}/messages/${messageId}`) .body({ content, }) .execute(); if (!msg.id) { throw new Error('Message response missing id'); } return msg; } export async function deleteMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, ): Promise { await createBuilder(harness, token) .delete(`/channels/${channelId}/messages/${messageId}`) .expect(204) .execute(); } export async function ackMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, mentionCount = 0, ): Promise { await createBuilder(harness, token) .post(`/channels/${channelId}/messages/${messageId}/ack`) .body({ mention_count: mentionCount, }) .expect(204) .execute(); } export async function pinMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, ): Promise { await createBuilder(harness, token) .put(`/channels/${channelId}/pins/${messageId}`) .body(null) .expect(204) .execute(); } export async function unpinMessage( harness: ApiTestHarness, token: string, channelId: string, messageId: string, ): Promise { await createBuilder(harness, token).delete(`/channels/${channelId}/pins/${messageId}`).expect(204).execute(); } export async function getPins( harness: ApiTestHarness, token: string, channelId: string, ): Promise> { const result = await createBuilder<{items: Array<{message: MessageResponse; pinned_at: string}>; has_more: boolean}>( harness, token, ) .get(`/channels/${channelId}/messages/pins`) .execute(); return result.items.map((item) => item.message); } export async function createGuild(harness: ApiTestHarness, token: string, name: string): Promise { const guild = await createBuilder(harness, token) .post('/guilds') .body({ name, }) .execute(); if (!guild.id) { throw new Error('Guild response missing id'); } return guild; } export async function createDMChannel( harness: ApiTestHarness, token: string, recipientUserId: string, ): Promise { const channel = await createBuilder(harness, token) .post('/users/@me/channels') .body({ recipient_id: recipientUserId, }) .execute(); if (!channel.id) { throw new Error('Channel response missing id'); } return channel; } export async function createChannelInvite( harness: ApiTestHarness, token: string, channelId: string, ): Promise<{code: string}> { return createBuilder<{code: string}>(harness, token).post(`/channels/${channelId}/invites`).body({}).execute(); } export async function acceptInvite(harness: ApiTestHarness, token: string, inviteCode: string): Promise { await createBuilder(harness, token).post(`/invites/${inviteCode}`).body({}).expect(200).execute(); } export async function addReaction( harness: ApiTestHarness, token: string, channelId: string, messageId: string, emoji: string, ): Promise { await createBuilder(harness, token) .put(`/channels/${channelId}/messages/${messageId}/reactions/${emoji}/@me`) .body(null) .expect(204) .execute(); } export async function removeReaction( harness: ApiTestHarness, token: string, channelId: string, messageId: string, emoji: string, userId?: string, ): Promise { const user = userId ?? '@me'; await createBuilder(harness, token) .delete(`/channels/${channelId}/messages/${messageId}/reactions/${emoji}/${user}`) .expect(204) .execute(); } export async function removeAllReactions( harness: ApiTestHarness, token: string, channelId: string, messageId: string, ): Promise { await createBuilder(harness, token) .delete(`/channels/${channelId}/messages/${messageId}/reactions`) .expect(204) .execute(); } export async function removeAllReactionsForEmoji( harness: ApiTestHarness, token: string, channelId: string, messageId: string, emoji: string, ): Promise { await createBuilder(harness, token) .delete(`/channels/${channelId}/messages/${messageId}/reactions/${emoji}`) .expect(204) .execute(); } export async function deleteAttachment( harness: ApiTestHarness, token: string, channelId: string, messageId: string, attachmentId: string, ): Promise { await createBuilder(harness, token) .delete(`/channels/${channelId}/messages/${messageId}/attachments/${attachmentId}`) .body(null) .expect(204) .execute(); } export async function updateChannelPermissions( harness: ApiTestHarness, token: string, channelId: string, overwriteId: string, overwrite: { type: number; allow?: string; deny?: string; }, ): Promise { await createBuilder(harness, token) .put(`/channels/${channelId}/permissions/${overwriteId}`) .body(overwrite) .expect(204) .execute(); } export async function editMessageWithAttachments( harness: ApiTestHarness, token: string, channelId: string, messageId: string, updates: { content?: string; attachments?: Array>; embeds?: Array>; flags?: number; }, ): Promise { const msg = await createBuilder(harness, token) .patch(`/channels/${channelId}/messages/${messageId}`) .body(updates) .execute(); if (!msg.id) { throw new Error('Message response missing id'); } return msg; } export async function createFriendship(harness: ApiTestHarness, user1: TestAccount, user2: TestAccount): Promise { 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(); } export async function ensureSessionStarted(harness: ApiTestHarness, token: string): Promise { const me = await createBuilder<{id: string; flags?: string | number}>(harness, token).get('/users/@me').execute(); const HAS_SESSION_STARTED = BigInt(1) << BigInt(39); const currentFlags = BigInt(me.flags ?? 0); if ((currentFlags & HAS_SESSION_STARTED) !== BigInt(0)) { return; } await createBuilder(harness, token) .patch(`/test/users/${me.id}/flags`) .body({ flags: (currentFlags | HAS_SESSION_STARTED).toString(), }) .execute(); } export interface SeedMessageInput { message_id?: string; timestamp?: string; content?: string; author_id?: string; } export interface SeededMessage { message_id: string; bucket: number; timestamp: string; } export interface SeedMessagesResult { messages: Array; buckets_populated: Array; channel_state_updated: boolean; } export async function seedMessages( harness: ApiTestHarness, channelId: string, messages: Array, options?: { authorId?: string; clearExisting?: boolean; skipBucketIndex?: boolean; }, ): Promise { return createBuilder(harness, '') .post('/test/messages/seed') .body({ channel_id: channelId, messages, author_id: options?.authorId, clear_existing: options?.clearExisting ?? false, skip_bucket_index: options?.skipBucketIndex ?? false, }) .execute(); } export async function seedMessagesWithContent( harness: ApiTestHarness, channelId: string, count: number, authorId: string, ): Promise { const messages: Array = []; const baseTime = Date.now() - 3600000; for (let i = 0; i < count; i++) { messages.push({ timestamp: new Date(baseTime + i * 1000).toISOString(), content: `Test message ${i + 1}`, }); } return seedMessages(harness, channelId, messages, {authorId, clearExisting: true}); } export async function seedMessagesAtTimestamps( harness: ApiTestHarness, channelId: string, timestamps: Array, authorId: string, ): Promise { const messages: Array = timestamps.map((ts, i) => ({ timestamp: ts.toISOString(), content: `Test message ${i + 1}`, })); return seedMessages(harness, channelId, messages, {authorId, clearExisting: true}); } export interface ChannelStateResponse { channel_id: string; exists: boolean; created_bucket?: number; has_messages?: boolean; last_message_id?: string | null; last_message_bucket?: number | null; updated_at?: string; } export async function getChannelState(harness: ApiTestHarness, channelId: string): Promise { return createBuilder(harness, '').get(`/test/channels/${channelId}/state`).execute(); } export interface ChannelBucket { bucket: number; updated_at: string; } export interface ChannelBucketsResponse { channel_id: string; buckets: Array; count: number; } export async function getChannelBuckets(harness: ApiTestHarness, channelId: string): Promise { return createBuilder(harness, '').get(`/test/channels/${channelId}/buckets`).execute(); } export async function clearChannelMessages(harness: ApiTestHarness, channelId: string): Promise { await createBuilder<{channel_id: string; cleared: boolean}>(harness, '') .delete(`/test/channels/${channelId}/messages`) .execute(); } export async function markChannelAsIndexed(harness: ApiTestHarness, channelId: string): Promise { await createBuilder<{channel_id: string; indexed_at: string}>(harness, '') .post(`/test/channels/${channelId}/mark-indexed`) .body({}) .execute(); } export async function markGuildChannelsAsIndexed( harness: ApiTestHarness, token: string, guildId: string, ): Promise { const channels = await createBuilder>(harness, token) .get(`/guilds/${guildId}/channels`) .execute(); await Promise.all(channels.map((channel) => markChannelAsIndexed(harness, channel.id))); } export async function markUserDmChannelsAsIndexed(harness: ApiTestHarness, token: string): Promise { const channels = await createBuilder>(harness, token).get('/users/@me/channels').execute(); await Promise.all(channels.map((channel) => markChannelAsIndexed(harness, channel.id))); }