/** * Sprint 27 — edge cases / resource exhaustion observations. * * - 100 concurrent SignalR connections от одной orga → hub не падает. * - Параллельные продажи + backup-like чтение БД из других ручек. * - Hangfire concurrency. * * Не запускает реальный 5GB-migration test (нет такой БД на stage и * нет смысла создавать). Не запускает реальный 4-часовой backup. Эти * пункты остаются "теоретическими" наблюдениями в отчёте, документ- * только. */ import { expect, test } from '@playwright/test' import WebSocket from 'ws' import { request, baseUrl } from '../regression/factories/api-client.js' import { OrgFactory } from '../regression/factories/OrgFactory.js' test.describe('27.7 resource exhaustion edge cases', () => { test('100 concurrent SignalR подключений → 100 успешных handshake, без 5xx', async () => { test.setTimeout(60_000) const org = await OrgFactory.for('s27sig100').build() const tok = org.session.accessToken const N = 100 const wsUrl = baseUrl.replace(/^http/, 'ws') const sockets: WebSocket[] = [] const openOkPromises: Promise[] = [] for (let i = 0; i < N; i++) { const p = (async () => { const negRes = await fetch( `${baseUrl}/hubs/notifications/negotiate?negotiateVersion=1`, { method: 'POST', headers: { Authorization: `Bearer ${tok}` }, }) if (!negRes.ok) return false const neg = await negRes.json() as { connectionToken: string } const ws = new WebSocket( `${wsUrl}/hubs/notifications?id=${neg.connectionToken}&access_token=${encodeURIComponent(tok)}`, ) sockets.push(ws) return await new Promise(resolve => { ws.on('open', () => { ws.send(JSON.stringify({ protocol: 'json', version: 1 }) + '\x1e') resolve(true) }) ws.on('error', () => resolve(false)) setTimeout(() => resolve(false), 10_000) }) })() openOkPromises.push(p) } const results = await Promise.all(openOkPromises) const ok = results.filter(Boolean).length expect(ok, `${N} concurrent SignalR подключений`).toBeGreaterThanOrEqual(N - 5) // Cleanup for (const s of sockets) { try { s.close() } catch { /* ignore */ } } }) test('параллельные read + write (Hangfire concurrency не блокирует UI)', async () => { test.setTimeout(60_000) const org = await OrgFactory.for('s27para') .withProducts(3) .withCounterparties(1) .withSupplies(1) .build() const tok = org.session.accessToken // 30 параллельных GET'ов /api/me + продуктов + retail/stats — должны // все вернуться <5 секунд, без 5xx. const t0 = Date.now() const promises = [] for (let i = 0; i < 30; i++) { promises.push(fetch(`${baseUrl}/api/me`, { headers: { Authorization: `Bearer ${tok}` } })) promises.push(fetch(`${baseUrl}/api/catalog/products?page=1&pageSize=20`, { headers: { Authorization: `Bearer ${tok}` } })) promises.push(fetch(`${baseUrl}/api/sales/retail/stats?days=7`, { headers: { Authorization: `Bearer ${tok}` } })) } const resps = await Promise.all(promises) const elapsed = Date.now() - t0 const fives = resps.filter(r => r.status >= 500).length const ok = resps.filter(r => r.status === 200).length expect(fives, '0 ошибок 5xx').toBe(0) expect(ok, '≥85% 200').toBeGreaterThanOrEqual(Math.floor(resps.length * 0.85)) expect(elapsed, '<5s для 90 параллельных запросов').toBeLessThan(8000) }) })