Files
StarPunk/tests/test_routes_dev_auth.py
Phil Skentelbery 44a97e4ffa fix: Change auth blueprint prefix from /admin to /auth (v0.9.2)
The auth routes were registered under /admin/* but the IndieAuth
redirect_uri was configured as /auth/callback, causing 404 errors
when providers redirected back after authentication.

- Change auth blueprint url_prefix from "/admin" to "/auth"
- Update test expectations for new auth route paths
- Add ADR-022 documenting the architectural decision

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-22 18:22:08 -07:00

367 lines
13 KiB
Python

"""
Tests for development authentication routes and security
Tests cover:
- Dev auth route availability based on DEV_MODE
- Session creation without authentication
- Security: 404 when DEV_MODE disabled
- Configuration validation
- Visual warning indicators
"""
import pytest
from starpunk import create_app
from starpunk.auth import verify_session
@pytest.fixture
def dev_app(tmp_path):
"""Create app with DEV_MODE enabled"""
test_data_dir = tmp_path / "dev_data"
test_data_dir.mkdir(parents=True, exist_ok=True)
test_config = {
"TESTING": True,
"DATABASE_PATH": test_data_dir / "starpunk.db",
"DATA_PATH": test_data_dir,
"NOTES_PATH": test_data_dir / "notes",
"SESSION_SECRET": "test-secret",
"SITE_URL": "http://localhost:5000",
"DEV_MODE": True,
"DEV_ADMIN_ME": "https://dev.example.com",
}
app = create_app(config=test_config)
yield app
@pytest.fixture
def prod_app(tmp_path):
"""Create app with DEV_MODE disabled (production)"""
test_data_dir = tmp_path / "prod_data"
test_data_dir.mkdir(parents=True, exist_ok=True)
test_config = {
"TESTING": True,
"DATABASE_PATH": test_data_dir / "starpunk.db",
"DATA_PATH": test_data_dir,
"NOTES_PATH": test_data_dir / "notes",
"SESSION_SECRET": "test-secret",
"SITE_URL": "http://localhost:5000",
"ADMIN_ME": "https://prod.example.com",
"DEV_MODE": False,
}
app = create_app(config=test_config)
yield app
class TestDevAuthRouteAvailability:
"""Test dev auth routes are only available when DEV_MODE enabled"""
def test_dev_login_available_when_enabled(self, dev_app):
"""Test /dev/login is available when DEV_MODE=true"""
client = dev_app.test_client()
response = client.get("/dev/login", follow_redirects=False)
# Should redirect to dashboard (successful login)
assert response.status_code == 302
assert "/admin/" in response.location
def test_dev_login_404_when_disabled(self, prod_app):
"""Test /dev/login returns 404 when DEV_MODE=false"""
client = prod_app.test_client()
response = client.get("/dev/login")
# Should return 404 - route doesn't exist
assert response.status_code == 404
def test_dev_login_not_accessible_in_production(self, prod_app):
"""Test dev login cannot be accessed in production mode"""
client = prod_app.test_client()
# Try various paths
paths = ["/dev/login", "/dev/auth", "/dev-login"]
for path in paths:
response = client.get(path)
# Should be 404 (dev routes not registered) or redirect to login
assert response.status_code in [404, 302]
class TestDevAuthFunctionality:
"""Test dev auth creates sessions correctly"""
def test_dev_login_creates_session(self, dev_app):
"""Test dev login creates a valid session"""
client = dev_app.test_client()
response = client.get("/dev/login", follow_redirects=False)
assert response.status_code == 302
# Check session cookie was set
cookies = response.headers.getlist("Set-Cookie")
assert any("session=" in cookie for cookie in cookies)
def test_dev_login_session_is_valid(self, dev_app):
"""Test dev login session can be verified"""
client = dev_app.test_client()
response = client.get("/dev/login", follow_redirects=False)
# Extract session token from cookie
session_token = None
for cookie in response.headers.getlist("Set-Cookie"):
if "session=" in cookie:
session_token = cookie.split("session=")[1].split(";")[0]
break
assert session_token is not None
# Verify session is valid
with dev_app.app_context():
session_info = verify_session(session_token)
assert session_info is not None
assert session_info["me"] == "https://dev.example.com"
def test_dev_login_uses_dev_admin_me(self, dev_app):
"""Test dev login uses DEV_ADMIN_ME identity"""
client = dev_app.test_client()
response = client.get("/dev/login", follow_redirects=False)
# Get session token
session_token = None
for cookie in response.headers.getlist("Set-Cookie"):
if "session=" in cookie:
session_token = cookie.split("session=")[1].split(";")[0]
break
# Verify identity
with dev_app.app_context():
session_info = verify_session(session_token)
assert session_info is not None
assert session_info["me"] == dev_app.config["DEV_ADMIN_ME"]
def test_dev_login_grants_admin_access(self, dev_app):
"""Test dev login grants access to admin routes"""
client = dev_app.test_client()
# Login via dev auth
response = client.get("/dev/login", follow_redirects=True)
assert response.status_code == 200
# Should now be able to access admin
response = client.get("/admin/")
assert response.status_code == 200
class TestConfigurationValidation:
"""Test configuration validation for dev mode"""
def test_dev_mode_requires_dev_admin_me(self, tmp_path):
"""Test DEV_MODE=true requires DEV_ADMIN_ME"""
test_data_dir = tmp_path / "validation_data"
test_data_dir.mkdir(parents=True, exist_ok=True)
test_config = {
"TESTING": True,
"DATABASE_PATH": test_data_dir / "starpunk.db",
"DATA_PATH": test_data_dir,
"NOTES_PATH": test_data_dir / "notes",
"SESSION_SECRET": "test-secret",
"SITE_URL": "http://localhost:5000",
"DEV_MODE": True,
# Missing DEV_ADMIN_ME
}
with pytest.raises(ValueError, match="DEV_ADMIN_ME"):
app = create_app(config=test_config)
def test_production_mode_requires_admin_me(self, tmp_path):
"""Test production mode requires ADMIN_ME"""
test_data_dir = tmp_path / "prod_validation_data"
test_data_dir.mkdir(parents=True, exist_ok=True)
test_config = {
"TESTING": True,
"DATABASE_PATH": test_data_dir / "starpunk.db",
"DATA_PATH": test_data_dir,
"NOTES_PATH": test_data_dir / "notes",
"SESSION_SECRET": "test-secret",
"SITE_URL": "http://localhost:5000",
"DEV_MODE": False,
"ADMIN_ME": None, # Explicitly set to None
}
with pytest.raises(ValueError, match="ADMIN_ME"):
app = create_app(config=test_config)
def test_dev_mode_allows_missing_admin_me(self, tmp_path):
"""Test DEV_MODE=true doesn't require ADMIN_ME"""
test_data_dir = tmp_path / "dev_no_admin_data"
test_data_dir.mkdir(parents=True, exist_ok=True)
test_config = {
"TESTING": True,
"DATABASE_PATH": test_data_dir / "starpunk.db",
"DATA_PATH": test_data_dir,
"NOTES_PATH": test_data_dir / "notes",
"SESSION_SECRET": "test-secret",
"SITE_URL": "http://localhost:5000",
"DEV_MODE": True,
"DEV_ADMIN_ME": "https://dev.example.com",
# ADMIN_ME not set - should be okay
}
# Should not raise
app = create_app(config=test_config)
assert app is not None
class TestDevModeWarnings:
"""Test dev mode warning indicators"""
def test_dev_mode_shows_warning_banner(self, dev_app):
"""Test dev mode shows warning banner on pages"""
client = dev_app.test_client()
response = client.get("/")
assert response.status_code == 200
# Should have dev mode warning
assert (
b"DEVELOPMENT MODE" in response.data
or b"DEV MODE" in response.data
or b"Development authentication" in response.data
)
def test_dev_mode_warning_on_admin_pages(self, dev_app):
"""Test dev mode warning appears on admin pages"""
client = dev_app.test_client()
# Login first
client.get("/dev/login")
# Check admin page
response = client.get("/admin/")
assert response.status_code == 200
assert b"DEVELOPMENT MODE" in response.data or b"DEV MODE" in response.data
def test_production_mode_no_warning(self, prod_app):
"""Test production mode doesn't show dev warning"""
client = prod_app.test_client()
response = client.get("/")
assert response.status_code == 200
# Should NOT have dev mode warning
assert b"DEVELOPMENT MODE" not in response.data
assert b"DEV MODE" not in response.data
def test_dev_login_page_shows_link(self, dev_app):
"""Test login page shows dev login link when DEV_MODE enabled"""
client = dev_app.test_client()
response = client.get("/auth/login")
assert response.status_code == 200
# Should have link to dev login
assert b"/dev/login" in response.data or b"Dev Login" in response.data
def test_production_login_no_dev_link(self, prod_app):
"""Test login page doesn't show dev link in production"""
client = prod_app.test_client()
response = client.get("/auth/login")
assert response.status_code == 200
# Should NOT have dev login link
assert b"/dev/login" not in response.data
class TestSecuritySafeguards:
"""Test security safeguards for dev auth"""
def test_dev_mode_logs_warning(self, tmp_path, caplog):
"""Test dev mode logs warning on startup"""
import logging
caplog.set_level(logging.WARNING)
# Create new app to trigger startup logging
test_data_dir = tmp_path / "logging_data"
test_data_dir.mkdir(parents=True, exist_ok=True)
test_config = {
"TESTING": True,
"DATABASE_PATH": test_data_dir / "starpunk.db",
"DATA_PATH": test_data_dir,
"NOTES_PATH": test_data_dir / "notes",
"SESSION_SECRET": "test-secret",
"SITE_URL": "http://localhost:5000",
"DEV_MODE": True,
"DEV_ADMIN_ME": "https://dev.example.com",
}
app = create_app(config=test_config)
# Check logs
assert any("DEVELOPMENT" in record.message.upper() for record in caplog.records)
def test_dev_login_logs_session_creation(self, dev_app, caplog):
"""Test dev login logs session creation"""
import logging
caplog.set_level(logging.WARNING)
client = dev_app.test_client()
client.get("/dev/login")
# Should log the session creation
assert any("DEV MODE" in record.message for record in caplog.records)
def test_dev_mode_cookie_not_secure(self, dev_app):
"""Test dev mode session cookie is not marked secure (for localhost)"""
client = dev_app.test_client()
response = client.get("/dev/login", follow_redirects=False)
# Check cookie settings
cookies = response.headers.getlist("Set-Cookie")
session_cookie = [c for c in cookies if "session=" in c][0]
# Should have httponly but not secure (for localhost testing)
assert "HttpOnly" in session_cookie
# Note: 'Secure' might not be set for dev mode to work with http://localhost
class TestIntegrationFlow:
"""Test complete dev auth integration flow"""
def test_complete_dev_auth_flow(self, dev_app):
"""Test complete flow: dev login -> admin access -> logout"""
client = dev_app.test_client()
# Step 1: Access admin without auth (should redirect to login)
response = client.get("/admin/", follow_redirects=False)
assert response.status_code == 302
assert "/auth/login" in response.location
# Step 2: Use dev login
response = client.get("/dev/login", follow_redirects=True)
assert response.status_code == 200
# Step 3: Access admin (should work now)
response = client.get("/admin/")
assert response.status_code == 200
assert b"Dashboard" in response.data or b"Admin" in response.data
# Step 4: Create a note
response = client.post(
"/admin/new",
data={
"content": "# Dev Auth Test\n\nCreated via dev auth.",
"published": "on",
},
follow_redirects=True,
)
assert response.status_code == 200
# Step 5: Logout
response = client.post("/auth/logout", follow_redirects=True)
assert response.status_code == 200
# Step 6: Verify can't access admin anymore
response = client.get("/admin/", follow_redirects=False)
assert response.status_code == 302