Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 1s
94 lines
3.6 KiB
TypeScript
94 lines
3.6 KiB
TypeScript
import { Users } from './fixtures/userStates';
|
|
import { Authenticated } from './page-objects/auth';
|
|
import { test, expect } from './utils/test';
|
|
|
|
// We mock the REST `method.call` response instead of registering a real
|
|
// Meteor method that requires 2FA: there isn't one in the codebase right now
|
|
// (every twoFactorRequired/`requireSecondFactor: true` user-facing surface is
|
|
// a typed REST endpoint), and the bug we're guarding against lives inside
|
|
// `ddpOverREST`'s `.catch` — a unit-style server fixture exercises it more
|
|
// reliably than wiring up TOTP for a real account.
|
|
test.use({ storageState: Users.admin.state });
|
|
|
|
const METHOD_NAME = 'rc-test-totp-flow';
|
|
|
|
test.describe('account-totp ddpOverREST preserves the totp-required error', () => {
|
|
test('opens the TOTP modal when the REST envelope carries a `totp-required` DDP frame, and resolves on retry', async ({ page }) => {
|
|
await new Authenticated(page).goto();
|
|
|
|
let calls = 0;
|
|
await page.route(`**/api/v1/method.call/${METHOD_NAME}`, async (route) => {
|
|
calls += 1;
|
|
const body = JSON.parse(route.request().postData() ?? '{}');
|
|
const message = JSON.parse(body.message);
|
|
const lastParam = message.params[message.params.length - 1];
|
|
const hasTotp = lastParam && typeof lastParam === 'object' && lastParam.twoFactorCode && lastParam.twoFactorMethod === 'totp';
|
|
|
|
if (!hasTotp) {
|
|
// First leg: server demands a TOTP code. The body is shaped exactly
|
|
// like `mountResult({ id, error })` in server/api/v1/misc.ts —
|
|
// `message` is a DDP-encoded `result` frame whose `error.error`
|
|
// carries the original `totp-required` code. The bug under test
|
|
// rebuilt this envelope as `error: 'unknown'` and the 2FA modal
|
|
// never opened.
|
|
await route.fulfill({
|
|
status: 400,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
success: false,
|
|
message: JSON.stringify({
|
|
msg: 'result',
|
|
id: message.id,
|
|
error: {
|
|
isClientSafe: true,
|
|
error: 'totp-required',
|
|
reason: 'TOTP Required',
|
|
message: 'TOTP Required [totp-required]',
|
|
errorType: 'Meteor.Error',
|
|
details: { method: 'totp', availableMethods: ['totp'] },
|
|
},
|
|
}),
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
// Second leg: client retried with the code; respond success.
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: 'application/json',
|
|
body: JSON.stringify({
|
|
success: true,
|
|
message: JSON.stringify({
|
|
msg: 'result',
|
|
id: message.id,
|
|
result: { ok: true, code: lastParam.twoFactorCode },
|
|
}),
|
|
}),
|
|
});
|
|
});
|
|
|
|
// Drive the call from the page so it goes through Meteor.callAsync →
|
|
// withAsyncTOTP → ddpOverREST → process2faAsyncReturn — the exact path
|
|
// where the catch handler used to drop `error: 'totp-required'` and the
|
|
// TOTP modal therefore never opened.
|
|
await page.evaluate((method) => {
|
|
(window as any).__totpCallResult = (window as any).Meteor.callAsync(method)
|
|
.then((value: unknown) => ({ ok: true, value }))
|
|
.catch((err: unknown) => ({ ok: false, error: String((err as Error)?.message ?? err) }));
|
|
}, METHOD_NAME);
|
|
|
|
const totpModal = page.getByRole('dialog', { name: 'Enter TOTP password' });
|
|
await expect(totpModal).toBeVisible({ timeout: 10_000 });
|
|
|
|
await totpModal.getByPlaceholder('Enter code here').fill('123456');
|
|
await totpModal.getByRole('button', { name: 'Verify' }).click();
|
|
|
|
await expect(totpModal).toBeHidden();
|
|
|
|
const result = await page.evaluate(() => (window as any).__totpCallResult);
|
|
expect(result).toEqual({ ok: true, value: { ok: true, code: '123456' } });
|
|
expect(calls).toBe(2);
|
|
});
|
|
});
|