""" 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("/admin/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("/admin/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 "/admin/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("/admin/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