- مقالهها ستون status (draft/published) گرفتند — GET /api/site و همه انتهای عمومی فقط منتشرشده را برمیگردانند؛ autosave دیگر پیشنویس نیمهکاره را زنده نمیکند - کلمه کلیدی هدف چکلیست سئو فیلد seo_keyphrase شد (قبلاً localStorage بود) - robots.txt و sitemap.xml از بکاند + سرو /blog و /projects با تزریق title/description/OG/JSON-LD از دیتابیس برای خزندههای بدون JS (app/seo_pages.py) - فرانت: applySeo در seo.ts (OG/توییتر/canonical/JSON-LD)، ادغام پیشنویس برای ادمین، برچسب پیشنویس در وبلاگ، حالت خالی وبلاگ، دکمه انتشار در صفحه نوشتن - تغییر رمز ادمین POST /api/auth/change-password + دیالوگ «رمز» در نوار مدیریت - محدودسازی ورود: ۵ تلاش ناموفق در ۱۵ دقیقه از هر IP → 429 - ساخت دکمه + برای ردیف سئوی صفحههای بدون ردیف (SingletonPill) - تستهای pytest (۱۵ مورد) + پینکردن requirements + حذف package.json سرگردان فرانت
94 lines
2.9 KiB
Python
94 lines
2.9 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()
|
|
yield
|
|
rate_limit._failed.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)
|