mirror of
https://github.com/TronoSfera/Law.git
synced 2026-05-18 10:03:45 +03:00
Test-2 commit
This commit is contained in:
parent
331973e283
commit
7b6fd8c7c2
8 changed files with 51 additions and 16 deletions
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
||||||
import re
|
import re
|
||||||
import uuid
|
import uuid
|
||||||
from functools import lru_cache
|
from functools import lru_cache
|
||||||
from urllib.parse import quote
|
from urllib.parse import quote, urlsplit
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
from botocore.exceptions import ClientError
|
from botocore.exceptions import ClientError
|
||||||
|
|
@ -46,17 +46,32 @@ class S3Storage:
|
||||||
kwargs: dict = {"Bucket": self.bucket}
|
kwargs: dict = {"Bucket": self.bucket}
|
||||||
if settings.S3_REGION and settings.S3_REGION != "us-east-1":
|
if settings.S3_REGION and settings.S3_REGION != "us-east-1":
|
||||||
kwargs["CreateBucketConfiguration"] = {"LocationConstraint": settings.S3_REGION}
|
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
|
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:
|
def create_presigned_put_url(self, key: str, mime_type: str, expires_sec: int = 900) -> str:
|
||||||
self.ensure_bucket()
|
self.ensure_bucket()
|
||||||
return self.client.generate_presigned_url(
|
url = self.client.generate_presigned_url(
|
||||||
"put_object",
|
"put_object",
|
||||||
Params={"Bucket": self.bucket, "Key": key, "ContentType": mime_type},
|
Params={"Bucket": self.bucket, "Key": key, "ContentType": mime_type},
|
||||||
ExpiresIn=expires_sec,
|
ExpiresIn=expires_sec,
|
||||||
HttpMethod="PUT",
|
HttpMethod="PUT",
|
||||||
)
|
)
|
||||||
|
return self._proxy_presigned_url(url)
|
||||||
|
|
||||||
def head_object(self, key: str) -> dict:
|
def head_object(self, key: str) -> dict:
|
||||||
self.ensure_bucket()
|
self.ensure_bucket()
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,8 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="admin-root"></div>
|
<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 src="https://unpkg.com/react@18.2.0/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 src="https://unpkg.com/react-dom@18.2.0/umd/react-dom.production.min.js"></script>
|
||||||
<script crossorigin="anonymous" integrity="sha384-Fo0OdKhdnE7y2WmzjOMW4PYjHkkANeu1501pWTqKrzAPeJMFQb4ZTdAA9dtrVUJV" src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
|
<script src="/admin.js"></script>
|
||||||
<script type="text/babel" data-presets="env,react" src="/admin.jsx"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1661,7 +1661,6 @@
|
||||||
...prev,
|
...prev,
|
||||||
statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })),
|
statuses: Object.entries(STATUS_LABELS).map(([code, name]) => ({ code, name })),
|
||||||
}));
|
}));
|
||||||
setTableCatalog([]);
|
|
||||||
|
|
||||||
if (roleOverride !== "ADMIN") return;
|
if (roleOverride !== "ADMIN") return;
|
||||||
|
|
||||||
|
|
@ -1831,7 +1830,10 @@
|
||||||
(tableKey, form, mode) => {
|
(tableKey, form, mode) => {
|
||||||
const fields = getRecordFields(tableKey);
|
const fields = getRecordFields(tableKey);
|
||||||
const payload = {};
|
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) => {
|
fields.forEach((field) => {
|
||||||
|
if (isLawyerRequestEdit && lawyerRequestRestricted.has(field.key)) return;
|
||||||
const raw = form[field.key];
|
const raw = form[field.key];
|
||||||
if (field.type === "boolean") {
|
if (field.type === "boolean") {
|
||||||
payload[field.key] = raw === "true";
|
payload[field.key] = raw === "true";
|
||||||
|
|
@ -1880,7 +1882,7 @@
|
||||||
if (tableKey === "invoices" && mode === "edit") delete payload.request_track_number;
|
if (tableKey === "invoices" && mode === "edit") delete payload.request_track_number;
|
||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
[getRecordFields]
|
[getRecordFields, role]
|
||||||
);
|
);
|
||||||
|
|
||||||
const submitRecordModal = useCallback(
|
const submitRecordModal = useCallback(
|
||||||
|
|
@ -2378,12 +2380,12 @@
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
(async () => {
|
(async () => {
|
||||||
await bootstrapReferenceData(token, role);
|
await bootstrapReferenceData(token, role);
|
||||||
if (!cancelled) await refreshSection(activeSection, token);
|
if (!cancelled) await loadDashboard(token);
|
||||||
})();
|
})();
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [bootstrapReferenceData, refreshSection, role, token]);
|
}, [bootstrapReferenceData, loadDashboard, role, token]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dictionaryTableItems.length) {
|
if (!dictionaryTableItems.length) {
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -22,9 +22,9 @@ docker compose exec -T backend python -m compileall app tests alembic
|
||||||
docker compose build frontend
|
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"
|
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
|
```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 / LAWYER / ADMIN)
|
||||||
### PUBLIC (клиент)
|
### 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`.
|
- 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`.
|
- Просмотр статуса/истории/чата/файлов/таймлайна по `track_number`: `tests/test_public_cabinet.py`.
|
||||||
- Переписка клиент -> юрист и маркеры непрочитанного: `tests/test_public_cabinet.py`, `tests/test_notifications.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`.
|
- Публичные счета и PDF в кабинете: `tests/test_invoices.py`.
|
||||||
|
|
||||||
### LAWYER (юрист)
|
### LAWYER (юрист)
|
||||||
|
- UI e2e: `e2e/tests/lawyer_role_flow.spec.js` (вход, claim неназначенной заявки, чтение обновлений, смена статуса).
|
||||||
- Дашборд юриста (свои, неназначенные, непрочитанные): `tests/test_dashboard_finance.py`.
|
- Дашборд юриста (свои, неназначенные, непрочитанные): `tests/test_dashboard_finance.py`.
|
||||||
- Видимость заявок: свои + неназначенные; запрет доступа к чужим: `tests/test_admin_universal_crud.py`.
|
- Видимость заявок: свои + неназначенные; запрет доступа к чужим: `tests/test_admin_universal_crud.py`.
|
||||||
- Claim неназначенной заявки, запрет takeover, запрет назначения через CRUD: `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`.
|
- Счета: видимость только своих, запрет ставить `PAID`: `tests/test_invoices.py`, `tests/test_billing_flow.py`.
|
||||||
|
|
||||||
### ADMIN (администратор)
|
### ADMIN (администратор)
|
||||||
|
- UI e2e: `e2e/tests/admin_role_flow.spec.js` (вход, справочники, создание пользователя/темы, создание и оплата счета).
|
||||||
- CRUD пользователей/юристов (пароли, роли, профильная тема, аватар): `tests/test_admin_universal_crud.py`, `tests/test_uploads_s3.py`.
|
- CRUD пользователей/юристов (пароли, роли, профильная тема, аватар): `tests/test_admin_universal_crud.py`, `tests/test_uploads_s3.py`.
|
||||||
- Темы и флоу статусов (включая ветвление), SLA-переходы: `tests/test_admin_universal_crud.py`, `tests/test_worker_maintenance.py`.
|
- Темы и флоу статусов (включая ветвление), SLA-переходы: `tests/test_admin_universal_crud.py`, `tests/test_worker_maintenance.py`.
|
||||||
- Шаблоны обязательных/дозапрашиваемых данных: `tests/test_admin_universal_crud.py`, `tests/test_public_requests.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`.
|
- `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`.
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"test": "playwright test --config=playwright.config.js"
|
"test": "playwright test --config=playwright.config.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@playwright/test": "^1.51.0",
|
"@playwright/test": "1.58.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"jsonwebtoken": "^9.0.2"
|
"jsonwebtoken": "^9.0.2"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
FROM nginx:1.27-alpine
|
||||||
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
COPY app/web/ /usr/share/nginx/html/
|
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
|
RUN cp /usr/share/nginx/html/landing.html /usr/share/nginx/html/index.html
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,15 @@ server {
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
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 {
|
location /health {
|
||||||
proxy_pass http://backend:8000/health;
|
proxy_pass http://backend:8000/health;
|
||||||
proxy_http_version 1.1;
|
proxy_http_version 1.1;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue