fix(api): SSO discovery, channel create and deleted bots (#3390)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-11 13:31:18 +02:00
1 parent edc913096a
commit 0967bf3136
17 files changed
+508 -156

No files matched your search

@@ -4,7 +4,6 @@ import {applicationIdToUserId, createApplicationID, type GuildID} from '@app/api
import {mapGuildMemberToResponse} from '@app/api/guild/GuildModel';
import {Logger} from '@app/api/Logger';
import {createRequestCache} from '@app/api/middleware/RequestCacheMiddleware';
import {remapAuthorMessagesToDeletedUser} from '@app/api/oauth/ApplicationMessageAuthorAnonymization';
import {chunkArray} from '@app/api/utils/ArrayUtils';
import {getWorkerDependencies} from '@app/api/worker/WorkerContext';
import {
@@ -25,15 +24,8 @@ const applicationProcessDeletion: WorkerTaskHandler = async (payload, helpers) =
helpers.logger.debug({payload: validated}, 'Processing applicationProcessDeletion task');
const applicationId = createApplicationID(BigInt(validated.applicationId));
const botUserId = applicationIdToUserId(applicationId);
const {
userRepository,
guildRepository,
channelRepository,
applicationRepository,
userCacheService,
gatewayService,
snowflakeService,
} = getWorkerDependencies();
const {userRepository, guildRepository, applicationRepository, userCacheService, gatewayService} =
getWorkerDependencies();
Logger.debug({applicationId, botUserId}, 'Starting application deletion');
try {
const application = await applicationRepository.getApplication(applicationId);
@@ -42,21 +34,8 @@ const applicationProcessDeletion: WorkerTaskHandler = async (payload, helpers) =
return;
}
const botUser = await userRepository.findUniqueAssert(botUserId);
const replacementAuthorId = await remapAuthorMessagesToDeletedUser({
originalAuthorId: botUserId,
channelRepository,
userRepository,
snowflakeService,
});
if (botUser.flags & UserFlags.DELETED) {
Logger.info(
{
applicationId,
botUserId,
replacementAuthorId: replacementAuthorId?.toString() ?? null,
},
'Bot user already marked as deleted, skipping profile update',
);
Logger.info({applicationId, botUserId}, 'Bot user already marked as deleted, skipping profile update');
await applicationRepository.deleteApplication(applicationId);
return;
}
@@ -66,6 +45,11 @@ const applicationProcessDeletion: WorkerTaskHandler = async (payload, helpers) =
username: DELETED_USER_USERNAME,
global_name: DELETED_USER_GLOBAL_NAME,
discriminator: DELETED_USER_DISCRIMINATOR,
avatar_hash: null,
banner_hash: null,
bio: null,
pronouns: null,
accent_color: null,
},
botUser.toRow(),
);
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {createTestAccount} from '@app/api/auth/tests/AuthTestUtils';
import {createApplicationID, createUserID} from '@app/api/BrandedTypes';
import {getGatewayService} from '@app/api/middleware/ServiceRegistry';
import {
createUserCacheService,
getApplicationRepository,
getGuildRepository,
getUserRepository,
} from '@app/api/middleware/ServiceSingletons';
import {createOAuth2Application, createUniqueApplicationName} from '@app/api/oauth/tests/OAuth2TestUtils';
import {type ApiTestHarness, createApiTestHarness} from '@app/api/test/ApiTestHarness';
import {NoopLogger} from '@app/api/test/mocks/NoopLogger';
import {HTTP_STATUS} from '@app/api/test/TestConstants';
import {createBuilder, createBuilderWithoutAuth} from '@app/api/test/TestRequestBuilder';
import applicationProcessDeletion from '@app/api/worker/tasks/ApplicationProcessDeletion';
import {clearWorkerDependencies, setWorkerDependenciesForTest} from '@app/api/worker/WorkerContext';
import {UserFlags} from '@fluxer/constants/src/UserConstants';
import type {MessageResponse} from '@fluxer/schema/src/domains/message/MessageResponseSchemas';
import type {WorkerTaskHelpers} from '@pkgs/worker/src/contracts/WorkerTask';
import {afterAll, afterEach, beforeAll, beforeEach, describe, expect, test} from 'vitest';
function createHelpers(): WorkerTaskHelpers {
return {
logger: new NoopLogger(),
jobId: 7200000000000000000n,
addJob: async () => 0n,
reportProgress: async () => {},
shouldCancel: async () => false,
setContextLink: async () => {},
};
}
describe('applicationProcessDeletion task', () => {
let harness: ApiTestHarness;
beforeAll(async () => {
harness = await createApiTestHarness();
});
afterAll(async () => {
await harness.shutdown();
});
beforeEach(async () => {
await harness.resetData();
setWorkerDependenciesForTest({
userRepository: getUserRepository(),
guildRepository: getGuildRepository(),
applicationRepository: getApplicationRepository(),
userCacheService: createUserCacheService(),
gatewayService: getGatewayService(),
});
});
afterEach(() => {
clearWorkerDependencies();
});
test('keeps the bot as author of its messages and clears its profile', async () => {
const account = await createTestAccount(harness);
const createResult = await createOAuth2Application(harness, account.token, {
name: createUniqueApplicationName(),
});
const botUserId = createUserID(BigInt(createResult.botUserId));
const userRepository = getUserRepository();
const botUser = await userRepository.findUniqueAssert(botUserId);
await userRepository.patchUpsert(
botUserId,
{
avatar_hash: 'a'.repeat(32),
banner_hash: 'b'.repeat(32),
bio: 'bot bio',
pronouns: 'it/its',
accent_color: 0x4641d9,
},
botUser.toRow(),
);
const guild = await createBuilder<{
id: string;
system_channel_id?: string;
}>(harness, account.token)
.post('/guilds')
.body({name: `Bot Message Retention ${Date.now()}`})
.expect(HTTP_STATUS.OK)
.execute();
const channelId = guild.system_channel_id;
if (!channelId) {
throw new Error('Guild response missing system channel');
}
const seeded = await createBuilder<{
messages: Array<{
message_id: string;
}>;
}>(harness, '')
.post('/test/messages/seed')
.body({
channel_id: channelId,
author_id: createResult.botUserId,
clear_existing: true,
messages: [{content: 'bot message before deletion'}],
})
.expect(HTTP_STATUS.OK)
.execute();
const botMessageId = seeded.messages[0]?.message_id;
if (!botMessageId) {
throw new Error('Seeded message response missing message id');
}
await applicationProcessDeletion({applicationId: createResult.application.id}, createHelpers());
expect(
await getApplicationRepository().getApplication(createApplicationID(BigInt(createResult.application.id))),
).toBeNull();
const deletedBot = await userRepository.findUniqueAssert(botUserId);
expect(deletedBot.isBot).toBe(true);
expect(deletedBot.flags & UserFlags.DELETED).toBe(UserFlags.DELETED);
expect(deletedBot.username).toBe('DeletedUser');
expect(deletedBot.avatarHash).toBeNull();
expect(deletedBot.bannerHash).toBeNull();
expect(deletedBot.bio).toBeNull();
expect(deletedBot.pronouns).toBeNull();
expect(deletedBot.accentColor).toBeNull();
const messages = await createBuilder<Array<MessageResponse>>(harness, account.token)
.get(`/channels/${channelId}/messages`)
.expect(HTTP_STATUS.OK)
.execute();
const foundMessage = messages.find((message) => message.id === botMessageId);
expect(foundMessage?.author.id).toBe(createResult.botUserId);
expect(foundMessage?.author.bot).toBe(true);
expect(foundMessage?.author.username).toBe('DeletedUser');
expect(foundMessage?.author.discriminator).toBe('0000');
expect(foundMessage?.author.avatar).toBeNull();
const authorCount = await createBuilderWithoutAuth<{
count: number;
}>(harness)
.get(`/test/users/${createResult.botUserId}/messages/count`)
.expect(HTTP_STATUS.OK)
.execute();
expect(authorCount.count).toBe(1);
});
});