chore(quality): apply ruff autofixes and formatting (223 fixes, regression-free: 118/118 tests pass)

This commit is contained in:
Agent Zero
2026-06-10 21:24:24 +00:00
parent c53af91d74
commit aec40d03ac
56 changed files with 459 additions and 528 deletions
+67 -21
View File
@@ -44,9 +44,7 @@ async def session_factory(
engine: AsyncEngine,
) -> async_sessionmaker[AsyncSession]:
"""Session factory bound to the test engine."""
return async_sessionmaker(
bind=engine, expire_on_commit=False, class_=AsyncSession
)
return async_sessionmaker(bind=engine, expire_on_commit=False, class_=AsyncSession)
@pytest_asyncio.fixture
@@ -131,7 +129,9 @@ async def second_user(
"/api/v1/auth/login",
data={"username": payload["email"], "password": payload["password"]},
)
assert login_resp.status_code == 200, f"login failed: {login_resp.status_code} {login_resp.text}"
assert login_resp.status_code == 200, (
f"login failed: {login_resp.status_code} {login_resp.text}"
)
token = login_resp.json()["access_token"]
return {
@@ -167,7 +167,12 @@ async def seed_data(
# 2 accounts
acc1 = await client.post(
"/api/v1/accounts/",
json={"name": "Acme Corp", "industry": "sme", "size": "sme", "website": "https://acme.test"},
json={
"name": "Acme Corp",
"industry": "sme",
"size": "sme",
"website": "https://acme.test",
},
headers=headers,
)
assert acc1.status_code == 201, f"seed acc1: {acc1.status_code} {acc1.text}"
@@ -184,7 +189,12 @@ async def seed_data(
# 3 contacts (2 with account, 1 standalone)
c1 = await client.post(
"/api/v1/contacts/",
json={"first_name": "Anna", "last_name": "Schmidt", "email": "anna@acme.example", "account_id": acc1_id},
json={
"first_name": "Anna",
"last_name": "Schmidt",
"email": "anna@acme.example",
"account_id": acc1_id,
},
headers=headers,
)
assert c1.status_code == 201, c1.text
@@ -192,7 +202,12 @@ async def seed_data(
c2 = await client.post(
"/api/v1/contacts/",
json={"first_name": "Bob", "last_name": "Mueller", "email": "bob@globex.example", "account_id": acc2_id},
json={
"first_name": "Bob",
"last_name": "Mueller",
"email": "bob@globex.example",
"account_id": acc2_id,
},
headers=headers,
)
assert c2.status_code == 201, c2.text
@@ -208,13 +223,15 @@ async def seed_data(
# 5 deals (different stages)
deal_ids: list[int] = []
for i, (title, stage) in enumerate([
("Deal A", "lead"),
("Deal B", "qualified"),
("Deal C", "proposal"),
("Deal D", "negotiation"),
("Deal E", "won"),
]):
for i, (title, stage) in enumerate(
[
("Deal A", "lead"),
("Deal B", "qualified"),
("Deal C", "proposal"),
("Deal D", "negotiation"),
("Deal E", "won"),
]
):
d = await client.post(
"/api/v1/deals/",
json={
@@ -230,6 +247,7 @@ async def seed_data(
# 10 activities (4 overdue, 4 future, 2 completed)
from datetime import UTC, datetime, timedelta
past = (datetime.now(UTC) - timedelta(days=2)).isoformat()
future = (datetime.now(UTC) + timedelta(days=2)).isoformat()
future2 = (datetime.now(UTC) + timedelta(days=10)).isoformat()
@@ -237,10 +255,20 @@ async def seed_data(
for i in range(10):
if i < 4:
due = past
body_data: dict[str, object] = {"type": "task", "subject": f"Overdue task {i}", "due_date": due, "deal_id": deal_ids[i % 5]}
body_data: dict[str, object] = {
"type": "task",
"subject": f"Overdue task {i}",
"due_date": due,
"deal_id": deal_ids[i % 5],
}
elif i < 8:
due = future if i % 2 == 0 else future2
body_data = {"type": "call", "subject": f"Upcoming call {i}", "due_date": due, "account_id": acc1_id}
body_data = {
"type": "call",
"subject": f"Upcoming call {i}",
"due_date": due,
"account_id": acc1_id,
}
else:
body_data = {"type": "meeting", "subject": f"Done meeting {i}", "account_id": acc1_id}
a = await client.post("/api/v1/activities/", json=body_data, headers=headers)
@@ -250,18 +278,36 @@ async def seed_data(
# 3 tags
tag_ids: list[int] = []
for name in ["VIP", "Strategic", "Enterprise"]:
t = await client.post("/api/v1/tags/", json={"name": name, "color": "#FF0080"}, headers=headers)
t = await client.post(
"/api/v1/tags/", json={"name": name, "color": "#FF0080"}, headers=headers
)
assert t.status_code == 201, t.text
tag_ids.append(t.json()["id"])
# 4 notes (mixed parents: 2 account, 1 contact, 1 deal)
n1 = await client.post("/api/v1/notes/", json={"body": "Note on Acme", "parent_type": "account", "parent_id": acc1_id}, headers=headers)
n1 = await client.post(
"/api/v1/notes/",
json={"body": "Note on Acme", "parent_type": "account", "parent_id": acc1_id},
headers=headers,
)
assert n1.status_code == 201, n1.text
n2 = await client.post("/api/v1/notes/", json={"body": "Note on Globex", "parent_type": "account", "parent_id": acc2_id}, headers=headers)
n2 = await client.post(
"/api/v1/notes/",
json={"body": "Note on Globex", "parent_type": "account", "parent_id": acc2_id},
headers=headers,
)
assert n2.status_code == 201, n2.text
n3 = await client.post("/api/v1/notes/", json={"body": "Note on Anna", "parent_type": "contact", "parent_id": c1_id}, headers=headers)
n3 = await client.post(
"/api/v1/notes/",
json={"body": "Note on Anna", "parent_type": "contact", "parent_id": c1_id},
headers=headers,
)
assert n3.status_code == 201, n3.text
n4 = await client.post("/api/v1/notes/", json={"body": "Note on Deal A", "parent_type": "deal", "parent_id": deal_ids[0]}, headers=headers)
n4 = await client.post(
"/api/v1/notes/",
json={"body": "Note on Deal A", "parent_type": "deal", "parent_id": deal_ids[0]},
headers=headers,
)
assert n4.status_code == 201, n4.text
return {
+6 -2
View File
@@ -21,7 +21,9 @@ async def test_create_account(client: AsyncClient, auth_headers: dict[str, str])
@pytest.mark.asyncio
async def test_create_account_missing_required(client: AsyncClient, auth_headers: dict[str, str]) -> None:
async def test_create_account_missing_required(
client: AsyncClient, auth_headers: dict[str, str]
) -> None:
resp = await client.post(
"/api/v1/accounts/",
json={"industry": "sme"}, # name missing
@@ -97,7 +99,9 @@ async def test_db_write_account_no_password_field(
assert resp.status_code == 201
async with session_factory() as session:
result = await session.execute(text("SELECT name, industry FROM accounts WHERE name='Schema Test Co'"))
result = await session.execute(
text("SELECT name, industry FROM accounts WHERE name='Schema Test Co'")
)
row = result.fetchone()
assert row is not None
assert row[0] == "Schema Test Co"
+1 -1
View File
@@ -2,7 +2,7 @@
from __future__ import annotations
from datetime import UTC, datetime, timedelta
from datetime import datetime
import pytest
from httpx import AsyncClient
+11 -26
View File
@@ -3,9 +3,7 @@
from __future__ import annotations
import time
from datetime import timedelta
import pytest
from httpx import AsyncClient
from jose import jwt
@@ -43,9 +41,7 @@ async def test_register_success(client: AsyncClient) -> None:
# === FR-1.1 / Akzeptanzkriterium 2: register duplicate email ===
async def test_register_duplicate_email(
client: AsyncClient, registered_user: dict
) -> None:
async def test_register_duplicate_email(client: AsyncClient, registered_user: dict) -> None:
"""AC #2: POST /api/v1/auth/register mit existierender Email → 409."""
# registered_user already exists; trying again with same email (and 2nd user
# would also be blocked by bootstrap). 409 is correct because of the email conflict.
@@ -93,9 +89,7 @@ async def test_register_weak_password(client: AsyncClient) -> None:
# === FR-1.2 / Akzeptanzkriterium 4: login success ===
async def test_login_success(
client: AsyncClient, registered_user: dict
) -> None:
async def test_login_success(client: AsyncClient, registered_user: dict) -> None:
"""AC #4: POST /api/v1/auth/login mit korrekten Credentials → 200 + JWT."""
resp = await client.post(
"/api/v1/auth/login",
@@ -114,9 +108,7 @@ async def test_login_success(
# === FR-1.2 / Akzeptanzkriterium 5: login wrong password ===
async def test_login_wrong_password(
client: AsyncClient, registered_user: dict
) -> None:
async def test_login_wrong_password(client: AsyncClient, registered_user: dict) -> None:
"""AC #5: POST /api/v1/auth/login mit falschem Passwort → 401."""
resp = await client.post(
"/api/v1/auth/login",
@@ -203,12 +195,11 @@ async def test_db_user_has_hashed_password(
) -> None:
"""AC: DB-User wird mit gehashtem password_hash angelegt (kein Klartext)."""
from sqlalchemy import select
from app.models.user import User
async with session_factory() as session:
result = await session.execute(
select(User).where(User.email == registered_user["email"])
)
result = await session.execute(select(User).where(User.email == registered_user["email"]))
user = result.scalar_one()
# bcrypt hashes start with $2b$ (or $2a$ for passlib), never plain text
assert user.password_hash.startswith("$"), (
@@ -216,19 +207,17 @@ async def test_db_user_has_hashed_password(
)
assert user.password_hash != registered_user["password"]
assert len(user.password_hash) > 50, (
"Bcrypt hash should be ~60 chars long, got "
f"{len(user.password_hash)}"
f"Bcrypt hash should be ~60 chars long, got {len(user.password_hash)}"
)
# === Bonus: no default admin bootstrap on startup ===
async def test_no_default_admin_on_startup(
client: AsyncClient, session_factory
) -> None:
async def test_no_default_admin_on_startup(client: AsyncClient, session_factory) -> None:
"""AC: KEIN admin/admin Bootstrap-User beim App-Start (Frisch-DB = leer)."""
from sqlalchemy import select, func
from sqlalchemy import func, select
from app.models.user import User
# Fresh DB → no users
@@ -238,10 +227,6 @@ async def test_no_default_admin_on_startup(
assert count == 0, f"Fresh DB should have 0 users, found {count}"
# Also check: no user with role=admin and well-known email
result = await session.execute(
select(User).where(User.role == "admin")
)
result = await session.execute(select(User).where(User.role == "admin"))
admins = result.scalars().all()
assert len(admins) == 0, (
f"Fresh DB should have no admin users, found {len(admins)}"
)
assert len(admins) == 0, f"Fresh DB should have no admin users, found {len(admins)}"
+8 -7
View File
@@ -13,7 +13,12 @@ async def test_create_contact_with_account(
acc_id = seed_data["account_ids"][0]
resp = await client.post(
"/api/v1/contacts/",
json={"first_name": "Diana", "last_name": "Prince", "email": "diana@x.example", "account_id": acc_id},
json={
"first_name": "Diana",
"last_name": "Prince",
"email": "diana@x.example",
"account_id": acc_id,
},
headers=auth_headers,
)
assert resp.status_code == 201, resp.text
@@ -34,9 +39,7 @@ async def test_create_contact_invalid_account(
@pytest.mark.asyncio
async def test_list_contacts_filter_account(
client: AsyncClient, seed_data: dict
) -> None:
async def test_list_contacts_filter_account(client: AsyncClient, seed_data: dict) -> None:
acc_id = seed_data["account_ids"][0]
resp = await client.get(f"/api/v1/contacts/?account_id={acc_id}", headers=seed_data["headers"])
assert resp.status_code == 200
@@ -45,9 +48,7 @@ async def test_list_contacts_filter_account(
@pytest.mark.asyncio
async def test_search_contacts_by_email(
client: AsyncClient, seed_data: dict
) -> None:
async def test_search_contacts_by_email(client: AsyncClient, seed_data: dict) -> None:
# seed_data creates contact with email 'anna@acme.example' on account 0
resp = await client.get("/api/v1/contacts/?q=anna@acme.example", headers=seed_data["headers"])
assert resp.status_code == 200
+3 -1
View File
@@ -11,7 +11,9 @@ async def test_kpis(client: AsyncClient, seed_data: dict) -> None:
resp = await client.get("/api/v1/dashboard/kpis", headers=seed_data["headers"])
assert resp.status_code == 200, resp.text
body = resp.json()
assert {"open_deals_count", "pipeline_value", "won_this_month", "conversion_rate"} <= set(body.keys())
assert {"open_deals_count", "pipeline_value", "won_this_month", "conversion_rate"} <= set(
body.keys()
)
assert isinstance(body["open_deals_count"], int)
assert isinstance(body["pipeline_value"], (int, float))
assert isinstance(body["won_this_month"], int)
+2
View File
@@ -37,6 +37,7 @@ async def test_update_deal_stage_creates_history(
# Verify DealStageHistory row exists in DB
from sqlalchemy import text
async with session_factory() as session:
result = await session.execute(
text("SELECT to_stage FROM deal_stage_history WHERE deal_id = :did ORDER BY id"),
@@ -74,6 +75,7 @@ async def test_db_write_deal(
) -> None:
"""DB roundtrip: read back a seeded deal directly via SQL."""
from sqlalchemy import text
deal_id = seed_data["deal_ids"][2]
async with session_factory() as session:
result = await session.execute(
+13 -6
View File
@@ -27,6 +27,7 @@ def _read(rel: str) -> str:
# api.js
# ---------------------------------------------------------------------------
def test_api_js_exports_api_and_ApiError():
content = _read("js/api.js")
assert "export class ApiError" in content
@@ -56,6 +57,7 @@ def test_api_js_handles_204_no_content():
# store.js
# ---------------------------------------------------------------------------
def test_store_js_defines_auth_and_notifications():
content = _read("js/store.js")
assert "Alpine.store('auth'" in content
@@ -81,6 +83,7 @@ def test_store_js_notifications_has_push_and_dismiss():
# app.css
# ---------------------------------------------------------------------------
def test_app_css_contains_tailwind_overrides():
content = _read("css/app.css")
# The file defines custom CSS variables (--crm-primary, etc.)
@@ -107,10 +110,10 @@ def test_app_css_has_toast_styles():
# ---------------------------------------------------------------------------
COMPONENTS = [
("account-list.js", "accountList"),
("deal-kanban.js", "dealKanban"),
("activity-list.js", "activityList"),
("dashboard-kpis.js", "dashboardKpis"),
("account-list.js", "accountList"),
("deal-kanban.js", "dealKanban"),
("activity-list.js", "activityList"),
("dashboard-kpis.js", "dashboardKpis"),
]
@@ -120,11 +123,13 @@ def test_alpine_component_defined(filename, factory):
globally with Alpine.data(name, factory)."""
content = _read(f"components/{filename}")
# factory function definition
assert f"export function {factory}" in content, \
assert f"export function {factory}" in content, (
f"{filename} missing `export function {factory}()`"
)
# Alpine.data() registration
assert f"Alpine.data('{factory}'" in content, \
assert f"Alpine.data('{factory}'" in content, (
f"{filename} missing `Alpine.data('{factory}', …)` registration"
)
def test_deal_kanban_handles_drag_and_drop():
@@ -159,6 +164,7 @@ def test_account_list_uses_pagination():
# notifications.js (toast component)
# ---------------------------------------------------------------------------
def test_notifications_js_registers_toast_container():
content = _read("js/components/notifications.js")
assert "toastContainer" in content
@@ -169,6 +175,7 @@ def test_notifications_js_registers_toast_container():
# auth.js
# ---------------------------------------------------------------------------
def test_auth_js_exports_login_logout_register():
content = _read("js/auth.js")
assert "export async function login" in content
+11 -11
View File
@@ -33,18 +33,18 @@ def _read(name: str) -> str:
# Each page: (filename, [required substrings])
# ---------------------------------------------------------------------------
PAGES = [
("index.html", ["Login", "Registrieren", "Anmelden", "Konto erstellen"]),
("app.html", ["Dashboard", "Logout", "Pipeline"]),
("accounts.html", ["Accounts", "Suche (Name)", "Branche", "+ New Account"]),
("accounts-detail.html", ["Account-Detail", "Bearbeiten", "Löschen", "Info"]),
("contacts.html", ["Contacts", "Vorname", "Nachname", "+ New Contact"]),
("contacts-detail.html", ["Contact-Detail", "Bearbeiten", "Löschen"]),
("pipeline.html", ["Pipeline", "Owner-ID (optional)", "+ New Deal", "Ziehe Karten"]),
("activities.html", ["Activities", "+ New Activity", "Fällig"]),
("index.html", ["Login", "Registrieren", "Anmelden", "Konto erstellen"]),
("app.html", ["Dashboard", "Logout", "Pipeline"]),
("accounts.html", ["Accounts", "Suche (Name)", "Branche", "+ New Account"]),
("accounts-detail.html", ["Account-Detail", "Bearbeiten", "Löschen", "Info"]),
("contacts.html", ["Contacts", "Vorname", "Nachname", "+ New Contact"]),
("contacts-detail.html", ["Contact-Detail", "Bearbeiten", "Löschen"]),
("pipeline.html", ["Pipeline", "Owner-ID (optional)", "+ New Deal", "Ziehe Karten"]),
("activities.html", ["Activities", "+ New Activity", "Fällig"]),
("settings-profile.html", ["Mein Profil", "Avatar-URL", "E-Mail-Benachrichtigungen"]),
("settings-users.html", ["User-Verwaltung", "+ New User", "Rolle"]),
("settings-org.html", ["Org-Einstellungen", "Aktuelle Organisation", "v1.1"]),
("404.html", ["404", "Seite nicht gefunden", "Zum Dashboard"]),
("settings-users.html", ["User-Verwaltung", "+ New User", "Rolle"]),
("settings-org.html", ["Org-Einstellungen", "Aktuelle Organisation", "v1.1"]),
("404.html", ["404", "Seite nicht gefunden", "Zum Dashboard"]),
# Dashboard: ships inside app.html. We test the marker text directly there.
]
+24 -18
View File
@@ -43,9 +43,8 @@ def test_no_x_html_in_alpine_templates():
stripped = re.sub(r"<!--.*?-->", "", line)
if XHTML_PATTERN.search(stripped):
offenders.append((path.name, lineno, line.strip()[:120]))
assert not offenders, (
"x-html usage is forbidden (R-5); offenders:\n"
+ "\n".join(f" {n}:{ln}: {snippet}" for n, ln, snippet in offenders)
assert not offenders, "x-html usage is forbidden (R-5); offenders:\n" + "\n".join(
f" {n}:{ln}: {snippet}" for n, ln, snippet in offenders
)
@@ -54,21 +53,23 @@ def test_no_innerHTML_in_alpine_pages():
for path in _all_html_files():
content = path.read_text(encoding="utf-8")
# allow within <script> blocks only if commented out / disabled
assert "innerHTML" not in content, \
assert "innerHTML" not in content, (
f"{path.name} contains innerHTML which is forbidden (R-5)"
)
def test_x_text_is_used_instead():
"""Sanity check: the auth/dashboard pages use x-text for user-derived strings."""
index = (WEBUI / "index.html").read_text(encoding="utf-8")
# The error message div should use x-text (so user input never gets HTML-parsed)
assert "x-text=\"error\"" in index or 'x-text="error"' in index
assert 'x-text="error"' in index or 'x-text="error"' in index
# ---------------------------------------------------------------------------
# R-1: JWT in localStorage
# ---------------------------------------------------------------------------
def test_jwt_uses_localStorage():
"""api.js must read/write the JWT in localStorage, not cookies/sessionStorage."""
api = (WEBUI / "js" / "api.js").read_text(encoding="utf-8")
@@ -90,14 +91,16 @@ def test_jwt_not_in_cookies():
"""Defence in depth: do NOT put the JWT in document.cookie."""
for js in WEBUI.rglob("*.js"):
content = js.read_text(encoding="utf-8")
assert "document.cookie" not in content, \
assert "document.cookie" not in content, (
f"{js.relative_to(ROOT)} must not use document.cookie for the JWT"
)
# ---------------------------------------------------------------------------
# 13.3: CSP-Header in main.py
# ---------------------------------------------------------------------------
def test_csp_header_in_main_py():
"""app/main.py must wire up a middleware (or response-header decorator)
that sets Content-Security-Policy. We don't run the server here — we
@@ -105,29 +108,30 @@ def test_csp_header_in_main_py():
assert MAIN_PY.exists(), f"Missing {MAIN_PY}"
content = MAIN_PY.read_text(encoding="utf-8")
# The middleware / decorator must mention the CSP header value
assert "Content-Security-Policy" in content, \
"app/main.py does not set Content-Security-Policy"
assert "Content-Security-Policy" in content, "app/main.py does not set Content-Security-Policy"
# The header must allow the Tailwind CDN at minimum
assert "cdn.tailwindcss.com" in content, \
assert "cdn.tailwindcss.com" in content, (
"CSP-Header must allow https://cdn.tailwindcss.com in script-src"
)
# And block scripts from arbitrary origins
assert "default-src 'self'" in content or 'default-src "self"' in content, \
assert "default-src 'self'" in content or 'default-src "self"' in content, (
"CSP-Header must set default-src 'self'"
)
def test_csp_blocks_object_embedding():
"""object-src 'none' is part of the agreed lockdown."""
content = MAIN_PY.read_text(encoding="utf-8")
assert "object-src 'none'" in content, \
"CSP-Header must set object-src 'none'"
assert "object-src 'none'" in content, "CSP-Header must set object-src 'none'"
def test_csp_uses_starlette_middleware():
"""Per architecture: CSP is set in a FastAPI/Starlette middleware,
not in nginx / a response handler."""
content = MAIN_PY.read_text(encoding="utf-8")
assert "@app.middleware(\"http\")" in content or '@app.middleware("http")' in content, \
"CSP-Header must be set in an @app.middleware(\"http\") decorator"
assert '@app.middleware("http")' in content or '@app.middleware("http")' in content, (
'CSP-Header must be set in an @app.middleware("http") decorator'
)
# ---------------------------------------------------------------------------
@@ -155,9 +159,9 @@ def test_protected_page_has_auth_gate(filename):
content = (WEBUI / filename).read_text(encoding="utf-8")
# The auth-gate is `x-data="{ init: () => Alpine.store('auth').init() }"`
# or `x-init="init()"` on a component that checks the JWT.
assert "Alpine.store('auth').init()" in content or \
"$store.auth.init()" in content, \
assert "Alpine.store('auth').init()" in content or "$store.auth.init()" in content, (
f"{filename} has no Alpine.store('auth').init() auth-gate"
)
def test_index_page_has_no_auth_gate():
@@ -165,13 +169,15 @@ def test_index_page_has_no_auth_gate():
content = (WEBUI / "index.html").read_text(encoding="utf-8")
# The auth store's init() redirects to /index.html when no JWT exists,
# so it must NOT be called from the login page.
assert "Alpine.store('auth').init()" not in content, \
assert "Alpine.store('auth').init()" not in content, (
"index.html (login page) must not call auth.init() (would cause loop)"
)
def test_404_page_has_no_auth_gate():
"""404 page is reachable without auth (so users can find their way back)."""
content = (WEBUI / "404.html").read_text(encoding="utf-8")
# The 404 must not call auth.init() — it is intentionally public.
assert "Alpine.store('auth').init()" not in content, \
assert "Alpine.store('auth').init()" not in content, (
"404.html must not call auth.init() (public page)"
)
+1 -3
View File
@@ -22,9 +22,7 @@ async def test_health_no_auth_required(client: AsyncClient) -> None:
resp = await client.get("/health")
assert resp.status_code == 200, resp.text
# Even with a bogus header, it should still be 200 (public endpoint)
resp = await client.get(
"/health", headers={"Authorization": "Bearer not-a-real-token"}
)
resp = await client.get("/health", headers={"Authorization": "Bearer not-a-real-token"})
assert resp.status_code == 200, resp.text
+1 -3
View File
@@ -7,9 +7,7 @@ from httpx import AsyncClient
@pytest.mark.asyncio
async def test_create_tag(
client: AsyncClient, auth_headers: dict[str, str]
) -> None:
async def test_create_tag(client: AsyncClient, auth_headers: dict[str, str]) -> None:
resp = await client.post(
"/api/v1/tags/",
json={"name": "Important", "color": "#FF0000"},
+4 -8
View File
@@ -4,7 +4,6 @@ from __future__ import annotations
from httpx import AsyncClient
# === /users/me ===
@@ -36,9 +35,10 @@ async def test_get_me_invalid_token_format(client: AsyncClient) -> None:
async def test_get_me_bogus_token(client: AsyncClient) -> None:
"""GET /api/v1/users/me with a token signed with the wrong key returns 401."""
from jose import jwt
import time
from jose import jwt
bogus = jwt.encode(
{
"sub": "1",
@@ -173,9 +173,7 @@ async def test_list_users_as_sales_rep_forbidden(
# === Refresh & Logout ===
async def test_refresh_token(
client: AsyncClient, auth_headers: dict[str, str]
) -> None:
async def test_refresh_token(client: AsyncClient, auth_headers: dict[str, str]) -> None:
"""POST /api/v1/auth/refresh issues a new token for the current user."""
resp = await client.post("/api/v1/auth/refresh", headers=auth_headers)
assert resp.status_code == 200, resp.text
@@ -185,9 +183,7 @@ async def test_refresh_token(
assert data["expires_in"] > 0
async def test_logout(
client: AsyncClient, auth_headers: dict[str, str]
) -> None:
async def test_logout(client: AsyncClient, auth_headers: dict[str, str]) -> None:
"""POST /api/v1/auth/logout returns 200 with confirmation message."""
resp = await client.post("/api/v1/auth/logout", headers=auth_headers)
assert resp.status_code == 200, resp.text