7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
312 lines
10 KiB
Python
312 lines
10 KiB
Python
"""
|
|
Integration tests for the complete authentication system.
|
|
"""
|
|
|
|
import os
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from local_deep_research.database.auth_db import (
|
|
get_auth_db_session,
|
|
init_auth_database,
|
|
)
|
|
from local_deep_research.database.models.auth import User
|
|
from local_deep_research.web.app_factory import create_app
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_data_dir():
|
|
"""Create a temporary data directory for testing."""
|
|
temp_dir = tempfile.mkdtemp()
|
|
yield Path(temp_dir)
|
|
shutil.rmtree(temp_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def app(temp_data_dir, monkeypatch):
|
|
"""Create a Flask app configured for testing."""
|
|
monkeypatch.setenv("LDR_DATA_DIR", str(temp_data_dir))
|
|
|
|
# Clear database manager state before creating app
|
|
from local_deep_research.database.encrypted_db import db_manager
|
|
|
|
db_manager.close_all_databases()
|
|
|
|
# Reset db_manager's data directory to temp directory
|
|
db_manager.data_dir = temp_data_dir / "encrypted_databases"
|
|
db_manager.data_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Remove any existing user databases in the temp directory
|
|
encrypted_db_dir = temp_data_dir / "encrypted_databases"
|
|
if encrypted_db_dir.exists():
|
|
shutil.rmtree(encrypted_db_dir)
|
|
encrypted_db_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Initialize fresh auth database
|
|
init_auth_database()
|
|
|
|
# Clean up any existing test users before starting
|
|
auth_db = get_auth_db_session()
|
|
auth_db.query(User).filter(User.username.like("integrationtest%")).delete()
|
|
auth_db.query(User).filter(User.username.like("testuser%")).delete()
|
|
auth_db.commit()
|
|
auth_db.close()
|
|
|
|
app, _ = create_app()
|
|
app.config["TESTING"] = True
|
|
app.config["WTF_CSRF_ENABLED"] = False
|
|
app.config["SESSION_COOKIE_SECURE"] = False
|
|
|
|
yield app
|
|
|
|
# Cleanup after test
|
|
db_manager.close_all_databases()
|
|
|
|
# Clean up test users from auth database
|
|
auth_db = get_auth_db_session()
|
|
auth_db.query(User).filter(User.username.like("integrationtest%")).delete()
|
|
auth_db.query(User).filter(User.username.like("testuser%")).delete()
|
|
auth_db.commit()
|
|
auth_db.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def client(app):
|
|
"""Create a test client."""
|
|
return app.test_client()
|
|
|
|
|
|
class TestAuthIntegration:
|
|
"""Integration tests for authentication system."""
|
|
|
|
def test_full_user_lifecycle(self, client):
|
|
"""Test complete user lifecycle: register, login, use app, logout."""
|
|
# 1. Start unauthenticated - should redirect to login
|
|
response = client.get("/")
|
|
assert response.status_code == 302
|
|
assert "/auth/login" in response.location
|
|
|
|
# 2. Register new user
|
|
response = client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": "integrationtest",
|
|
"password": "TestPass123", # pragma: allowlist secret
|
|
"confirm_password": "TestPass123", # pragma: allowlist secret
|
|
"acknowledge": "true",
|
|
},
|
|
follow_redirects=True,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# 3. Should be logged in automatically after registration
|
|
response = client.get("/")
|
|
assert response.status_code == 200
|
|
# The main page should show we're logged in (username might be displayed)
|
|
|
|
# 4. Access protected routes
|
|
response = client.get("/history")
|
|
assert response.status_code == 200
|
|
|
|
response = client.get("/settings")
|
|
assert response.status_code == 200
|
|
|
|
# 5. Check auth status
|
|
response = client.get("/auth/check")
|
|
assert response.status_code == 200
|
|
data = response.get_json()
|
|
assert data["authenticated"] is True
|
|
assert data["username"] == "integrationtest"
|
|
|
|
# 6. Logout
|
|
response = client.post("/auth/logout", follow_redirects=True)
|
|
assert response.status_code == 200
|
|
assert b"You have been logged out successfully" in response.data
|
|
|
|
# 7. Should not be able to access protected routes
|
|
response = client.get("/history")
|
|
assert response.status_code == 302
|
|
assert "/auth/login" in response.location
|
|
|
|
# 8. Login again
|
|
response = client.post(
|
|
"/auth/login",
|
|
data={"username": "integrationtest", "password": "TestPass123"},
|
|
follow_redirects=True,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# 9. Should be able to access protected routes again
|
|
response = client.get("/history")
|
|
assert response.status_code == 200
|
|
|
|
def test_multiple_users(self, client):
|
|
"""Test that multiple users can register and have separate sessions."""
|
|
# Register first user
|
|
client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": "user1",
|
|
"password": "Password1X",
|
|
"confirm_password": "Password1X",
|
|
"acknowledge": "true",
|
|
},
|
|
)
|
|
|
|
# Check logged in as user1
|
|
response = client.get("/auth/check")
|
|
data = response.get_json()
|
|
assert data["username"] == "user1"
|
|
|
|
# Logout
|
|
client.post("/auth/logout")
|
|
|
|
# Register second user
|
|
client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": "user2",
|
|
"password": "Password2X",
|
|
"confirm_password": "Password2X",
|
|
"acknowledge": "true",
|
|
},
|
|
)
|
|
|
|
# Check logged in as user2
|
|
response = client.get("/auth/check")
|
|
data = response.get_json()
|
|
assert data["username"] == "user2"
|
|
|
|
# Verify both users exist in auth database
|
|
auth_db = get_auth_db_session()
|
|
users = auth_db.query(User).all()
|
|
assert len(users) == 2
|
|
usernames = [user.username for user in users]
|
|
assert "user1" in usernames
|
|
assert "user2" in usernames
|
|
auth_db.close()
|
|
|
|
def test_session_persistence(self, client):
|
|
"""Test that sessions persist across requests."""
|
|
# Register and login
|
|
client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": "sessiontest",
|
|
"password": "TestPass123", # pragma: allowlist secret
|
|
"confirm_password": "TestPass123", # pragma: allowlist secret
|
|
"acknowledge": "true",
|
|
},
|
|
)
|
|
|
|
# Make multiple requests
|
|
for _ in range(5):
|
|
response = client.get("/history")
|
|
assert response.status_code == 200
|
|
|
|
response = client.get("/auth/check")
|
|
data = response.get_json()
|
|
assert data["username"] == "sessiontest"
|
|
|
|
def test_protected_api_endpoints(self, client):
|
|
"""Test that API endpoints require authentication."""
|
|
# Test various API endpoints without auth
|
|
endpoints = [
|
|
("/api/start_research", "POST"),
|
|
("/api/history", "GET"),
|
|
("/settings/api", "GET"),
|
|
("/history/api", "GET"),
|
|
("/metrics/api/metrics", "GET"),
|
|
]
|
|
|
|
for endpoint, method in endpoints:
|
|
if method == "GET":
|
|
response = client.get(endpoint)
|
|
else:
|
|
response = client.post(endpoint, json={})
|
|
|
|
# All these endpoints contain the slash-bounded `api` segment
|
|
# (either /api/ in the middle or trailing /api), so the auth
|
|
# decorator returns JSON 401 for unauthenticated callers.
|
|
assert response.status_code == 401
|
|
assert response.get_json()["error"] == "Authentication required"
|
|
|
|
# Register and try again
|
|
client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": "apitest",
|
|
"password": "TestPass123", # pragma: allowlist secret
|
|
"confirm_password": "TestPass123", # pragma: allowlist secret
|
|
"acknowledge": "true",
|
|
},
|
|
)
|
|
|
|
# Now endpoints should be accessible (may return errors but not redirects)
|
|
for endpoint, method in endpoints:
|
|
if method == "GET":
|
|
response = client.get(endpoint)
|
|
else:
|
|
response = client.post(endpoint, json={})
|
|
|
|
assert response.status_code != 302 # Not a redirect
|
|
|
|
@pytest.mark.skipif(
|
|
os.environ.get("CI") == "true"
|
|
or os.environ.get("GITHUB_ACTIONS") == "true",
|
|
reason="Password change with encrypted DB re-keying is complex to test in CI",
|
|
)
|
|
def test_password_change_flow(self, client):
|
|
"""Test the complete password change flow."""
|
|
# Register user
|
|
client.post(
|
|
"/auth/register",
|
|
data={
|
|
"username": "changetest",
|
|
"password": "OldPass123",
|
|
"confirm_password": "OldPass123",
|
|
"acknowledge": "true",
|
|
},
|
|
)
|
|
|
|
# Change password
|
|
response = client.post(
|
|
"/auth/change-password",
|
|
data={
|
|
"current_password": "OldPass123",
|
|
"new_password": "NewPass456", # pragma: allowlist secret
|
|
"confirm_password": "NewPass456",
|
|
},
|
|
follow_redirects=True,
|
|
)
|
|
|
|
assert b"Password changed successfully" in response.data
|
|
assert b"Please login with your new password" in response.data
|
|
|
|
# Should be logged out
|
|
response = client.get("/auth/check")
|
|
assert response.status_code == 401
|
|
|
|
# Old password should fail
|
|
response = client.post(
|
|
"/auth/login",
|
|
data={"username": "changetest", "password": "OldPass123"},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
# New password should work
|
|
response = client.post(
|
|
"/auth/login",
|
|
data={"username": "changetest", "password": "NewPass456"},
|
|
follow_redirects=True,
|
|
)
|
|
assert response.status_code == 200
|
|
|
|
# Verify logged in
|
|
response = client.get("/auth/check")
|
|
data = response.get_json()
|
|
assert data["username"] == "changetest"
|