Add ruff linter/formatter for Python
- Add ruff as dev dependency - Configure ruff in pyproject.toml with strict 88-char line limit - Ignore B008 (FastAPI Depends pattern is standard) - Allow longer lines in tests for readability - Fix all lint issues in source files - Add Makefile targets: lint-backend, format-backend, fix-backend
This commit is contained in:
parent
69bc8413e0
commit
6c218130e9
31 changed files with 1234 additions and 876 deletions
|
|
@ -1,9 +1,9 @@
|
|||
"""Tests for user profile and contact details."""
|
||||
import pytest
|
||||
|
||||
from sqlalchemy import select
|
||||
|
||||
from models import User, ROLE_REGULAR
|
||||
from auth import get_password_hash
|
||||
from models import User
|
||||
from tests.helpers import unique_email
|
||||
|
||||
# Valid npub for testing (32 zero bytes encoded as bech32)
|
||||
|
|
@ -16,7 +16,7 @@ class TestUserContactFields:
|
|||
async def test_contact_fields_default_to_none(self, client_factory):
|
||||
"""New users should have all contact fields as None."""
|
||||
email = unique_email("test")
|
||||
|
||||
|
||||
async with client_factory.get_db_session() as db:
|
||||
user = User(
|
||||
email=email,
|
||||
|
|
@ -25,7 +25,7 @@ class TestUserContactFields:
|
|||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
|
||||
assert user.contact_email is None
|
||||
assert user.telegram is None
|
||||
assert user.signal is None
|
||||
|
|
@ -34,7 +34,7 @@ class TestUserContactFields:
|
|||
async def test_contact_fields_can_be_set(self, client_factory):
|
||||
"""Contact fields can be set when creating a user."""
|
||||
email = unique_email("test")
|
||||
|
||||
|
||||
async with client_factory.get_db_session() as db:
|
||||
user = User(
|
||||
email=email,
|
||||
|
|
@ -47,7 +47,7 @@ class TestUserContactFields:
|
|||
db.add(user)
|
||||
await db.commit()
|
||||
await db.refresh(user)
|
||||
|
||||
|
||||
assert user.contact_email == "contact@example.com"
|
||||
assert user.telegram == "@alice"
|
||||
assert user.signal == "alice.42"
|
||||
|
|
@ -56,7 +56,7 @@ class TestUserContactFields:
|
|||
async def test_contact_fields_persist_after_reload(self, client_factory):
|
||||
"""Contact fields should persist in the database."""
|
||||
email = unique_email("test")
|
||||
|
||||
|
||||
async with client_factory.get_db_session() as db:
|
||||
user = User(
|
||||
email=email,
|
||||
|
|
@ -69,12 +69,12 @@ class TestUserContactFields:
|
|||
db.add(user)
|
||||
await db.commit()
|
||||
user_id = user.id
|
||||
|
||||
|
||||
# Reload from database in a new session
|
||||
async with client_factory.get_db_session() as db:
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
loaded_user = result.scalar_one()
|
||||
|
||||
|
||||
assert loaded_user.contact_email == "contact@example.com"
|
||||
assert loaded_user.telegram == "@bob"
|
||||
assert loaded_user.signal == "bob.99"
|
||||
|
|
@ -83,7 +83,7 @@ class TestUserContactFields:
|
|||
async def test_contact_fields_can_be_updated(self, client_factory):
|
||||
"""Contact fields can be updated after user creation."""
|
||||
email = unique_email("test")
|
||||
|
||||
|
||||
async with client_factory.get_db_session() as db:
|
||||
user = User(
|
||||
email=email,
|
||||
|
|
@ -92,21 +92,21 @@ class TestUserContactFields:
|
|||
db.add(user)
|
||||
await db.commit()
|
||||
user_id = user.id
|
||||
|
||||
|
||||
# Update fields
|
||||
async with client_factory.get_db_session() as db:
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one()
|
||||
|
||||
|
||||
user.contact_email = "new@example.com"
|
||||
user.telegram = "@updated"
|
||||
await db.commit()
|
||||
|
||||
|
||||
# Verify update persisted
|
||||
async with client_factory.get_db_session() as db:
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one()
|
||||
|
||||
|
||||
assert user.contact_email == "new@example.com"
|
||||
assert user.telegram == "@updated"
|
||||
assert user.signal is None # Still None
|
||||
|
|
@ -115,7 +115,7 @@ class TestUserContactFields:
|
|||
async def test_contact_fields_can_be_cleared(self, client_factory):
|
||||
"""Contact fields can be set back to None."""
|
||||
email = unique_email("test")
|
||||
|
||||
|
||||
async with client_factory.get_db_session() as db:
|
||||
user = User(
|
||||
email=email,
|
||||
|
|
@ -126,21 +126,21 @@ class TestUserContactFields:
|
|||
db.add(user)
|
||||
await db.commit()
|
||||
user_id = user.id
|
||||
|
||||
|
||||
# Clear fields
|
||||
async with client_factory.get_db_session() as db:
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one()
|
||||
|
||||
|
||||
user.contact_email = None
|
||||
user.telegram = None
|
||||
await db.commit()
|
||||
|
||||
|
||||
# Verify cleared
|
||||
async with client_factory.get_db_session() as db:
|
||||
result = await db.execute(select(User).where(User.id == user_id))
|
||||
user = result.scalar_one()
|
||||
|
||||
|
||||
assert user.contact_email is None
|
||||
assert user.telegram is None
|
||||
|
||||
|
|
@ -152,7 +152,7 @@ class TestGetProfileEndpoint:
|
|||
"""Regular user can fetch their profile."""
|
||||
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert "contact_email" in data
|
||||
|
|
@ -169,7 +169,7 @@ class TestGetProfileEndpoint:
|
|||
"""Admin user gets 403 when trying to access profile."""
|
||||
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 403
|
||||
assert "regular users" in response.json()["detail"].lower()
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ class TestGetProfileEndpoint:
|
|||
"""Unauthenticated user gets 401."""
|
||||
async with client_factory.create() as client:
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_profile_returns_existing_data(self, client_factory, regular_user):
|
||||
|
|
@ -191,11 +191,11 @@ class TestGetProfileEndpoint:
|
|||
user.contact_email = "contact@test.com"
|
||||
user.telegram = "@testuser"
|
||||
await db.commit()
|
||||
|
||||
|
||||
# Fetch via API
|
||||
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["contact_email"] == "contact@test.com"
|
||||
|
|
@ -219,7 +219,7 @@ class TestUpdateProfileEndpoint:
|
|||
"nostr_npub": VALID_NPUB,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["contact_email"] == "new@example.com"
|
||||
|
|
@ -234,10 +234,10 @@ class TestUpdateProfileEndpoint:
|
|||
"/api/profile",
|
||||
json={"telegram": "@persisted"},
|
||||
)
|
||||
|
||||
|
||||
# Fetch again to verify
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["telegram"] == "@persisted"
|
||||
|
||||
|
|
@ -248,7 +248,7 @@ class TestUpdateProfileEndpoint:
|
|||
"/api/profile",
|
||||
json={"telegram": "@admin"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 403
|
||||
|
||||
async def test_unauthenticated_user_gets_401(self, client_factory):
|
||||
|
|
@ -258,7 +258,7 @@ class TestUpdateProfileEndpoint:
|
|||
"/api/profile",
|
||||
json={"telegram": "@test"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 401
|
||||
|
||||
async def test_can_clear_fields(self, client_factory, regular_user):
|
||||
|
|
@ -272,7 +272,7 @@ class TestUpdateProfileEndpoint:
|
|||
"telegram": "@test",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Then clear them
|
||||
response = await client.put(
|
||||
"/api/profile",
|
||||
|
|
@ -283,7 +283,7 @@ class TestUpdateProfileEndpoint:
|
|||
"nostr_npub": None,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["contact_email"] is None
|
||||
|
|
@ -296,7 +296,7 @@ class TestUpdateProfileEndpoint:
|
|||
"/api/profile",
|
||||
json={"contact_email": "not-an-email"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "field_errors" in data["detail"]
|
||||
|
|
@ -309,7 +309,7 @@ class TestUpdateProfileEndpoint:
|
|||
"/api/profile",
|
||||
json={"telegram": "missing_at_sign"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "field_errors" in data["detail"]
|
||||
|
|
@ -322,13 +322,15 @@ class TestUpdateProfileEndpoint:
|
|||
"/api/profile",
|
||||
json={"nostr_npub": "npub1invalid"},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "field_errors" in data["detail"]
|
||||
assert "nostr_npub" in data["detail"]["field_errors"]
|
||||
|
||||
async def test_multiple_invalid_fields_returns_all_errors(self, client_factory, regular_user):
|
||||
async def test_multiple_invalid_fields_returns_all_errors(
|
||||
self, client_factory, regular_user
|
||||
):
|
||||
"""Multiple invalid fields return all errors."""
|
||||
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
||||
response = await client.put(
|
||||
|
|
@ -338,13 +340,15 @@ class TestUpdateProfileEndpoint:
|
|||
"telegram": "no-at",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 422
|
||||
data = response.json()
|
||||
assert "contact_email" in data["detail"]["field_errors"]
|
||||
assert "telegram" in data["detail"]["field_errors"]
|
||||
|
||||
async def test_partial_update_preserves_other_fields(self, client_factory, regular_user):
|
||||
async def test_partial_update_preserves_other_fields(
|
||||
self, client_factory, regular_user
|
||||
):
|
||||
"""Updating one field doesn't affect others (they get set to the request values)."""
|
||||
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
||||
# Set initial values
|
||||
|
|
@ -355,7 +359,7 @@ class TestUpdateProfileEndpoint:
|
|||
"telegram": "@initial",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Update only telegram, but note: PUT replaces all fields
|
||||
# So we need to include all fields we want to keep
|
||||
response = await client.put(
|
||||
|
|
@ -365,7 +369,7 @@ class TestUpdateProfileEndpoint:
|
|||
"telegram": "@updated",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["contact_email"] == "initial@example.com"
|
||||
|
|
@ -386,10 +390,10 @@ class TestProfilePrivacy:
|
|||
"telegram": "@secret",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Check /api/auth/me doesn't expose it
|
||||
response = await client.get("/api/auth/me")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
# These fields should NOT be in the response
|
||||
|
|
@ -402,12 +406,15 @@ class TestProfilePrivacy:
|
|||
class TestProfileGodfather:
|
||||
"""Tests for godfather information in profile."""
|
||||
|
||||
async def test_profile_shows_godfather_email(self, client_factory, admin_user, regular_user):
|
||||
async def test_profile_shows_godfather_email(
|
||||
self, client_factory, admin_user, regular_user
|
||||
):
|
||||
"""Profile shows godfather email for users who signed up with invite."""
|
||||
from tests.helpers import unique_email
|
||||
from sqlalchemy import select
|
||||
|
||||
from models import User
|
||||
|
||||
from tests.helpers import unique_email
|
||||
|
||||
# Create invite
|
||||
async with client_factory.create(cookies=admin_user["cookies"]) as client:
|
||||
async with client_factory.get_db_session() as db:
|
||||
|
|
@ -415,13 +422,13 @@ class TestProfileGodfather:
|
|||
select(User).where(User.email == regular_user["email"])
|
||||
)
|
||||
godfather = result.scalar_one()
|
||||
|
||||
|
||||
create_resp = await client.post(
|
||||
"/api/admin/invites",
|
||||
json={"godfather_id": godfather.id},
|
||||
)
|
||||
identifier = create_resp.json()["identifier"]
|
||||
|
||||
|
||||
# Register new user with invite
|
||||
new_email = unique_email("godchild")
|
||||
async with client_factory.create() as client:
|
||||
|
|
@ -434,20 +441,22 @@ class TestProfileGodfather:
|
|||
},
|
||||
)
|
||||
new_user_cookies = dict(reg_resp.cookies)
|
||||
|
||||
|
||||
# Check profile shows godfather
|
||||
async with client_factory.create(cookies=new_user_cookies) as client:
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["godfather_email"] == regular_user["email"]
|
||||
|
||||
async def test_profile_godfather_null_for_seeded_users(self, client_factory, regular_user):
|
||||
async def test_profile_godfather_null_for_seeded_users(
|
||||
self, client_factory, regular_user
|
||||
):
|
||||
"""Profile shows null godfather for users without one (e.g., seeded users)."""
|
||||
async with client_factory.create(cookies=regular_user["cookies"]) as client:
|
||||
response = await client.get("/api/profile")
|
||||
|
||||
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["godfather_email"] is None
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue