settings: digest opt-in + tone (PATCH /api/settings/digest + UI)
Adds DigestPrefsIn/Out models, PATCH /api/settings/digest endpoint, email digest section in settings.html, and last_email_send context in pages.py. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5c89f4d04a
commit
14fe47103f
4 changed files with 176 additions and 1 deletions
71
tests/test_settings_digest_api.py
Normal file
71
tests/test_settings_digest_api.py
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
"""PATCH /api/settings/digest persists opt-in + tone."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
|
||||
def _build(tmp_path):
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.ext.asyncio import async_sessionmaker, create_async_engine
|
||||
|
||||
from app import db as db_mod
|
||||
from app.auth import sign_session
|
||||
from app.db import Base
|
||||
from app.models import User
|
||||
from app.routers import api as api_router
|
||||
|
||||
engine = create_async_engine(f"sqlite+aiosqlite:///{tmp_path}/s.db")
|
||||
factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
db_mod._engine = engine
|
||||
db_mod._session_factory = factory
|
||||
|
||||
async def _seed():
|
||||
async with engine.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
async with factory() as s:
|
||||
s.add(User(id=1, email="u@x", tier="paid",
|
||||
email_digest_opt_in=True))
|
||||
await s.commit()
|
||||
|
||||
asyncio.run(_seed())
|
||||
app = FastAPI()
|
||||
app.include_router(api_router.router, prefix="/api")
|
||||
return TestClient(app), sign_session(1)
|
||||
|
||||
|
||||
def test_patch_round_trip(tmp_path):
|
||||
client, sess = _build(tmp_path)
|
||||
r = client.patch(
|
||||
"/api/settings/digest",
|
||||
json={"opt_in": False, "tone": "NOVICE"},
|
||||
cookies={"cassandra_session": sess},
|
||||
)
|
||||
assert r.status_code == 200, r.text
|
||||
assert r.json() == {"opt_in": False, "tone": "NOVICE"}
|
||||
|
||||
async def _check():
|
||||
from app import db as db_mod
|
||||
from app.models import User
|
||||
async with db_mod._session_factory() as s:
|
||||
u = await s.get(User, 1)
|
||||
assert u.email_digest_opt_in is False
|
||||
assert u.digest_tone == "NOVICE"
|
||||
asyncio.run(_check())
|
||||
|
||||
|
||||
def test_patch_rejects_invalid_tone(tmp_path):
|
||||
client, sess = _build(tmp_path)
|
||||
r = client.patch(
|
||||
"/api/settings/digest",
|
||||
json={"opt_in": True, "tone": "PRO"},
|
||||
cookies={"cassandra_session": sess},
|
||||
)
|
||||
assert r.status_code == 422
|
||||
|
||||
|
||||
def test_patch_requires_auth(tmp_path):
|
||||
client, _ = _build(tmp_path)
|
||||
r = client.patch("/api/settings/digest",
|
||||
json={"opt_in": True, "tone": "NOVICE"})
|
||||
assert r.status_code in (401, 303)
|
||||
Loading…
Add table
Add a link
Reference in a new issue