mirror of
https://github.com/khoaliber/LetterFeed.git
synced 2026-03-02 21:19:13 +00:00
feat: authentication
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import os
|
||||
|
||||
os.environ["LETTERFEED_DATABASE_URL"] = "sqlite:///./test.db"
|
||||
os.environ["LETTERFEED_SECRET_KEY"] = "testsecret"
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
170
backend/app/tests/test_auth.py
Normal file
170
backend/app/tests/test_auth.py
Normal file
@@ -0,0 +1,170 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.settings import create_or_update_settings
|
||||
from app.schemas.settings import SettingsCreate
|
||||
|
||||
|
||||
def test_auth_status_disabled(client: TestClient):
|
||||
"""Test auth status when auth is disabled."""
|
||||
response = client.get("/auth/status")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"auth_enabled": False}
|
||||
|
||||
|
||||
def test_auth_status_enabled(client: TestClient, db_session: Session):
|
||||
"""Test auth status when auth is enabled."""
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="test.com",
|
||||
imap_username="test",
|
||||
imap_password="password",
|
||||
auth_username="admin",
|
||||
auth_password="password",
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
response = client.get("/auth/status")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"auth_enabled": True}
|
||||
|
||||
|
||||
def test_login_endpoint(client: TestClient, db_session: Session):
|
||||
"""Test the /auth/login endpoint directly."""
|
||||
# Setup auth credentials in the database
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="test.com",
|
||||
imap_username="test",
|
||||
imap_password="password",
|
||||
auth_username="admin",
|
||||
auth_password="password",
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
# Test with correct credentials
|
||||
login_data = {"username": "admin", "password": "password"}
|
||||
response = client.post("/auth/login", data=login_data)
|
||||
assert response.status_code == 200
|
||||
json_response = response.json()
|
||||
assert "access_token" in json_response
|
||||
assert json_response["token_type"] == "bearer"
|
||||
|
||||
# Test with incorrect password
|
||||
login_data["password"] = "wrongpassword"
|
||||
response = client.post("/auth/login", data=login_data)
|
||||
assert response.status_code == 401
|
||||
|
||||
# Test with incorrect username
|
||||
login_data["username"] = "wronguser"
|
||||
login_data["password"] = "password"
|
||||
response = client.post("/auth/login", data=login_data)
|
||||
assert response.status_code == 401
|
||||
|
||||
# Test with no credentials
|
||||
response = client.post("/auth/login")
|
||||
assert response.status_code == 422 # FastAPI validation error for missing form data
|
||||
|
||||
|
||||
def test_protected_route_no_auth(client: TestClient, db_session: Session):
|
||||
"""Test accessing a protected route without auth enabled."""
|
||||
# Health is not protected, newsletters is.
|
||||
response = client.get("/newsletters")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_protected_route_with_auth_fail(client: TestClient, db_session: Session):
|
||||
"""Test accessing a protected route with auth enabled but wrong credentials."""
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="test.com",
|
||||
imap_username="test",
|
||||
imap_password="password",
|
||||
auth_username="admin",
|
||||
auth_password="password",
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
response = client.get("/newsletters")
|
||||
assert response.status_code == 401
|
||||
|
||||
response = client.get(
|
||||
"/newsletters", headers={"Authorization": "Bearer wrongtoken"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
def test_protected_route_with_auth_success(client: TestClient, db_session: Session):
|
||||
"""Test accessing a protected route with auth enabled and correct credentials."""
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="test.com",
|
||||
imap_username="test",
|
||||
imap_password="password",
|
||||
auth_username="admin",
|
||||
auth_password="password",
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
# First, log in to get a token
|
||||
login_data = {"username": "admin", "password": "password"}
|
||||
response = client.post("/auth/login", data=login_data)
|
||||
token = response.json()["access_token"]
|
||||
|
||||
# Then, use the token to access the protected route
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
response = client.get("/newsletters", headers=headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_unprotected_route_with_auth(client: TestClient, db_session: Session):
|
||||
"""Test that feed endpoint is not protected."""
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="test.com",
|
||||
imap_username="test",
|
||||
imap_password="password",
|
||||
auth_username="admin",
|
||||
auth_password="password",
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
# Log in to get a token
|
||||
login_data = {"username": "admin", "password": "password"}
|
||||
login_response = client.post("/auth/login", data=login_data)
|
||||
token = login_response.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
# Create a newsletter to get a feed from
|
||||
newsletter_data = {"name": "Test Newsletter", "sender_emails": ["test@test.com"]}
|
||||
create_response = client.post("/newsletters", json=newsletter_data, headers=headers)
|
||||
newsletter_id = create_response.json()["id"]
|
||||
|
||||
response = client.get(f"/feeds/{newsletter_id}")
|
||||
assert response.status_code == 200
|
||||
|
||||
|
||||
def test_auth_with_env_vars(client: TestClient):
|
||||
"""Test authentication using environment variables."""
|
||||
with patch("app.core.auth.env_settings") as mock_env_settings:
|
||||
mock_env_settings.auth_username = "env_admin"
|
||||
mock_env_settings.auth_password = "env_password"
|
||||
mock_env_settings.secret_key = "test-secret"
|
||||
mock_env_settings.algorithm = "HS256"
|
||||
mock_env_settings.access_token_expire_minutes = 30
|
||||
|
||||
# Log in to get a token
|
||||
login_data = {"username": "env_admin", "password": "env_password"}
|
||||
login_response = client.post("/auth/login", data=login_data)
|
||||
assert login_response.status_code == 200
|
||||
token = login_response.json()["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
|
||||
response = client.get("/newsletters", headers=headers)
|
||||
assert response.status_code == 200
|
||||
|
||||
response = client.get(
|
||||
"/newsletters", headers={"Authorization": "Bearer wrongtoken"}
|
||||
)
|
||||
assert response.status_code == 401
|
||||
|
||||
response = client.get("/auth/status")
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {"auth_enabled": True}
|
||||
@@ -167,8 +167,12 @@ def test_process_emails_auto_add_sender(mock_imap, db_session: Session):
|
||||
|
||||
@patch("app.services.email_processor.imaplib.IMAP4_SSL")
|
||||
def test_process_emails_no_settings(mock_imap, db_session: Session):
|
||||
"""Test processing emails with no settings in the database."""
|
||||
# No settings in the DB
|
||||
"""Test processing emails with no settings configured."""
|
||||
# This test ensures that email processing is skipped if settings are not configured.
|
||||
# In the new flow, initial settings are created at startup, so we call it here.
|
||||
from app.crud.settings import create_initial_settings
|
||||
|
||||
create_initial_settings(db_session)
|
||||
process_emails(db_session)
|
||||
mock_imap.assert_not_called()
|
||||
|
||||
|
||||
@@ -20,10 +20,19 @@ def test_create_or_update_settings(db_session: Session):
|
||||
search_folder="INBOX",
|
||||
move_to_folder="Archive",
|
||||
mark_as_read=True,
|
||||
auth_username="user",
|
||||
auth_password="password",
|
||||
)
|
||||
settings = create_or_update_settings(db_session, settings_data)
|
||||
assert settings.imap_server == "imap.test.com"
|
||||
assert settings.mark_as_read
|
||||
assert settings.auth_username == "user"
|
||||
|
||||
# check password hash
|
||||
from app.models.settings import Settings as SettingsModel
|
||||
|
||||
db_settings = db_session.query(SettingsModel).first()
|
||||
assert db_settings.auth_password_hash is not None
|
||||
|
||||
updated_settings_data = SettingsCreate(
|
||||
imap_server="imap.updated.com",
|
||||
@@ -32,11 +41,13 @@ def test_create_or_update_settings(db_session: Session):
|
||||
search_folder="Inbox",
|
||||
move_to_folder=None,
|
||||
mark_as_read=False,
|
||||
auth_username="new_user",
|
||||
)
|
||||
updated_settings = create_or_update_settings(db_session, updated_settings_data)
|
||||
assert updated_settings.imap_server == "imap.updated.com"
|
||||
assert not updated_settings.mark_as_read
|
||||
assert updated_settings.move_to_folder is None
|
||||
assert updated_settings.auth_username == "new_user"
|
||||
|
||||
|
||||
def test_get_settings(db_session: Session):
|
||||
@@ -62,6 +73,8 @@ def test_get_settings_with_env_override(db_session: Session):
|
||||
imap_username="db_user",
|
||||
imap_password="db_pass",
|
||||
email_check_interval=15,
|
||||
auth_username="db_user",
|
||||
auth_password="db_password",
|
||||
)
|
||||
create_or_update_settings(db_session, db_settings_data)
|
||||
|
||||
@@ -72,8 +85,11 @@ def test_get_settings_with_env_override(db_session: Session):
|
||||
"imap_username": "env_user",
|
||||
"imap_password": "env_pass",
|
||||
"email_check_interval": 30,
|
||||
"auth_username": "env_auth_user",
|
||||
"auth_password": "env_auth_password",
|
||||
}
|
||||
mock_env_settings.imap_password = "env_pass"
|
||||
mock_env_settings.auth_password = "env_auth_password"
|
||||
|
||||
# 3. Call get_settings and assert the override
|
||||
settings = get_settings(db_session, with_password=True)
|
||||
@@ -81,8 +97,10 @@ def test_get_settings_with_env_override(db_session: Session):
|
||||
assert settings.imap_username == "env_user"
|
||||
assert settings.imap_password == "env_pass"
|
||||
assert settings.email_check_interval == 30
|
||||
assert settings.auth_username == "env_auth_user"
|
||||
assert "imap_server" in settings.locked_fields
|
||||
assert "imap_username" in settings.locked_fields
|
||||
assert "auth_username" in settings.locked_fields
|
||||
|
||||
# 4. Call create_or_update_settings and assert that locked fields are not updated
|
||||
update_data = SettingsCreate(
|
||||
@@ -90,11 +108,14 @@ def test_get_settings_with_env_override(db_session: Session):
|
||||
imap_username="new_user",
|
||||
imap_password="new_pass",
|
||||
email_check_interval=45,
|
||||
auth_username="new_auth_user",
|
||||
auth_password="new_auth_password",
|
||||
)
|
||||
updated_settings = create_or_update_settings(db_session, update_data)
|
||||
assert updated_settings.imap_server == "env.imap.com" # Should not change
|
||||
assert updated_settings.imap_username == "env_user" # Should not change
|
||||
assert updated_settings.email_check_interval == 30 # Should not change
|
||||
assert updated_settings.auth_username == "env_auth_user" # Should not change
|
||||
|
||||
|
||||
def test_create_newsletter(db_session: Session):
|
||||
|
||||
@@ -2,6 +2,10 @@ import uuid
|
||||
from unittest.mock import patch
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.crud.settings import create_or_update_settings
|
||||
from app.schemas.settings import SettingsCreate
|
||||
|
||||
|
||||
def test_health_check(client: TestClient):
|
||||
@@ -11,12 +15,8 @@ def test_health_check(client: TestClient):
|
||||
assert response.json() == {"status": "ok"}
|
||||
|
||||
|
||||
@patch("app.core.imap.imaplib.IMAP4_SSL")
|
||||
def test_update_imap_settings(mock_imap, client: TestClient):
|
||||
def test_update_imap_settings(client: TestClient):
|
||||
"""Test updating IMAP settings."""
|
||||
mock_imap.return_value.login.return_value = (None, None)
|
||||
mock_imap.return_value.logout.return_value = (None, None)
|
||||
|
||||
settings_data = {
|
||||
"imap_server": "imap.example.com",
|
||||
"imap_username": "test@example.com",
|
||||
@@ -34,12 +34,8 @@ def test_update_imap_settings(mock_imap, client: TestClient):
|
||||
assert response.json()["mark_as_read"]
|
||||
|
||||
|
||||
@patch("app.core.imap.imaplib.IMAP4_SSL")
|
||||
def test_get_imap_settings(mock_imap, client: TestClient):
|
||||
def test_get_imap_settings(client: TestClient):
|
||||
"""Test getting IMAP settings."""
|
||||
mock_imap.return_value.login.return_value = (None, None)
|
||||
mock_imap.return_value.logout.return_value = (None, None)
|
||||
|
||||
settings_data = {
|
||||
"imap_server": "imap.example.com",
|
||||
"imap_username": "test@example.com",
|
||||
@@ -57,20 +53,20 @@ def test_get_imap_settings(mock_imap, client: TestClient):
|
||||
|
||||
|
||||
@patch("app.core.imap.imaplib.IMAP4_SSL")
|
||||
def test_test_imap_connection(mock_imap, client: TestClient):
|
||||
def test_test_imap_connection(mock_imap, client: TestClient, db_session: Session):
|
||||
"""Test the IMAP connection."""
|
||||
mock_imap.return_value.login.return_value = (None, None)
|
||||
mock_imap.return_value.logout.return_value = (None, None)
|
||||
|
||||
settings_data = {
|
||||
"imap_server": "imap.example.com",
|
||||
"imap_username": "test@example.com",
|
||||
"imap_password": "password",
|
||||
"search_folder": "INBOX",
|
||||
"move_to_folder": "Processed",
|
||||
"mark_as_read": True,
|
||||
}
|
||||
client.post("/imap/settings", json=settings_data)
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="imap.example.com",
|
||||
imap_username="test@example.com",
|
||||
imap_password="password",
|
||||
search_folder="INBOX",
|
||||
move_to_folder="Processed",
|
||||
mark_as_read=True,
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
response = client.post("/imap/test")
|
||||
assert response.status_code == 200
|
||||
@@ -78,7 +74,7 @@ def test_test_imap_connection(mock_imap, client: TestClient):
|
||||
|
||||
|
||||
@patch("app.core.imap.imaplib.IMAP4_SSL")
|
||||
def test_get_imap_folders(mock_imap, client: TestClient):
|
||||
def test_get_imap_folders(mock_imap, client: TestClient, db_session: Session):
|
||||
"""Test getting IMAP folders."""
|
||||
mock_imap.return_value.login.return_value = (None, None)
|
||||
mock_imap.return_value.logout.return_value = (None, None)
|
||||
@@ -87,15 +83,15 @@ def test_get_imap_folders(mock_imap, client: TestClient):
|
||||
[b'(NOCONNECT NOSELECT) "/" "INBOX"', b'(NOCONNECT NOSELECT) "/" "Processed"'],
|
||||
)
|
||||
|
||||
settings_data = {
|
||||
"imap_server": "imap.example.com",
|
||||
"imap_username": "test@example.com",
|
||||
"imap_password": "password",
|
||||
"search_folder": "INBOX",
|
||||
"move_to_folder": "Processed",
|
||||
"mark_as_read": True,
|
||||
}
|
||||
client.post("/imap/settings", json=settings_data)
|
||||
settings_data = SettingsCreate(
|
||||
imap_server="imap.example.com",
|
||||
imap_username="test@example.com",
|
||||
imap_password="password",
|
||||
search_folder="INBOX",
|
||||
move_to_folder="Processed",
|
||||
mark_as_read=True,
|
||||
)
|
||||
create_or_update_settings(db_session, settings_data)
|
||||
|
||||
response = client.get("/imap/folders")
|
||||
assert response.status_code == 200
|
||||
@@ -141,7 +137,7 @@ def test_get_single_newsletter(client: TestClient):
|
||||
"""Test getting a single newsletter."""
|
||||
unique_email = f"newsletter_{uuid.uuid4()}@example.com"
|
||||
newsletter_data = {"name": "Third Newsletter", "sender_emails": [unique_email]}
|
||||
create_response = client.post("/newsletters/", json=newsletter_data)
|
||||
create_response = client.post("/newsletters", json=newsletter_data)
|
||||
newsletter_id = create_response.json()["id"]
|
||||
|
||||
response = client.get(f"/newsletters/{newsletter_id}")
|
||||
@@ -151,7 +147,7 @@ def test_get_single_newsletter(client: TestClient):
|
||||
|
||||
def test_get_nonexistent_newsletter(client: TestClient):
|
||||
"""Test getting a nonexistent newsletter."""
|
||||
response = client.get("/newsletters/999")
|
||||
response = client.get("/newsletters/nonexistent")
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Newsletter not found"}
|
||||
|
||||
@@ -160,7 +156,7 @@ def test_get_newsletter_feed(client: TestClient):
|
||||
"""Test generating a newsletter feed."""
|
||||
unique_email = f"feed_test_{uuid.uuid4()}@example.com"
|
||||
newsletter_data = {"name": "Feed Test Newsletter", "sender_emails": [unique_email]}
|
||||
create_response = client.post("/newsletters/", json=newsletter_data)
|
||||
create_response = client.post("/newsletters", json=newsletter_data)
|
||||
newsletter_id = create_response.json()["id"]
|
||||
|
||||
# Add some entries to the newsletter
|
||||
@@ -195,6 +191,6 @@ def test_get_newsletter_feed(client: TestClient):
|
||||
|
||||
def test_get_newsletter_feed_nonexistent_newsletter(client: TestClient):
|
||||
"""Test generating a feed for a nonexistent newsletter."""
|
||||
response = client.get("/feeds/999")
|
||||
response = client.get("/feeds/nonexistent")
|
||||
assert response.status_code == 404
|
||||
assert response.json() == {"detail": "Newsletter not found"}
|
||||
|
||||
Reference in New Issue
Block a user