From 9c12c3f5391c2e55fd35d9d33524c747795252ff Mon Sep 17 00:00:00 2001 From: Dmitri Date: Tue, 7 Jul 2026 11:30:24 +0200 Subject: [PATCH] deleted todo, updated docs --- README.md | 5 +- backend/docs/modules/services.md | 2 +- backend/docs/testing.md | 8 ++ backend/src/services/videoProcessing.ts | 18 ++- backend/tests/video-processing.test.ts | 117 +++++++++++++++++++ docker/README.md | 7 +- docker/docker-compose.yml | 4 +- documentation/project-improvement-todo.ru.md | 71 ----------- 8 files changed, 149 insertions(+), 83 deletions(-) create mode 100644 backend/tests/video-processing.test.ts delete mode 100644 documentation/project-improvement-todo.ru.md diff --git a/README.md b/README.md index 42ed03c..0628270 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,8 @@ rm -rf data && docker-compose up docker-compose up ``` -Access at `http://localhost:3000` +Access the frontend at `http://localhost:3001`, the backend API at +`http://localhost:3000/api`, and Swagger at `http://localhost:3000/api-docs`. ## Environment Variables @@ -204,7 +205,7 @@ DB_HOST=localhost DB_PORT=5432 DB_NAME=app_39215 DB_USER=app_39215 -DB_PASSWORD=your-password +DB_PASS=your-password # JWT SECRET_KEY=your-secret-key diff --git a/backend/docs/modules/services.md b/backend/docs/modules/services.md index 8e0229a..7f6b5ea 100644 --- a/backend/docs/modules/services.md +++ b/backend/docs/modules/services.md @@ -91,7 +91,7 @@ Services with domain-specific business logic beyond simple CRUD. | project_element_defaults | `project_element_defaults.ts` | Element defaults with reset/diff | 34 | | global_ui_control_defaults | `global_ui_control_defaults.ts` | Global defaults CRUD service for system controls | 6 | | project_ui_control_settings | `project_ui_control_settings.ts` | Transactional find/upsert/delete for project UI-control overrides | 51 | -| videoProcessing | `videoProcessing.ts` | FFmpeg video reversal for transition videos with single-worker queue, `-threads 1`, hard timeout, metadata logs, and circuit breaker | ~240 | +| videoProcessing | `videoProcessing.ts` | Direct bundled `ffmpeg-static`/`ffprobe-static` execution for transition video reversal and metadata probing with single-worker queue, `-threads 1`, hard timeout, metadata logs, and circuit breaker | ~400 | ### 3. Specialized Module Directories diff --git a/backend/docs/testing.md b/backend/docs/testing.md index 9a13eb8..ae243e6 100644 --- a/backend/docs/testing.md +++ b/backend/docs/testing.md @@ -24,6 +24,9 @@ The suite is split into three layers: file-storage provider overrides. - Circuit breaker behavior for ignored failures, recorded failures, and open breaker rejection. +- Bundled FFmpeg/FFprobe video processing smoke coverage: availability, + metadata probing, and reverse video generation through + `backend/src/services/videoProcessing.ts`. - Auth service password reset, password update rejection rules, and email verification token behavior with real bcrypt and DB writes. - Route ID/body ID update contract enforcement. @@ -52,6 +55,11 @@ E2E tests bind a local HTTP listener on `127.0.0.1` with an ephemeral port. In restricted sandboxes this may require elevated permission for local socket binding. +The video processing unit test uses the bundled `ffmpeg-static` binary to +generate a tiny temporary fixture and then exercises `ffprobe-static` plus the +backend reverse-video service. It skips only if `ffmpeg-static` does not expose +a local binary path. + ## Test Helpers `backend/tests/http-test-utils.ts` provides `startTestServer(app)`, which starts diff --git a/backend/src/services/videoProcessing.ts b/backend/src/services/videoProcessing.ts index 327db3e..c5077a1 100644 --- a/backend/src/services/videoProcessing.ts +++ b/backend/src/services/videoProcessing.ts @@ -57,6 +57,10 @@ interface ProcessResult { stderr: string; } +function isRecord(value: unknown): value is Record { + return Boolean(value) && typeof value === 'object' && !Array.isArray(value); +} + function parseFrameRate(value: unknown): number | null { if (!value) { return null; @@ -103,7 +107,15 @@ function toError(value: unknown): Error { } function isMediaProbeOutput(value: unknown): value is MediaProbeOutput { - return Boolean(value) && typeof value === 'object'; + if (!isRecord(value)) { + return false; + } + + const { streams, format } = value; + return ( + (streams === undefined || Array.isArray(streams)) && + (format === undefined || isRecord(format)) + ); } function getFfmpegExecutablePath(): string { @@ -115,7 +127,7 @@ function getFfmpegExecutablePath(): string { async function runProcess( executablePath: string, - args: string[], + args: readonly string[], options: { timeoutMs: number; timeoutMessage: string }, ): Promise { return new Promise((resolve, reject) => { @@ -164,7 +176,7 @@ async function runProcess( finish( new Error( - `Process failed: ${executablePath} exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}`, + `Process failed: ${executablePath} exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}: ${stderr}`, ), ); }); diff --git a/backend/tests/video-processing.test.ts b/backend/tests/video-processing.test.ts new file mode 100644 index 0000000..4bee804 --- /dev/null +++ b/backend/tests/video-processing.test.ts @@ -0,0 +1,117 @@ +import assert from 'node:assert/strict'; +import { spawn } from 'node:child_process'; +import { promises as fs } from 'node:fs'; +import { createRequire } from 'node:module'; +import os from 'node:os'; +import path from 'node:path'; +import test from 'node:test'; + +import { + isFFmpegAvailable, + probeMediaMetadata, + reverseVideo, +} from '../src/services/videoProcessing.ts'; + +const loadCommonJsModule = createRequire(import.meta.url); +const ffmpegStaticValue: unknown = loadCommonJsModule('ffmpeg-static'); +const ffmpegPath = typeof ffmpegStaticValue === 'string' ? ffmpegStaticValue : null; + +async function runProcess( + executablePath: string, + args: readonly string[], +): Promise { + await new Promise((resolve, reject) => { + const child = spawn(executablePath, args, { + stdio: ['ignore', 'ignore', 'pipe'], + }); + + let stderr = ''; + child.stderr.setEncoding('utf8'); + child.stderr.on('data', (chunk: string) => { + stderr += chunk; + }); + child.on('error', reject); + child.on('close', (code, signal) => { + if (code === 0) { + resolve(); + return; + } + + reject( + new Error( + `Process failed with code ${code ?? 'null'} and signal ${signal ?? 'null'}: ${stderr}`, + ), + ); + }); + }); +} + +async function createFixtureVideo(filePath: string): Promise { + if (!ffmpegPath) { + throw new Error('Bundled FFmpeg binary is unavailable'); + } + + await runProcess(ffmpegPath, [ + '-y', + '-f', + 'lavfi', + '-i', + 'testsrc=size=32x32:rate=5', + '-f', + 'lavfi', + '-i', + 'sine=frequency=1000:sample_rate=44100', + '-t', + '0.4', + '-c:v', + 'libx264', + '-pix_fmt', + 'yuv420p', + '-c:a', + 'aac', + filePath, + ]); +} + +void test('video processing uses bundled FFmpeg and FFprobe binaries', async (t) => { + if (!ffmpegPath) { + t.skip('ffmpeg-static did not provide a bundled binary path'); + return; + } + + const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'video-processing-test-')); + const inputPath = path.join(tempDir, 'input.mp4'); + const reversedPath = path.join(tempDir, 'reversed.mp4'); + + try { + await createFixtureVideo(inputPath); + + assert.equal(await isFFmpegAvailable(), true); + + const inputMetadata = await probeMediaMetadata(inputPath); + assert.equal(inputMetadata.widthPx, 32); + assert.equal(inputMetadata.heightPx, 32); + assert.equal(inputMetadata.frameRate, 5); + assert.ok( + inputMetadata.durationSec !== null && inputMetadata.durationSec > 0, + 'expected probeMediaMetadata to read a positive duration', + ); + + const reversedBuffer = await reverseVideo( + await fs.readFile(inputPath), + 'input.mp4', + ); + assert.ok(reversedBuffer.length > 0); + + await fs.writeFile(reversedPath, reversedBuffer); + const reversedMetadata = await probeMediaMetadata(reversedPath); + assert.equal(reversedMetadata.widthPx, 32); + assert.equal(reversedMetadata.heightPx, 32); + assert.ok( + reversedMetadata.durationSec !== null && reversedMetadata.durationSec > 0, + 'expected reversed video to have a positive duration', + ); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } +}); diff --git a/docker/README.md b/docker/README.md index 4eaa9e7..9f26eee 100644 --- a/docker/README.md +++ b/docker/README.md @@ -12,7 +12,7 @@ ## Run services: - 1. Install docker compose (https://docs.docker.com/compose/install/) + 1. Install Docker Compose (https://docs.docker.com/compose/install/) 2. Move to `docker` folder. All next steps should be done from this folder. @@ -22,7 +22,7 @@ ``` chmod +x start-backend.sh && chmod +x wait-for-it.sh ``` - 4. Download dependend projects for services. + 4. Download dependent projects for services. 5. Review the docker-compose.yml file. Make sure that all services have Dockerfiles. Only db service doesn't require a Dockerfile. @@ -32,7 +32,7 @@ 7.1. With an empty database `rm -rf data && docker-compose up` - 7.2. With a stored (from previus runs) database data `docker-compose up` + 7.2. With stored data from previous runs `docker-compose up` 8. Check the services: @@ -43,4 +43,3 @@ 9. Stop services: 9.1. Just press `Ctr+C` - diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index abaef5e..7c479d1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -6,7 +6,7 @@ services: tty: true # docker run -t environment: - FRONT_PORT=3001 - - NEXT_PUBLIC_BACK_API=http://localhost:3000 + - NEXT_PUBLIC_BACK_API=http://localhost:3000/api ports: - "3001:3001" logging: @@ -16,7 +16,7 @@ services: max-file: "3" db: - image: postgres + image: postgres:14 volumes: - ./data/db:/var/lib/postgresql/data environment: diff --git a/documentation/project-improvement-todo.ru.md b/documentation/project-improvement-todo.ru.md deleted file mode 100644 index 723a75e..0000000 --- a/documentation/project-improvement-todo.ru.md +++ /dev/null @@ -1,71 +0,0 @@ -# TODO по улучшению проекта - -Дата исследования: 2026-06-28 - -Цель: строгий, но не enterprise-heavy backlog для небольшого production-проекта. Система должна стать более предсказуемой: явные boundaries, единые contracts, единая модель доступа, единый подход к server state и API validation. Улучшения выполняются маленькими PR без большого rewrite. - -## Принципы - -- Сначала фиксируем текущее поведение тестами/чеклистами, потом рефакторим. -- Для нового кода целевые правила обязательны; старый код приводится к ним постепенно. -- Не добавляем тяжёлую инфраструктуру "на будущее", но добавляем строгие boundaries там, где уже есть production-risk. -- Для risky changes используем маленькие PR и manual verification. -- Документацию в `documentation/` обновляем только когда реально меняется API, schema, workflow или deployment. - -## Целевые правила архитектуры - -Backend: - -- Route/controller layer: только auth/context, validation, вызов service, response mapping. -- Service/domain layer: business logic, permissions decisions, transactions. -- DB API/repository layer: только data access, filters, includes, pagination. -- Policy layer: вся логика доступа в одном месте, не в отдельных routes. -- Validation layer: все внешние body/query/params проходят schema validation. - -Frontend: - -- Redux: только client/app state. -- TanStack Query: server state, lists/details/mutations/cache invalidation. -- Feature code: новая feature-specific логика живёт рядом с feature, а не размазывается по generic folders. -- `any`, type assertions/casts, disabled hook rules и direct axios calls в feature components не являются целевым стандартом. - -## P1 - Frontend - -### Auth storage - -Сейчас token пишется и в `sessionStorage`, и в `localStorage`. - -TODO: - -- Решить, нужен ли persistent login. -- Если persistent login не нужен, оставить только `sessionStorage`. -- Если нужен, оставить `localStorage`, но осознанно и с коротким TTL/refresh policy. -- Заменить frontend `jsonwebtoken` decode на `jwt-decode` или `/auth/me`. - -## P2 - Security hardening - -### CSP - -Полный CSP может быть сложным из-за embeds/media. Не вводить сразу. - -TODO: - -- Сначала собрать список нужных domains для assets/embeds. -- Если будет время, включить report-only CSP на stage. -- Enforced CSP делать только после проверки runtime presentations. - -## Рекомендуемый порядок - -1. Добавить validation для самых рискованных endpoints и запретить новые routes без validation. -2. Проверить DB slow queries и добавить только реально нужные индексы. -3. Выбрать package manager и Node version. -4. Добавить минимальные smoke checks. -5. Постепенно выносить helpers из больших frontend/backend файлов при изменениях. -6. Делать cleanup/dependency upgrades небольшими отдельными PR. - -## Definition of Done - -- Production workflows проверены вручную или тестом. -- Нет большого rewrite без явной выгоды. -- DB changes имеют backup/rollback plan. -- Документация обновлена только там, где изменилось поведение.