refactor progress

This commit is contained in:
Hampus Kraft
2026-02-17 12:22:36 +00:00
parent cb31608523
commit d5abd1a7e4
8257 changed files with 1190207 additions and 761040 deletions

View File

@@ -0,0 +1,48 @@
/*
* 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 {createAuthHarness} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {authorizeBot, createOAuth2BotApplication, createTestAccount} from '@fluxer/api/src/bot/tests/BotTestUtils';
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
describe('Bot authorize allows no redirect', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createAuthHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
it('allows bot-only authorize flow without redirect_uri', async () => {
const owner = await createTestAccount(harness);
const endUser = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `Bot No Redirect ${Date.now()}`, []);
const {redirectUrl} = await authorizeBot(harness, endUser.token, botApp.appId, ['bot']);
expect(redirectUrl).toBeTruthy();
expect(typeof redirectUrl).toBe('string');
expect(redirectUrl.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,50 @@
/*
* 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 {createAuthHarness} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {authorizeBot, createOAuth2BotApplication, createTestAccount} from '@fluxer/api/src/bot/tests/BotTestUtils';
import {createGuild} from '@fluxer/api/src/guild/tests/GuildTestUtils';
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
describe('Bot authorize with guild selection redirects', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createAuthHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
it('redirects to guild selection when bot is authorized with guild_id', async () => {
const owner = await createTestAccount(harness);
const endUser = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `Bot Guild Select ${Date.now()}`, []);
const guild = await createGuild(harness, endUser.token, 'Test Guild for Bot Auth');
const {redirectUrl} = await authorizeBot(harness, endUser.token, botApp.appId, ['bot'], guild.id, '8');
expect(redirectUrl).toBeTruthy();
expect(typeof redirectUrl).toBe('string');
});
});

View File

@@ -0,0 +1,201 @@
/*
* 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 {randomUUID} from 'node:crypto';
import {createUniqueEmail, createUniqueUsername, registerUser} 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 {ApplicationResponse} from '@fluxer/schema/src/domains/oauth/OAuthSchemas';
export interface BotAccount {
userId: string;
token: string;
}
export interface TestBotAccount {
appId: string;
botUserId: string;
botToken: string;
clientSecret: string;
ownerEmail: string;
ownerPassword: string;
ownerUserId: string;
ownerToken: string;
}
export async function createTestAccount(harness: ApiTestHarness): Promise<{
email: string;
password: string;
userId: string;
token: string;
}> {
const email = createUniqueEmail('bot_owner');
const username = createUniqueUsername('bot_owner');
const password = 'BotOwnerPassword123!';
const reg = await registerUser(harness, {
email,
username,
global_name: 'Bot Owner',
password,
date_of_birth: '2000-01-01',
consent: true,
});
return {email, password, userId: reg.user_id, token: reg.token};
}
export async function createOAuth2BotApplication(
harness: ApiTestHarness,
ownerToken: string,
name: string,
redirectURIs: Array<string> = [],
): Promise<{appId: string; botUserId: string; botToken: string; clientSecret: string}> {
const app = await createBuilder<ApplicationResponse>(harness, ownerToken)
.post('/oauth2/applications')
.body({
name,
redirect_uris: redirectURIs,
})
.execute();
if (!app.id || !app.client_secret || !app.bot?.id || !app.bot?.token) {
throw new Error('Application response missing required fields');
}
return {
appId: app.id,
botUserId: app.bot.id,
botToken: app.bot.token,
clientSecret: app.client_secret,
};
}
export async function createTestBotAccount(
harness: ApiTestHarness,
params?: {
appName?: string;
redirectURIs?: Array<string>;
},
): Promise<TestBotAccount> {
const owner = await createTestAccount(harness);
const appName = params?.appName ?? `Test Bot ${randomUUID()}`;
const botApp = await createOAuth2BotApplication(harness, owner.token, appName, params?.redirectURIs);
return {
appId: botApp.appId,
botUserId: botApp.botUserId,
botToken: botApp.botToken,
clientSecret: botApp.clientSecret,
ownerEmail: owner.email,
ownerPassword: owner.password,
ownerUserId: owner.userId,
ownerToken: owner.token,
};
}
export async function authenticateWithBotToken(
harness: ApiTestHarness,
botToken: string,
): Promise<{userId: string; username: string}> {
const me = await createBuilder<{id: string; username: string}>(harness, `Bot ${botToken}`)
.get('/users/@me')
.execute();
if (!me.id || !me.username) {
throw new Error('User response missing required fields');
}
return {userId: me.id, username: me.username};
}
export async function resetBotToken(
harness: ApiTestHarness,
ownerToken: string,
appId: string,
password?: string,
): Promise<string> {
const body: Record<string, unknown> = {};
if (password !== undefined) {
body.password = password;
}
const result = await createBuilder<{token: string}>(harness, ownerToken)
.post(`/oauth2/applications/${appId}/bot/reset-token`)
.body(body)
.execute();
if (!result.token) {
throw new Error('Token reset response missing token');
}
return result.token;
}
export async function getGatewayBot(
harness: ApiTestHarness,
botToken: string,
): Promise<{
url: string | null;
shards: number | null;
}> {
const gateway = await createBuilder<{url?: string; shards?: number}>(harness, `Bot ${botToken}`)
.get('/gateway/bot')
.expect(404)
.execute();
return {
url: gateway.url ?? null,
shards: gateway.shards ?? null,
};
}
export async function authorizeBot(
harness: ApiTestHarness,
userToken: string,
clientId: string,
scopes: Array<string>,
guildId?: string,
permissions?: string,
): Promise<{redirectUrl: string}> {
const body: Record<string, unknown> = {
client_id: clientId,
scope: scopes.join(' '),
};
if (guildId) {
body.guild_id = guildId;
}
if (permissions) {
body.permissions = permissions;
}
const consent = await createBuilder<{redirect_to: string}>(harness, userToken)
.post('/oauth2/authorize/consent')
.body(body)
.execute();
if (!consent.redirect_to) {
throw new Error('Authorization response missing redirect_to');
}
return {redirectUrl: consent.redirect_to};
}

View File

@@ -0,0 +1,138 @@
/*
* 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 {createAuthHarness, totpCodeNow} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
authenticateWithBotToken,
createOAuth2BotApplication,
createTestAccount,
resetBotToken,
} from '@fluxer/api/src/bot/tests/BotTestUtils';
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
describe('Bot token reset', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createAuthHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
it('resets bot tokens through OAuth2 application API', async () => {
const owner = await createTestAccount(harness);
const appName = `Test Bot Reset ${Date.now()}`;
const redirectURI = 'https://example.com/callback';
const botApp = await createOAuth2BotApplication(harness, owner.token, appName, [redirectURI]);
expect(botApp.botToken).toBeTruthy();
expect(botApp.botToken.length).toBeGreaterThan(20);
const botAccount = await authenticateWithBotToken(harness, botApp.botToken);
expect(botAccount.userId).toBe(botApp.botUserId);
const newToken = await resetBotToken(harness, owner.token, botApp.appId, owner.password);
expect(newToken).toBeTruthy();
expect(newToken).not.toBe(botApp.botToken);
const newBotAccount = await authenticateWithBotToken(harness, newToken);
expect(newBotAccount.userId).toBe(botApp.botUserId);
await createBuilder(harness, `Bot ${botApp.botToken}`).get('/users/@me').expect(HTTP_STATUS.UNAUTHORIZED).execute();
const anotherToken = await resetBotToken(harness, owner.token, botApp.appId, owner.password);
expect(anotherToken).toBeTruthy();
expect(anotherToken).not.toBe(newToken);
expect(anotherToken).not.toBe(botApp.botToken);
const finalBotAccount = await authenticateWithBotToken(harness, anotherToken);
expect(finalBotAccount.userId).toBe(botApp.botUserId);
await createBuilder(harness, `Bot ${newToken}`).get('/users/@me').expect(HTTP_STATUS.UNAUTHORIZED).execute();
});
it('requires sudo mode for MFA-enabled accounts', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `MFA Bot ${Date.now()}`, []);
const secret = 'JBSWY3DPEHPK3PXP';
const totpCode = totpCodeNow(secret);
const totpData = await createBuilder<{backup_codes: Array<{code: string}>}>(harness, owner.token)
.post('/users/@me/mfa/totp/enable')
.body({secret, code: totpCode, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
expect(totpData.backup_codes.length).toBeGreaterThan(0);
const loginResp = await createBuilderWithoutAuth<{mfa: true; ticket: string}>(harness)
.post('/auth/login')
.body({email: owner.email, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
expect(loginResp.mfa).toBe(true);
const mfaLoginResp = await createBuilderWithoutAuth<{token: string}>(harness)
.post('/auth/login/mfa/totp')
.body({code: totpCodeNow(secret), ticket: loginResp.ticket})
.expect(HTTP_STATUS.OK)
.execute();
const mfaToken = mfaLoginResp.token;
await createBuilder(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({})
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
const totpResetResp = await createBuilder<{token: string}>(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({
mfa_method: 'totp',
mfa_code: totpCodeNow(secret),
})
.expect(HTTP_STATUS.OK)
.execute();
expect(totpResetResp.token).toBeTruthy();
expect(totpResetResp.token).not.toBe(botApp.botToken);
const newBotAccount = await authenticateWithBotToken(harness, totpResetResp.token);
expect(newBotAccount.userId).toBe(botApp.botUserId);
const secondResetResp = await createBuilder<{token: string}>(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({
mfa_method: 'totp',
mfa_code: totpCodeNow(secret),
})
.expect(HTTP_STATUS.OK)
.execute();
expect(secondResetResp.token).toBeTruthy();
expect(secondResetResp.token).not.toBe(totpResetResp.token);
const finalBotAccount = await authenticateWithBotToken(harness, secondResetResp.token);
expect(finalBotAccount.userId).toBe(botApp.botUserId);
});
});

View File

@@ -0,0 +1,209 @@
/*
* 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 {createAuthHarness, totpCodeNow} from '@fluxer/api/src/auth/tests/AuthTestUtils';
import {
authenticateWithBotToken,
createOAuth2BotApplication,
createTestAccount,
resetBotToken,
} from '@fluxer/api/src/bot/tests/BotTestUtils';
import type {ApiTestHarness} from '@fluxer/api/src/test/ApiTestHarness';
import {HTTP_STATUS} from '@fluxer/api/src/test/TestConstants';
import {createBuilder, createBuilderWithoutAuth} from '@fluxer/api/src/test/TestRequestBuilder';
import {afterEach, beforeEach, describe, expect, it} from 'vitest';
describe('Bot token reset with MFA sudo mode', () => {
let harness: ApiTestHarness;
beforeEach(async () => {
harness = await createAuthHarness();
});
afterEach(async () => {
await harness?.shutdown();
});
it('requires sudo mode verification when user has MFA enabled', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `MFA Bot ${Date.now()}`, []);
const secret = 'JBSWY3DPEHPK3PXP';
const totpCode = totpCodeNow(secret);
const totpData = await createBuilder<{backup_codes: Array<{code: string}>}>(harness, owner.token)
.post('/users/@me/mfa/totp/enable')
.body({secret, code: totpCode, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
expect(totpData.backup_codes.length).toBeGreaterThan(0);
const loginResp = await createBuilderWithoutAuth<{mfa: true; ticket: string}>(harness)
.post('/auth/login')
.body({email: owner.email, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
expect(loginResp.mfa).toBe(true);
const mfaLoginResp = await createBuilderWithoutAuth<{token: string}>(harness)
.post('/auth/login/mfa/totp')
.body({code: totpCodeNow(secret), ticket: loginResp.ticket})
.expect(HTTP_STATUS.OK)
.execute();
const mfaToken = mfaLoginResp.token;
await createBuilder(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({})
.expect(HTTP_STATUS.FORBIDDEN)
.execute();
const totpResetResp = await createBuilder<{token: string}>(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({
mfa_method: 'totp',
mfa_code: totpCodeNow(secret),
})
.expect(HTTP_STATUS.OK)
.execute();
expect(totpResetResp.token).toBeTruthy();
expect(totpResetResp.token).not.toBe(botApp.botToken);
const newBotAccount = await authenticateWithBotToken(harness, totpResetResp.token);
expect(newBotAccount.userId).toBe(botApp.botUserId);
});
it('accepts password for sudo mode verification', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `Password Sudo Bot ${Date.now()}`, []);
const newToken = await resetBotToken(harness, owner.token, botApp.appId, owner.password);
expect(newToken).toBeTruthy();
expect(newToken).not.toBe(botApp.botToken);
const newBotAccount = await authenticateWithBotToken(harness, newToken);
expect(newBotAccount.userId).toBe(botApp.botUserId);
});
it('rejects incorrect password for sudo mode verification', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `Bad Password Bot ${Date.now()}`, []);
await createBuilder(harness, owner.token)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({password: 'wrong-password-12345'})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
const originalBotAccount = await authenticateWithBotToken(harness, botApp.botToken);
expect(originalBotAccount.userId).toBe(botApp.botUserId);
});
it('accepts TOTP code for sudo mode verification', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `TOTP Sudo Bot ${Date.now()}`, []);
const secret = 'JBSWY3DPEHPK3PXP';
const totpCode = totpCodeNow(secret);
await createBuilder(harness, owner.token)
.post('/users/@me/mfa/totp/enable')
.body({secret, code: totpCode, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
const loginResp = await createBuilderWithoutAuth<{mfa: true; ticket: string}>(harness)
.post('/auth/login')
.body({email: owner.email, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
const mfaLoginResp = await createBuilderWithoutAuth<{token: string}>(harness)
.post('/auth/login/mfa/totp')
.body({code: totpCodeNow(secret), ticket: loginResp.ticket})
.expect(HTTP_STATUS.OK)
.execute();
const mfaToken = mfaLoginResp.token;
const totpResetResp = await createBuilder<{token: string}>(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({
mfa_method: 'totp',
mfa_code: totpCodeNow(secret),
})
.expect(HTTP_STATUS.OK)
.execute();
expect(totpResetResp.token).toBeTruthy();
expect(totpResetResp.token).not.toBe(botApp.botToken);
const newBotAccount = await authenticateWithBotToken(harness, totpResetResp.token);
expect(newBotAccount.userId).toBe(botApp.botUserId);
});
it('rejects incorrect TOTP code for sudo mode verification', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `Bad TOTP Bot ${Date.now()}`, []);
const secret = 'JBSWY3DPEHPK3PXP';
const totpCode = totpCodeNow(secret);
await createBuilder(harness, owner.token)
.post('/users/@me/mfa/totp/enable')
.body({secret, code: totpCode, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
const loginResp = await createBuilderWithoutAuth<{mfa: true; ticket: string}>(harness)
.post('/auth/login')
.body({email: owner.email, password: owner.password})
.expect(HTTP_STATUS.OK)
.execute();
const mfaLoginResp = await createBuilderWithoutAuth<{token: string}>(harness)
.post('/auth/login/mfa/totp')
.body({code: totpCodeNow(secret), ticket: loginResp.ticket})
.expect(HTTP_STATUS.OK)
.execute();
const mfaToken = mfaLoginResp.token;
await createBuilder(harness, mfaToken)
.post(`/oauth2/applications/${botApp.appId}/bot/reset-token`)
.body({
mfa_method: 'totp',
mfa_code: '000000',
})
.expect(HTTP_STATUS.BAD_REQUEST)
.execute();
const originalBotAccount = await authenticateWithBotToken(harness, botApp.botToken);
expect(originalBotAccount.userId).toBe(botApp.botUserId);
});
it('allows sudo mode after password verification for non-MFA users', async () => {
const owner = await createTestAccount(harness);
const botApp = await createOAuth2BotApplication(harness, owner.token, `Non-MFA Sudo Bot ${Date.now()}`, []);
const newToken = await resetBotToken(harness, owner.token, botApp.appId, owner.password);
expect(newToken).toBeTruthy();
expect(newToken).not.toBe(botApp.botToken);
const newBotAccount = await authenticateWithBotToken(harness, newToken);
expect(newBotAccount.userId).toBe(botApp.botUserId);
});
});