Test-2 commit

This commit is contained in:
TronoSfera 2026-02-23 22:13:46 +03:00
parent 331973e283
commit 7b6fd8c7c2
8 changed files with 51 additions and 16 deletions

View file

@ -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()

View file

@ -8,9 +8,8 @@
</head>
<body>
<div id="admin-root"></div>
<script crossorigin="anonymous" integrity="sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z" src="https://unpkg.com/react@18/umd/react.production.min.js"></script>
<script crossorigin="anonymous" integrity="sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1" src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"></script>
<script crossorigin="anonymous" integrity="sha384-Fo0OdKhdnE7y2WmzjOMW4PYjHkkANeu1501pWTqKrzAPeJMFQb4ZTdAA9dtrVUJV" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/babel" data-presets="env,react" src="/admin.jsx"></script>
<script src="https://unpkg.com/react@18.2.0/umd/react.production.min.js"></script>
<script src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
<script src="/admin.js"></script>
</body>
</html>

View file

@ -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) {

Binary file not shown.

View file

@ -22,9 +22,9 @@ docker compose exec -T backend python -m compileall app tests alembic
docker compose build frontend
docker compose run --rm --no-deps --entrypoint sh frontend -lc "apk add --no-cache nodejs npm >/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`.

View file

@ -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"
}

View file

@ -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

View file

@ -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;