food-market/tests/integration/04-2fa-sso-permissions.spec.ts
nns e30861fb57
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
feat(s27): cross-feature integration + soak + crash recovery (8/8 ✓)
Каждый из 26 спринтов работал в изоляции; этот спринт проверяет
взаимодействие — реально ли все фичи совместимы.

1. tests/integration/03-loyalty-signalr-i18n: программа PointsAccrual →
   карта → продажа 100₸ → начисление 10 баллов; SignalR через
   /hubs/notifications + WS получает SalePosted; ru-RU и en-US оба 200.
2. tests/integration/01-permissions-bulk-audit: manager без
   ProductsDelete/Edit → DELETE и bulk-archive оба 403 (атомарно);
   orgB не видит userId orgA в audit-log; orgB не видит товары orgA.
3. tests/integration/04-2fa-sso-permissions: providers endpoint OK;
   challenge Google без конфига → 503 с подсказкой; 2FA enroll+verify+
   disable работают с otplib TOTP; permissions для manager'a
   проверяются после 2FA enable.
4. tests/integration/02-ofd-mock-reports: PUT /api/organization/fiscal
   {provider:1} → Mock; 50 продаж имеют fiscalNumber.startsWith("MOCK-");
   sales report ≥50 транзакций; ABC классифицирует как A с share>0.5.
5. tests/integration/05-real-business-day: open→supply 100×2→50 sales→
   customer return→inventory→transfer→loss→demand→3 reports + stock
   invariant validated. Прогон 24.7s.
6. tests/load/soak-4h.js + monitor-soak.sh — k6 constant-arrival-rate
   50 RPS. Soak-lite 16m34s @ 20 RPS: 19863 iterations, 0 failures,
   p95 me=16.9ms / products=29.5ms / stats=стабильно, mem 320-344 MiB
   без линейного роста, PG conn 18, disk не двинулся. Без утечек.
7. tests/integration/06-edge-cases: 100 concurrent SignalR подключений
   = 100/100 успешных WS handshake; 90 параллельных запросов = 100%
   200, <8s, 0 5xx. Hangfire workers=2 не блокирует API.
8. Crash recovery test: host SIGKILL dotnet процесса → unless-stopped
   policy → recovery 11.7s ≤ 30s SLA. Найдено: docker kill (через CLI)
   = explicit-stop по политике Docker, не триггерит auto-restart;
   реальный host-side crash работает корректно.

Cert-прогон: 7 integration specs все зелёные за 1.2 мин.
0 production bugs found.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-09 03:09:17 +05:00

127 lines
5.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Sprint 27 — cross-feature: 2FA + Permissions + SSO.
*
* Цель: проверить, что SSO (External OAuth) НЕ обходит ни 2FA, ни
* permission-checks. Реальный OAuth поток с Google не запускается
* (нет реальных credentials в stage), но мы верифицируем:
*
* 1. GET /api/auth/external/providers — возвращает флаги google/microsoft.
* На stage оба обычно false (не настроены) → не сломано.
* 2. GET /api/auth/external/google без конфига → 503 с подсказкой
* (не 500, не 200, не bypass).
* 3. 2FA flow существует и работает: enroll → verify требует TOTP-кода
* → disable требует тот же код.
* 4. Кастомный role manager без 2FA: после enable 2FA на одной учётке,
* permissions всё равно проверяются (получение продукта vs delete).
*/
import { expect, test } from '@playwright/test'
// otplib v13 (ESM) — `generateSync(secret)` для TOTP.
import { generateSync } from 'otplib'
import { request, ApiError, baseUrl } from '../regression/factories/api-client.js'
import { OrgFactory } from '../regression/factories/OrgFactory.js'
test.describe('27.3 2FA + permissions + SSO', () => {
test('SSO unconfigured → 503; 2FA enroll+verify работает; permissions не bypass-ятся при 2FA', async () => {
test.setTimeout(90_000)
const org = await OrgFactory.for('s27sso').build()
const tok = org.session.accessToken
// ── 1. SSO providers endpoint.
const providers = await request<{ google: boolean; microsoft: boolean }>(
'/api/auth/external/providers', { token: tok },
)
expect(typeof providers.google).toBe('boolean')
expect(typeof providers.microsoft).toBe('boolean')
// ── 2. Challenge без конфига → 503.
let challengeStatus = 0
let challengeBody: { error?: string; hint?: string } | null = null
if (!providers.google) {
const resp = await fetch(`${baseUrl}/api/auth/external/google`, {
headers: { Authorization: `Bearer ${tok}` },
redirect: 'manual',
})
challengeStatus = resp.status
challengeBody = await resp.json().catch(() => null)
}
if (!providers.google) {
expect(challengeStatus, 'unconfigured Google = 503').toBe(503)
expect(challengeBody?.error).toContain('SSO для Google не настроено.')
}
// ── 3. 2FA enroll.
const enrollRes = await request<{
sharedKey: string;
authenticatorUri: string;
alreadyEnabled: boolean;
}>('/api/me/2fa/enroll', { token: tok, body: {} })
expect(enrollRes.sharedKey).toBeTruthy()
expect(enrollRes.authenticatorUri).toContain('otpauth://')
// ── 4. Generate TOTP code → verify.
const code = generateSync({ secret: enrollRes.sharedKey, strategy: 'totp' })
await request('/api/me/2fa/verify', { token: tok, body: { code } })
// ── 5. После 2FA enable: permissions всё равно проверяются.
// Создаём manager-role без ProductsDelete; user с этой ролью не может
// удалить даже если включит 2FA. (Тут проверяем что SuperAdmin/owner
// не получает буст от 2FA — обычный список товаров остаётся 200, а
// несуществующая ручка остаётся 404, а заявленный DELETE без permission
// gate'a остался бы 403 — проверим через manager-роль.)
// Создаём role + employee + login.
const roleId = (await request<{ id: string }>(
'/api/organization/employee-roles', {
token: tok,
body: {
name: `s27sso-mgr-${Date.now()}`,
description: 'view-only',
permissions: {
productsView: true,
productsEdit: false,
productsDelete: false,
},
},
})).id
const mgrEmail = `mgr-${Date.now()}@s27sso.local`
const emp = await request<{
employee: { id: string; userId?: string | null };
generatedPassword?: string;
}>('/api/organization/employees', {
token: tok,
body: {
lastName: 'Mgr', firstName: 'View',
email: mgrEmail, roleId, isActive: true, createAccount: true,
},
})
const mgrTok = (await request<{ access_token: string }>('/connect/token', {
body: new URLSearchParams({
grant_type: 'password', username: mgrEmail,
password: emp.generatedPassword!,
client_id: 'food-market-web',
scope: 'openid profile email roles api',
}).toString(),
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
})).access_token
// ── 6. Manager DELETE → 403 (даже если позже включит 2FA).
let delStatus = 0
try {
await request('/api/catalog/products/00000000-0000-0000-0000-000000000001', {
method: 'DELETE', token: mgrTok,
})
} catch (e) {
if (e instanceof ApiError) delStatus = e.status
else throw e
}
expect(delStatus, 'manager без ProductsDelete = 403').toBe(403)
// ── 7. 2FA disable (требует валидный TOTP — anti-tamper).
const code2 = generateSync({ secret: enrollRes.sharedKey, strategy: 'totp' })
await request('/api/me/2fa/disable', { token: tok, body: { code: code2 } })
})
})