refactor progress
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
201
packages/api/src/bot/tests/BotTestUtils.tsx
Normal file
201
packages/api/src/bot/tests/BotTestUtils.tsx
Normal 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};
|
||||
}
|
||||
138
packages/api/src/bot/tests/BotTokenReset.test.tsx
Normal file
138
packages/api/src/bot/tests/BotTokenReset.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
209
packages/api/src/bot/tests/BotTokenResetWithMfaSudoMode.test.tsx
Normal file
209
packages/api/src/bot/tests/BotTokenResetWithMfaSudoMode.test.tsx
Normal 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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user