Security: deep /api/health (DB+MinIO probes, 503 on failure) + /api/live; rate-limited public forms with honeypots; real client IP behind Traefik (--proxy-headers); TipTap URL sanitization on write (nh3); non-root containers + slim .dockerignore; prod MinIO console bound to 127.0.0.1; HSTS/nosniff on TLS routers; JWT 8h; no default credentials in code. Reliability: Alembic migrations (create_all baseline); graceful shutdown (exec uvicorn); hardened backups (checksums, sanity check, freshness gauge, optional rclone off-site) + restore.sh + hourly db-backup sidecar (ops profile); stack.sh ops tooling (deploy/logs/health/backup/restore/monitoring); .gitattributes LF for shell scripts. Orchestration: json-file log rotation, pinned images, per-service memory limits, edge/internal networks, Traefik ping/accesslog/metrics + flood limiter, conditional minio-setup, pgvector db image, one-shot db-init (umami + aramesh_chatbot databases). Chatbot: unified from site-chatbot branch (RAG + pgvector, SSE, ChatWidget). Monitoring: Prometheus/Alertmanager/Loki/Promtail/Grafana + cAdvisor, node/postgres/blackbox exporters, provisioned dashboards and 10 alert rules; alerts delivered to Bale (tapi.bale.ai — Telegram-compatible). Analytics: Umami on shared Postgres, build-time tracker injection, custom events (contact/newsletter/phone/chat/lang) via frontend/src/analytics.ts. Hygiene: CSV export for contact messages and subscribers; upload image optimization (Pillow); CI script + GitHub-compatible workflow; audit.sh; gen-env.sh for strong credentials.
98 lines
3.1 KiB
Python
98 lines
3.1 KiB
Python
"""پیکربندی تستها — دیتابیس SQLite فایلمحور و دادههای حداقلی.
|
|
|
|
متغیرهای محیطی پیش از ایمپورت app تنظیم میشوند تا settings همانها را بخواند.
|
|
"""
|
|
|
|
import os
|
|
import pathlib
|
|
|
|
os.environ["DATABASE_URL"] = "sqlite:///./test.db?check_same_thread=false"
|
|
os.environ["SECRET_KEY"] = "test-secret-key"
|
|
os.environ["ADMIN_USERNAME"] = "admin"
|
|
os.environ["ADMIN_PASSWORD"] = "test-admin-pass-1"
|
|
os.environ["SITE_URL"] = "https://test.example"
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
|
|
from app import models
|
|
from app.auth import hash_password
|
|
from app.config import settings
|
|
from app.database import Base, SessionLocal, engine
|
|
from app.main import app
|
|
|
|
TEST_DB = pathlib.Path("test.db")
|
|
|
|
DOC = {"type": "doc", "content": [{"type": "paragraph"}]}
|
|
|
|
|
|
@pytest.fixture(scope="session", autouse=True)
|
|
def seed_db(tmp_path_factory):
|
|
# اتصالات ساخت جدول در ایمپورت ماژول باید بسته شوند تا فایل قدیمی حذف شود
|
|
engine.dispose()
|
|
if TEST_DB.exists():
|
|
TEST_DB.unlink()
|
|
Base.metadata.create_all(bind=engine)
|
|
# dist ساختگی فرانتاند برای تست تزریق متا در صفحات جزئیات
|
|
dist = tmp_path_factory.mktemp("dist")
|
|
(dist / "index.html").write_text(
|
|
"<!doctype html><html><head><title>Shell</title></head>"
|
|
"<body><div id=\"root\"></div></body></html>",
|
|
encoding="utf-8",
|
|
)
|
|
settings.frontend_dist = str(dist)
|
|
settings.site_url = "https://test.example"
|
|
|
|
db = SessionLocal()
|
|
try:
|
|
db.add(models.AdminUser(username="admin", password_hash=hash_password("test-admin-pass-1")))
|
|
db.add(models.Brand(name="گروه ساختمانی آرامش"))
|
|
db.add(
|
|
models.Article(
|
|
slug="published-post", title="مقاله منتشرشده", status="published",
|
|
excerpt="چکیده مقاله", body=DOC, position=1,
|
|
)
|
|
)
|
|
db.add(
|
|
models.Article(
|
|
slug="draft-post", title="پیشنویس", status="draft",
|
|
excerpt="چکیده پیشنویس", body=DOC, position=2,
|
|
)
|
|
)
|
|
db.commit()
|
|
finally:
|
|
db.close()
|
|
yield
|
|
db.close()
|
|
engine.dispose()
|
|
if TEST_DB.exists():
|
|
TEST_DB.unlink()
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def reset_rate_limiter():
|
|
from app import rate_limit
|
|
|
|
rate_limit._failed.clear()
|
|
rate_limit.contact_limiter._events.clear()
|
|
rate_limit.newsletter_limiter._events.clear()
|
|
yield
|
|
rate_limit._failed.clear()
|
|
rate_limit.contact_limiter._events.clear()
|
|
rate_limit.newsletter_limiter._events.clear()
|
|
|
|
|
|
@pytest.fixture()
|
|
def client():
|
|
return TestClient(app)
|
|
|
|
|
|
@pytest.fixture()
|
|
def auth_client(client):
|
|
res = client.post("/api/auth/login", json={"username": "admin", "password": "test-admin-pass-1"})
|
|
assert res.status_code == 200, res.text
|
|
token = res.json()["access_token"]
|
|
client.headers.update({"Authorization": f"Bearer {token}"})
|
|
yield client
|
|
client.headers.pop("Authorization", None)
|