Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/test-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,16 @@ run does not prove for each one; the taxonomy and cross-repository entries live
`**/v1/buy/paymentInfos` with static payloads, so a green run proves that the screen renders those
payloads, not that the API produces them. Unit tests against the utility pin the payload shapes
instead.
- **The RealUnit quotes and dashboard visual specs answer the admin list themselves.**
`e2e/realunit-quotes.spec.ts` and `e2e/realunit-dashboard.spec.ts` fulfil
`GET /v1/realunit/admin/quotes` (and, on the dashboard, holders, token info, price history,
transactions, and the three admin stats paths buy-volume, holders and registration) with
synthetic fixtures that include `userId`, `userName` and `deactivatedAt`.
They also fulfil staff/bootstrap GETs (`/v1/language`, `/v1/fiat`, `/v1/asset`, `/v1/bankAccount`,
`/v1/country`, `/v1/setting/infoBanner`, `/v2/user`) so a synthetic unsigned JWT does not 401.
A green run proves the quote list, pending-table and stats-chart fixtures render. It does not
prove that the API returns those payloads, that login or token verification works, or that those
staff/settings or stats endpoints return real data.
- **Two specs force KYC completeness.** Both collection-invoice cases — the refused QR and the
stored-detail error — override `**/v2/user` so that `kyc.dataComplete` is read as `true`, because
the invoice button is gated on that value. A green run therefore proves nothing about the gate for
Expand All @@ -131,6 +141,15 @@ run does not prove for each one; the taxonomy and cross-repository entries live
fixture with `{ userData: { verifiedName } }`. A green run proves that the review screen
accepts that name, not that the API returns the logged-in staff member's `verifiedName`.
The spec stays on the AML reset path and does not assert the Editor label.
- **The call-queue outcome spec answers staff identity and the dossier itself.**
`e2e/compliance-call-queue-outcome.spec.ts` fulfils `GET /v1/support/issue/clerk` with
`{ clerk }`, a differently named fallback on `GET /v1/support/{staffAccount}`,
`GET /v1/support/{customer}` with a synthetic dossier, empty lookup lists, a null
info banner, and `GET /v2/user` with a synthetic account. A green run proves the
outcome form renders that clerk name as a read-only signature and does not request a
clerks list, not that the API returns those records or the logged-in staff member's
`verifiedName`. The session is a synthetic unsigned JWT, so a green run also does not
prove login or token verification.
- **Full-stack guest assign/refund specs SQL-write `transaction.actionSecretHash`.**
`e2e-stack/specs/transactions.spec.ts` (`seedActionSecret`) updates the hash directly. A green run
does **not** prove that the mail/API path creates, hashes, or delivers the action secret.
Expand Down
5 changes: 3 additions & 2 deletions e2e-stack/specs/realunit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ test.describe('RealUnit area', () => {
await expect(page.getByRole('heading', { name: 'Pending Transactions' })).toBeVisible();
await expect(tableHeader(page, 'Type')).toBeVisible();
await expect(tableHeader(page, 'Amount')).toBeVisible();
await expect(tableHeader(page, 'User')).toBeVisible();
await expect(tableHeader(page, 'Address')).toBeVisible();
await expect(tableHeader(page, 'Name')).toBeVisible();
await expect(tableHeader(page, 'Created')).toBeVisible();

// Empty success → "No pending transactions found"; fetch failure → ErrorHint with quotesError copy.
Expand Down Expand Up @@ -379,7 +380,7 @@ test.describe('RealUnit area', () => {
await expect(page.getByRole('heading', { name: 'Received Transactions' })).toBeVisible();
await expect(tableHeader(page, 'Type')).toBeVisible();
await expect(tableHeader(page, 'Amount CHF')).toBeVisible();
await expect(tableHeader(page, 'User')).toBeVisible();
await expect(tableHeader(page, 'Address')).toBeVisible();
await expect(tableHeader(page, 'Date')).toBeVisible();

const emptyOrError = page
Expand Down
201 changes: 201 additions & 0 deletions e2e/compliance-call-queue-outcome.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
import { expect, Page, Route, test } from '@playwright/test';

const USER_DATA_ID = 2001;
const STAFF_ACCOUNT = 9001;
const QUEUE = 'ManualCheckPhone';
const SIGNATURE = 'Jane Clerk';

const userFixture = {
userData: {
id: USER_DATA_ID,
created: '2026-07-31T08:00:00.000Z',
status: 'Active',
riskStatus: 'Normal',
kycStatus: 'Completed',
kycLevel: 50,
accountType: 'Personal',
firstname: 'Test',
surname: 'Customer',
verifiedName: 'Test Customer',
mail: 'test.customer@example.com',
phone: '+41791234567',
language: { name: 'German', symbol: 'DE' },
nationality: { name: 'Switzerland', symbol: 'CH' },
country: { name: 'Switzerland', symbol: 'CH' },
phoneCallStatus: 'Pending',
phoneCallCheckDate: '2026-07-31T09:00:00.000Z',
},
kycFiles: [],
kycSteps: [],
kycLogs: [],
transactions: [
{
id: 101,
uid: 'synthetic-buy-crypto-42',
buyCryptoId: 42,
type: 'Buy',
sourceType: 'BuyCrypto',
inputAmount: 500,
inputAsset: 'EUR',
outputAsset: 'BTC',
amountInChf: 480,
amlCheck: 'Pass',
amlReason: 'NA',
isCompleted: false,
buyCryptoIsComplete: false,
buyCryptoStatus: 'AMLPending',
buyCryptoHasBatch: false,
buyCryptoHasChargeback: false,
buyCryptoReviewResetBlocked: false,
created: '2026-07-31T08:30:00.000Z',
},
],
bankTxs: [],
cryptoInputs: [],
ipLogs: [],
supportIssues: [],
users: [
{
id: 1,
address: '0x0000000000000000000000000000000000000001',
role: 'User',
status: 'Active',
walletName: 'Synthetic wallet',
created: '2026-07-31T08:00:00.000Z',
},
],
bankDatas: [],
buyRoutes: [],
sellRoutes: [],
swapRoutes: [],
virtualIbans: [],
refRewards: [],
notifications: [],
notes: [],
permissions: {
viewKycFiles: true,
viewKycLogs: true,
viewIpLogs: true,
viewSupportIssues: true,
canRequestLimit: true,
canPerformTransactionActions: true,
viewRecommendation: true,
},
};

function jwt(): string {
const encode = (value: object) => Buffer.from(JSON.stringify(value)).toString('base64url');
return `${encode({ alg: 'none', typ: 'JWT' })}.${encode({
account: STAFF_ACCOUNT,
user: STAFF_ACCOUNT,
role: 'Compliance',
exp: Math.floor(Date.now() / 1000) + 3600,
})}.synthetic`;
}

async function fulfillJson(route: Route, body: unknown): Promise<void> {
await route.fulfill({ status: 200, contentType: 'application/json', body: JSON.stringify(body) });
}

async function installSyntheticApi(page: Page): Promise<{ unexpectedRequests: string[]; seenGets: string[] }> {
const unexpectedRequests: string[] = [];
const seenGets: string[] = [];

await page.route('**/v1/**', async (route) => {
const request = route.request();
const path = new URL(request.url()).pathname;
if (request.method() === 'GET') seenGets.push(path);

if (request.method() === 'GET' && path === `/v1/support/${USER_DATA_ID}`) {
await fulfillJson(route, userFixture);
return;
}

if (request.method() === 'GET' && path === '/v1/support/issue/clerk') {
await fulfillJson(route, { clerk: SIGNATURE });
return;
}

if (request.method() === 'GET' && path === `/v1/support/${STAFF_ACCOUNT}`) {
await fulfillJson(route, { userData: { verifiedName: 'Fallback Name' } });
return;
}

if (
request.method() === 'GET' &&
['/v1/language', '/v1/fiat', '/v1/asset', '/v1/bankAccount', '/v1/country'].includes(path)
) {
await fulfillJson(route, []);
return;
}

if (request.method() === 'GET' && path === '/v1/setting/infoBanner') {
await fulfillJson(route, null);
return;
}

unexpectedRequests.push(`${request.method()} ${path}`);
await route.fulfill({
status: 501,
contentType: 'application/json',
body: JSON.stringify({ error: 'Unexpected test request' }),
});
});

await page.route('**/v2/**', async (route) => {
const request = route.request();
const path = new URL(request.url()).pathname;

if (request.method() === 'GET' && path === '/v2/user') {
await fulfillJson(route, {
id: STAFF_ACCOUNT,
activeAddress: { address: '0x0000000000000000000000000000000000000001' },
addresses: [],
kyc: { level: 50, status: 'Completed' },
language: { id: 1, name: 'German', symbol: 'DE' },
});
return;
}

unexpectedRequests.push(`${request.method()} ${path}`);
await route.fulfill({
status: 501,
contentType: 'application/json',
body: JSON.stringify({ error: 'Unexpected test request' }),
});
});

return { unexpectedRequests, seenGets };
}

test.describe('Call-queue outcome form signature', () => {
// Fixture timestamps are UTC; the screen formats them in the browser locale without an
// explicit timeZone, so pin Zurich like the other fullPage compliance screenshots.
test.use({ timezoneId: 'Europe/Zurich' });

test('shows the logged-in staff verified name and no clerk select', async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 1400 });
const { unexpectedRequests, seenGets } = await installSyntheticApi(page);

await page.goto(`/compliance/call-queues/${QUEUE}/${USER_DATA_ID}?session=${jwt()}&txId=101`);

await expect(page.getByText('Signature', { exact: true })).toBeVisible();
const signatureBlock = page.locator('label', { hasText: 'Signature' }).locator('..');
await expect(signatureBlock.getByRole('combobox')).toHaveCount(0);
await expect(signatureBlock.getByText(SIGNATURE)).toBeVisible();
await expect(
page.getByRole('combobox').filter({ has: page.locator('option', { hasText: SIGNATURE }) }),
).toHaveCount(0);

const form = signatureBlock.locator('xpath=ancestor::div[contains(@class,"rounded-lg")][1]');
await form.scrollIntoViewIfNeeded();
await expect(form).toHaveScreenshot('compliance-call-queue-outcome-signature.png', {
maxDiffPixels: 5000,
});

expect(seenGets).toContain('/v1/support/issue/clerk');
expect(seenGets).not.toContain(`/v1/support/${STAFF_ACCOUNT}`);
expect(unexpectedRequests.filter((r) => r.includes('clerk'))).toEqual([]);
expect(unexpectedRequests).toEqual([]);
});
});
Loading
Loading