From 7b6fd8c7c218e0a15ab391984c4ad7153d2ce861 Mon Sep 17 00:00:00 2001 From: TronoSfera <119615520+TronoSfera@users.noreply.github.com> Date: Mon, 23 Feb 2026 22:13:46 +0300 Subject: [PATCH] Test-2 commit --- app/services/s3_storage.py | 21 ++++++++++++++++++--- app/web/admin.html | 7 +++---- app/web/admin.jsx | 10 ++++++---- celerybeat-schedule | Bin 16384 -> 16384 bytes context/11_test_runbook.md | 10 ++++++---- e2e/package.json | 2 +- frontend/Dockerfile | 8 ++++++++ frontend/nginx.conf | 9 +++++++++ 8 files changed, 51 insertions(+), 16 deletions(-) diff --git a/app/services/s3_storage.py b/app/services/s3_storage.py index 2ec3ce9..553d6d7 100644 --- a/app/services/s3_storage.py +++ b/app/services/s3_storage.py @@ -3,7 +3,7 @@ from __future__ import annotations import re import uuid from functools import lru_cache -from urllib.parse import quote +from urllib.parse import quote, urlsplit import boto3 from botocore.exceptions import ClientError @@ -46,17 +46,32 @@ class S3Storage: kwargs: dict = {"Bucket": self.bucket} if settings.S3_REGION and settings.S3_REGION != "us-east-1": kwargs["CreateBucketConfiguration"] = {"LocationConstraint": settings.S3_REGION} - self.client.create_bucket(**kwargs) + try: + self.client.create_bucket(**kwargs) + except ClientError as create_exc: + create_code = str(create_exc.response.get("Error", {}).get("Code", "")) + if create_code not in {"BucketAlreadyOwnedByYou", "BucketAlreadyExists"}: + raise self._bucket_checked = True + @staticmethod + def _proxy_presigned_url(raw_url: str) -> str: + # Route pre-signed requests through frontend `/s3/*` proxy to avoid browser cross-origin issues. + parts = urlsplit(str(raw_url or "")) + if not parts.path: + return raw_url + query = ("?" + parts.query) if parts.query else "" + return "/s3" + parts.path + query + def create_presigned_put_url(self, key: str, mime_type: str, expires_sec: int = 900) -> str: self.ensure_bucket() - return self.client.generate_presigned_url( + url = self.client.generate_presigned_url( "put_object", Params={"Bucket": self.bucket, "Key": key, "ContentType": mime_type}, ExpiresIn=expires_sec, HttpMethod="PUT", ) + return self._proxy_presigned_url(url) def head_object(self, key: str) -> dict: self.ensure_bucket() diff --git a/app/web/admin.html b/app/web/admin.html index 5ebb06e..bb9e9ff 100644 --- a/app/web/admin.html +++ b/app/web/admin.html @@ -8,9 +8,8 @@
- - - - + + + diff --git a/app/web/admin.jsx b/app/web/admin.jsx index a11fd08..d25d09b 100644 --- a/app/web/admin.jsx +++ b/app/web/admin.jsx @@ -1661,7 +1661,6 @@ ...prev, statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })), })); - setTableCatalog([]); if (roleOverride !== "ADMIN") return; @@ -1831,7 +1830,10 @@ (tableKey, form, mode) => { const fields = getRecordFields(tableKey); const payload = {}; + const isLawyerRequestEdit = tableKey === "requests" && role === "LAWYER"; + const lawyerRequestRestricted = new Set(["assigned_lawyer_id", "effective_rate", "invoice_amount", "paid_at", "paid_by_admin_id"]); fields.forEach((field) => { + if (isLawyerRequestEdit && lawyerRequestRestricted.has(field.key)) return; const raw = form[field.key]; if (field.type === "boolean") { payload[field.key] = raw === "true"; @@ -1880,7 +1882,7 @@ if (tableKey === "invoices" && mode === "edit") delete payload.request_track_number; return payload; }, - [getRecordFields] + [getRecordFields, role] ); const submitRecordModal = useCallback( @@ -2378,12 +2380,12 @@ let cancelled = false; (async () => { await bootstrapReferenceData(token, role); - if (!cancelled) await refreshSection(activeSection, token); + if (!cancelled) await loadDashboard(token); })(); return () => { cancelled = true; }; - }, [bootstrapReferenceData, refreshSection, role, token]); + }, [bootstrapReferenceData, loadDashboard, role, token]); useEffect(() => { if (!dictionaryTableItems.length) { diff --git a/celerybeat-schedule b/celerybeat-schedule index 952570d9883ac4dda91e865fe2c596943cb6aea8..6a25c4cc348cb5cc428a6765852467e35c5bef72 100644 GIT binary patch delta 74 zcmZo@U~Fh$+|X~%FRCKVIOY754AHhJK~ub$C(koa<`-j=W}GGp6BC~-V9^OyHzN`z N22v-t`IvJ/dev/null && npx --yes esbuild /usr/share/nginx/html/admin.jsx --loader:.jsx=jsx --bundle --outfile=/tmp/admin.bundle.js" ``` -5. Браузерный E2E (Playwright) для публичного флоу: +5. Браузерный E2E (Playwright) для ролевых UI-флоу (PUBLIC / LAWYER / ADMIN): ```bash -docker run --rm --network law_default -v "$PWD:/work" -w /work/e2e mcr.microsoft.com/playwright:v1.51.0-noble sh -lc "npm install && E2E_BASE_URL=http://frontend npx playwright test --config=playwright.config.js" +docker run --rm --network law_default -v "$PWD:/work" -w /work/e2e mcr.microsoft.com/playwright:v1.58.2-jammy sh -lc "npm install --silent && E2E_BASE_URL=http://frontend E2E_ADMIN_EMAIL=admin@example.com E2E_ADMIN_PASSWORD='AdminPass-123!' E2E_LAWYER_EMAIL=ivan@mail.ru E2E_LAWYER_PASSWORD='LawyerPass-123!' npx playwright test --config=playwright.config.js" ``` ## Матрица проверок по задачам @@ -60,7 +60,7 @@ docker run --rm --network law_default -v "$PWD:/work" -w /work/e2e mcr.microsoft ## Ролевое покрытие (PUBLIC / LAWYER / ADMIN) ### PUBLIC (клиент) -- Лендинг и клиентский контур (ручной e2e через `http://localhost:8081`): открыть лендинг, создать заявку, открыть кабинет. +- Лендинг и клиентский контур через UI e2e: `e2e/tests/public_client_flow.spec.js` (создание заявки, кабинет, чат, загрузка файла). - OTP create/view + 7-day cookie + rate-limit: `tests/test_public_requests.py`, `tests/test_otp_rate_limit.py`. - Просмотр статуса/истории/чата/файлов/таймлайна по `track_number`: `tests/test_public_cabinet.py`. - Переписка клиент -> юрист и маркеры непрочитанного: `tests/test_public_cabinet.py`, `tests/test_notifications.py`. @@ -68,6 +68,7 @@ docker run --rm --network law_default -v "$PWD:/work" -w /work/e2e mcr.microsoft - Публичные счета и PDF в кабинете: `tests/test_invoices.py`. ### LAWYER (юрист) +- UI e2e: `e2e/tests/lawyer_role_flow.spec.js` (вход, claim неназначенной заявки, чтение обновлений, смена статуса). - Дашборд юриста (свои, неназначенные, непрочитанные): `tests/test_dashboard_finance.py`. - Видимость заявок: свои + неназначенные; запрет доступа к чужим: `tests/test_admin_universal_crud.py`. - Claim неназначенной заявки, запрет takeover, запрет назначения через CRUD: `tests/test_admin_universal_crud.py`. @@ -77,6 +78,7 @@ docker run --rm --network law_default -v "$PWD:/work" -w /work/e2e mcr.microsoft - Счета: видимость только своих, запрет ставить `PAID`: `tests/test_invoices.py`, `tests/test_billing_flow.py`. ### ADMIN (администратор) +- UI e2e: `e2e/tests/admin_role_flow.spec.js` (вход, справочники, создание пользователя/темы, создание и оплата счета). - CRUD пользователей/юристов (пароли, роли, профильная тема, аватар): `tests/test_admin_universal_crud.py`, `tests/test_uploads_s3.py`. - Темы и флоу статусов (включая ветвление), SLA-переходы: `tests/test_admin_universal_crud.py`, `tests/test_worker_maintenance.py`. - Шаблоны обязательных/дозапрашиваемых данных: `tests/test_admin_universal_crud.py`, `tests/test_public_requests.py`. @@ -94,4 +96,4 @@ docker run --rm --network law_default -v "$PWD:/work" -w /work/e2e mcr.microsoft ## Последний регрессионный прогон - `python -m unittest discover -s tests -p 'test_*.py' -v` — `94 tests OK`. -- `Playwright public flow` (`e2e/tests/public_client_flow.spec.js`) — `1 passed`. +- `Playwright UI roles` (`e2e/tests/admin_role_flow.spec.js`, `e2e/tests/lawyer_role_flow.spec.js`, `e2e/tests/public_client_flow.spec.js`) — `3 passed`. diff --git a/e2e/package.json b/e2e/package.json index ee14c48..296185b 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -7,7 +7,7 @@ "test": "playwright test --config=playwright.config.js" }, "devDependencies": { - "@playwright/test": "^1.51.0", + "@playwright/test": "1.58.2", "dotenv": "^16.4.5", "jsonwebtoken": "^9.0.2" } diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 26cf800..404cfd9 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,4 +1,12 @@ +FROM node:22-alpine AS admin-build +WORKDIR /build +COPY app/web/admin.jsx ./admin.jsx +RUN npm init -y >/dev/null 2>&1 \ + && npm install --silent esbuild@0.25.10 \ + && npx esbuild admin.jsx --loader:.jsx=jsx --format=iife --target=es2018 --outfile=admin.js + FROM nginx:1.27-alpine COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf COPY app/web/ /usr/share/nginx/html/ +COPY --from=admin-build /build/admin.js /usr/share/nginx/html/admin.js RUN cp /usr/share/nginx/html/landing.html /usr/share/nginx/html/index.html diff --git a/frontend/nginx.conf b/frontend/nginx.conf index f3b9a5f..f035dc8 100644 --- a/frontend/nginx.conf +++ b/frontend/nginx.conf @@ -40,6 +40,15 @@ server { proxy_set_header X-Forwarded-Proto $scheme; } + location /s3/ { + proxy_pass http://minio:9000/; + proxy_http_version 1.1; + proxy_set_header Host minio:9000; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + location /health { proxy_pass http://backend:8000/health; proxy_http_version 1.1;