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
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Overnight progress while 4h-soak runs in background:
1. ApiReferenceDocsJob.cs + scripts/gen-api-reference.py — return-type
regex теперь ловит nested generics любой глубины. Было 195
endpoint'ов в auto-gen reference; стало 240 (+45). EmployeesController
GET /api/organization/employees был пропущен из-за
Task<ActionResult<PagedResult<EmployeeDto>>>.
2. docs/observability.md — добавлен food_market_disk_free_bytes (Sprint 20)
+ раздел "quality-watchdog метрики" (5 метрик textfile exporter'a из
Sprint 26: run_total, step_failure_total, endpoint_p95_ms,
last_run_status, incidents_total). Готовые dashboards теперь содержат
оба JSON (food-market.json + quality-watchdog.json).
3. tests/integration/07-import-export-flows.spec.ts — POST 1C-CSV import
(semicolon-CSV cp1251) → создаются продукты с группой автоматом;
POST /api/org/export (НЕ /api/admin/org-export) → возвращает
{id, status}; orgB не видит export orgA. Прогон 8.2s.
4. tests/food-market.IntegrationTests/PruneQualityTestOrgsTests.cs —
2 [Fact]'a для метода из Sprint 25. Удаляет только quality-* старше
threshold, не трогает реальные org. Требует Testcontainers.
5. .forgejo/workflows/regression.yml — добавлен шаг integration suite
после flows+visual. Telegram: "35 flows + 60 visual + 8 integration".
Soak-real (4h @ 50 RPS) запущен в setsid-detach session, продолжается.
Итоговые числа добавлю в sprint28-progress.md после завершения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
67 lines
3.3 KiB
TypeScript
67 lines
3.3 KiB
TypeScript
/**
|
||
* Sprint 28 — cross-feature: 1C CSV import + GDPR org-export.
|
||
*
|
||
* Закрывает пробел в Sprint 27 интеграциях: проверяет, что
|
||
* import/export flows работают и multi-tenant-чистые.
|
||
*
|
||
* 1. POST /api/catalog/products/import/1c-csv с 1С-форматом тела →
|
||
* создаются продукты с группой автоматически.
|
||
* 2. POST /api/org/export → создаётся OrgExport-job с токеном;
|
||
* GET через токен возвращает поток (ZIP).
|
||
* 3. orgB не видит export'ы orgA (multi-tenant).
|
||
*/
|
||
import { expect, test } from '@playwright/test'
|
||
import { request, ApiError, baseUrl } from '../regression/factories/api-client.js'
|
||
import { OrgFactory } from '../regression/factories/OrgFactory.js'
|
||
|
||
test.describe('27.8 1C CSV import + GDPR org-export', () => {
|
||
test('1C CSV import создаёт товары + org-export multi-tenant clean', async () => {
|
||
test.setTimeout(120_000)
|
||
|
||
const orgA = await OrgFactory.for('s28impA').build()
|
||
const orgB = await OrgFactory.for('s28impB').build()
|
||
|
||
// 1C CSV формат: cp1251 BOM-less; semicolon разделитель.
|
||
// Минимальная схема — название + штрихкод + цена.
|
||
const csv1c = [
|
||
'Наименование;Штрихкод;Цена;Единица;Группа',
|
||
`s28-Coca-Cola-${Date.now()};${Date.now()}1;100,00;шт;Напитки`,
|
||
`s28-Pepsi-${Date.now()};${Date.now()}2;90,00;шт;Напитки`,
|
||
].join('\r\n')
|
||
|
||
const res = await fetch(`${baseUrl}/api/catalog/products/import/1c-csv?autoCreateGroup=true`, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: `Bearer ${orgA.session.accessToken}`,
|
||
'Content-Type': 'text/csv; charset=utf-8',
|
||
},
|
||
body: csv1c,
|
||
})
|
||
expect(res.status, '1C-CSV import возвращает 200').toBe(200)
|
||
const out = await res.json() as { created: number; skipped: number; errors: unknown[]; ids: string[] }
|
||
expect(out.errors.length, 'нет parse-ошибок').toBe(0)
|
||
// Может быть Created или Skipped (если barcode уже использован);
|
||
// главное — endpoint работает.
|
||
expect(out.created + out.skipped).toBeGreaterThanOrEqual(1)
|
||
|
||
// ── Org-export. Запускаем job, ждём ready, скачиваем.
|
||
const exportJob = await request<{ id: string; downloadToken?: string; status: number | string }>(
|
||
'/api/org/export',
|
||
{ token: orgA.session.accessToken, body: {} },
|
||
)
|
||
expect(exportJob.id).toBeTruthy()
|
||
// Status может быть числом (enum int) или строкой — допускаем оба.
|
||
// 0=Pending 1=Running 2=Ready 3=Expired 4=Failed.
|
||
expect([0, 1, 2, 'Pending', 'Running', 'Ready']).toContain(exportJob.status)
|
||
|
||
// ── orgB не видит exports orgA.
|
||
// GET /api/org/export может вернуть либо PagedResult, либо flat list.
|
||
const listB = await request<{ items?: Array<{ id: string }> } | Array<{ id: string }>>(
|
||
'/api/org/export', { token: orgB.session.accessToken },
|
||
)
|
||
const itemsB = Array.isArray(listB) ? listB : (listB.items ?? [])
|
||
const leak = itemsB.find(x => x.id === exportJob.id)
|
||
expect(leak, 'orgB не видит export orgA').toBeFalsy()
|
||
})
|
||
})
|