mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-11 05:02:24 +09:00
138 lines
5.0 KiB
TypeScript
138 lines
5.0 KiB
TypeScript
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
import {
|
|
type ChannelID,
|
|
channelIdToMessageId,
|
|
createChannelID,
|
|
createGuildID,
|
|
createUserID,
|
|
} from '@app/api/BrandedTypes';
|
|
import {ChannelRepository} from '@app/api/channel/repositories/ChannelRepository';
|
|
import {setCassandraQueryExecutorForTesting, upsertOne} from '@app/api/database/CassandraQueryExecution';
|
|
import type {ChannelRow} from '@app/api/database/types/ChannelTypes';
|
|
import {
|
|
KVThreadAutoArchiveQueueService,
|
|
THREAD_ARCHIVE_GUILDS_KEY,
|
|
threadArchiveQueueKey,
|
|
threadAutoArchiveDueAt,
|
|
} from '@app/api/infrastructure/KVThreadAutoArchiveQueueService';
|
|
import {Channels} from '@app/api/Tables';
|
|
import {InMemoryCassandraQueryExecutor} from '@app/api/test/InMemoryCassandraQueryExecutor';
|
|
import {MockKVProvider} from '@app/api/test/mocks/MockKVProvider';
|
|
import {ChannelTypes} from '@fluxer/constants/src/ChannelConstants';
|
|
import {ChannelFlags} from '@fluxer/constants/src/ThreadConstants';
|
|
import {createSnowflakeFromTimestamp} from '@fluxer/snowflake/src/Snowflake';
|
|
import {beforeEach, describe, expect, it} from 'vitest';
|
|
|
|
const GUILD_ID = createGuildID(1_910_000_000_000_000_000n);
|
|
const PARENT_ID = createChannelID(1_910_000_000_000_000_100n);
|
|
const OWNER_ID = createUserID(1_910_000_000_000_001_000n);
|
|
const HOUR = 3_600_000;
|
|
|
|
function threadRow(threadId: ChannelID): ChannelRow {
|
|
return {
|
|
channel_id: threadId,
|
|
guild_id: GUILD_ID,
|
|
type: ChannelTypes.PUBLIC_THREAD,
|
|
name: 'thread',
|
|
topic: null,
|
|
icon_hash: null,
|
|
url: null,
|
|
parent_id: PARENT_ID,
|
|
position: 0,
|
|
owner_id: OWNER_ID,
|
|
recipient_ids: null,
|
|
nsfw: null,
|
|
content_warning_level: null,
|
|
content_warning_text: null,
|
|
rate_limit_per_user: 0,
|
|
bitrate: null,
|
|
user_limit: null,
|
|
voice_connection_limit: null,
|
|
rtc_region: null,
|
|
last_message_id: null,
|
|
last_pin_timestamp: null,
|
|
permission_overwrites: null,
|
|
nicks: null,
|
|
soft_deleted: false,
|
|
indexed_at: null,
|
|
version: 1,
|
|
};
|
|
}
|
|
|
|
describe('KVThreadAutoArchiveQueueService', () => {
|
|
let kv: MockKVProvider;
|
|
let repositories: ChannelRepository;
|
|
let queue: KVThreadAutoArchiveQueueService;
|
|
let base: number;
|
|
|
|
beforeEach(async () => {
|
|
setCassandraQueryExecutorForTesting(new InMemoryCassandraQueryExecutor());
|
|
kv = new MockKVProvider();
|
|
repositories = new ChannelRepository();
|
|
queue = new KVThreadAutoArchiveQueueService(kv);
|
|
base = Date.now() - 10 * HOUR;
|
|
await upsertOne(Channels.upsertAll({...threadRow(PARENT_ID), type: ChannelTypes.GUILD_TEXT, parent_id: null}));
|
|
});
|
|
|
|
async function createThread(offsetMs: number, duration: number, flags = 0) {
|
|
const threadId = createChannelID(createSnowflakeFromTimestamp(base + offsetMs));
|
|
return repositories.threads.create({
|
|
channel: threadRow(threadId),
|
|
parentType: ChannelTypes.GUILD_TEXT,
|
|
autoArchiveDuration: duration,
|
|
invitable: null,
|
|
flags,
|
|
appliedTags: [],
|
|
hasStarter: false,
|
|
createdAt: new Date(base + offsetMs),
|
|
members: [{userId: OWNER_ID, flags: 1}],
|
|
});
|
|
}
|
|
|
|
it('scores by the later of the last message and the archive timestamp plus the duration', async () => {
|
|
const state = await createThread(0, 60);
|
|
expect(threadAutoArchiveDueAt(state, null)).toBe(base + HOUR);
|
|
const later = channelIdToMessageId(createChannelID(createSnowflakeFromTimestamp(base + 2 * HOUR)));
|
|
expect(threadAutoArchiveDueAt(state, later)).toBe(base + 3 * HOUR);
|
|
});
|
|
|
|
it('returns due threads per guild and tracks the guild index', async () => {
|
|
const due = await createThread(0, 60);
|
|
const notDue = await createThread(1000, 10080);
|
|
await queue.schedule(due, null);
|
|
await queue.schedule(notDue, null);
|
|
expect(await queue.listGuilds()).toEqual([GUILD_ID]);
|
|
expect(await queue.getDue(GUILD_ID, Date.now(), 200)).toEqual([due.threadId]);
|
|
await queue.remove(GUILD_ID, due.threadId);
|
|
expect(await queue.getDue(GUILD_ID, Date.now(), 200)).toEqual([]);
|
|
await queue.remove(GUILD_ID, notDue.threadId);
|
|
expect(await kv.smembers(THREAD_ARCHIVE_GUILDS_KEY)).toEqual([]);
|
|
});
|
|
|
|
it('keeps the guild indexed when a schedule lands between the empty check and the index removal', async () => {
|
|
const removed = await createThread(0, 60);
|
|
const added = await createThread(1000, 60);
|
|
await queue.schedule(removed, null);
|
|
const srem = kv.srem.bind(kv);
|
|
kv.srem = async (key, ...members) => {
|
|
kv.srem = srem;
|
|
await queue.schedule(added, null);
|
|
return srem(key, ...members);
|
|
};
|
|
await queue.remove(GUILD_ID, removed.threadId);
|
|
expect(await queue.listGuilds()).toEqual([GUILD_ID]);
|
|
expect(await queue.getDue(GUILD_ID, Date.now(), 200)).toEqual([added.threadId]);
|
|
});
|
|
|
|
it('never schedules pinned or archived threads', async () => {
|
|
const pinned = await createThread(0, 60, ChannelFlags.PINNED);
|
|
await queue.schedule(pinned, null);
|
|
expect(await kv.zcard(threadArchiveQueueKey(GUILD_ID))).toBe(0);
|
|
const archived = await createThread(10, 60);
|
|
const transition = await repositories.threads.updateState(archived.threadId, () => ({archived: true}));
|
|
await queue.schedule(transition!.state, null);
|
|
expect(await kv.zcard(threadArchiveQueueKey(GUILD_ID))).toBe(0);
|
|
});
|
|
});
|