feat(07.3-03): backend remember_me — TTL split + cookie Max-Age + 3 promoted tests
- services/auth.py: create_refresh_token gains remember_me=False param; selects 16h or 30d TTL (D-09, D-10, D-11) - api/auth.py: LoginRequest.remember_me bool field; _set_refresh_cookie remember_me param for conditional max_age; login handler threads remember_me through both calls (D-11, RM-03) - test_auth_es256.py: promote RM-01, RM-02, RM-03 stubs — all 9 phase tests now PASSED
This commit is contained in:
@@ -234,31 +234,94 @@ async def test_startup_rotation_idempotent(db_session):
|
||||
assert len(markers) == 1
|
||||
|
||||
|
||||
# ── RM helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
async def _do_login(async_client, auth_user, remember_me: bool = False) -> dict:
|
||||
"""POST /api/auth/login for the auth_user and return the response."""
|
||||
from tests.test_auth_api import FakeRedis
|
||||
from main import app
|
||||
app.state.redis = FakeRedis()
|
||||
resp = await async_client.post(
|
||||
"/api/auth/login",
|
||||
json={
|
||||
"email": auth_user["user"].email,
|
||||
"password": "Testpassword123!",
|
||||
"remember_me": remember_me,
|
||||
},
|
||||
)
|
||||
return resp
|
||||
|
||||
|
||||
# ── RM-01: default TTL is 16 hours ───────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason="RM-01: not yet implemented")
|
||||
@pytest.mark.asyncio
|
||||
async def test_default_ttl_16_hours(async_client, db_session, auth_user):
|
||||
pytest.xfail("not yet implemented")
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from sqlalchemy import select
|
||||
from db.models import RefreshToken
|
||||
|
||||
resp = await _do_login(async_client, auth_user, remember_me=False)
|
||||
assert resp.status_code == 200
|
||||
|
||||
uid = auth_user["user"].id
|
||||
result = await db_session.execute(
|
||||
select(RefreshToken)
|
||||
.where(RefreshToken.user_id == uid)
|
||||
.order_by(RefreshToken.id.desc())
|
||||
)
|
||||
row = result.scalars().first()
|
||||
assert row is not None
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
delta = row.expires_at.replace(tzinfo=timezone.utc) - now
|
||||
assert timedelta(hours=15, minutes=30) < delta < timedelta(hours=16, minutes=30)
|
||||
|
||||
|
||||
# ── RM-02: remember_me TTL is 30 days ────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason="RM-02: not yet implemented")
|
||||
@pytest.mark.asyncio
|
||||
async def test_remember_me_ttl_30_days(async_client, db_session, auth_user):
|
||||
pytest.xfail("not yet implemented")
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from sqlalchemy import select
|
||||
from db.models import RefreshToken
|
||||
|
||||
resp = await _do_login(async_client, auth_user, remember_me=True)
|
||||
assert resp.status_code == 200
|
||||
|
||||
uid = auth_user["user"].id
|
||||
result = await db_session.execute(
|
||||
select(RefreshToken)
|
||||
.where(RefreshToken.user_id == uid)
|
||||
.order_by(RefreshToken.id.desc())
|
||||
)
|
||||
row = result.scalars().first()
|
||||
assert row is not None
|
||||
|
||||
now = datetime.now(timezone.utc)
|
||||
delta = row.expires_at.replace(tzinfo=timezone.utc) - now
|
||||
assert timedelta(days=29, hours=23) < delta < timedelta(days=30, hours=1)
|
||||
|
||||
|
||||
# ── RM-03: cookie Max-Age values ─────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.mark.xfail(strict=True, reason="RM-03: not yet implemented")
|
||||
@pytest.mark.asyncio
|
||||
async def test_remember_me_cookie_max_age(async_client, auth_user):
|
||||
pytest.xfail("not yet implemented")
|
||||
# Default (no remember_me): Max-Age = 16 * 3600 = 57600
|
||||
resp_short = await _do_login(async_client, auth_user, remember_me=False)
|
||||
assert resp_short.status_code == 200
|
||||
# Parse raw Set-Cookie header for Max-Age
|
||||
set_cookie_short = resp_short.headers.get("set-cookie", "")
|
||||
assert "Max-Age=57600" in set_cookie_short
|
||||
|
||||
# With remember_me=True: Max-Age = 30 * 86400 = 2592000
|
||||
resp_long = await _do_login(async_client, auth_user, remember_me=True)
|
||||
assert resp_long.status_code == 200
|
||||
set_cookie_long = resp_long.headers.get("set-cookie", "")
|
||||
assert "Max-Age=2592000" in set_cookie_long
|
||||
|
||||
|
||||
# ── CFG-01 satellite: settings has jwt key fields ────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user