Add function-scoped auth_client fixture for auth tests

- Add auth_client fixture (function-scoped) for authentication tests
- Update all tests in test_auth_api.py to use auth_client
- Prevents auth tests from polluting the shared integration_client session
- Each auth test gets a fresh client, avoiding state leakage
This commit is contained in:
Mondo Diaz
2026-01-16 21:14:40 +00:00
parent 29e8638d7b
commit 28b434b944
2 changed files with 127 additions and 107 deletions

View File

@@ -249,6 +249,26 @@ def integration_client():
yield client yield client
@pytest.fixture
def auth_client():
"""
Create a function-scoped test client for authentication tests.
Unlike integration_client (session-scoped), this creates a fresh client
for each test. Use this for tests that manipulate authentication state
(login, logout, cookie clearing) to avoid polluting other tests.
Environment variables:
ORCHARD_TEST_URL: Base URL of the Orchard server (default: http://localhost:8080)
"""
import httpx
base_url = os.environ.get("ORCHARD_TEST_URL", "http://localhost:8080")
with httpx.Client(base_url=base_url, timeout=30.0) as client:
yield client
@pytest.fixture @pytest.fixture
def unique_test_id(): def unique_test_id():
"""Generate a unique ID for test isolation.""" """Generate a unique ID for test isolation."""

View File

@@ -17,9 +17,9 @@ class TestAuthLogin:
"""Tests for login endpoint.""" """Tests for login endpoint."""
@pytest.mark.integration @pytest.mark.integration
def test_login_success(self, integration_client): def test_login_success(self, auth_client):
"""Test successful login with default admin credentials.""" """Test successful login with default admin credentials."""
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
@@ -30,9 +30,9 @@ class TestAuthLogin:
assert "orchard_session" in response.cookies assert "orchard_session" in response.cookies
@pytest.mark.integration @pytest.mark.integration
def test_login_invalid_password(self, integration_client): def test_login_invalid_password(self, auth_client):
"""Test login with wrong password.""" """Test login with wrong password."""
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "wrongpassword"}, json={"username": "admin", "password": "wrongpassword"},
) )
@@ -40,9 +40,9 @@ class TestAuthLogin:
assert "Invalid username or password" in response.json()["detail"] assert "Invalid username or password" in response.json()["detail"]
@pytest.mark.integration @pytest.mark.integration
def test_login_nonexistent_user(self, integration_client): def test_login_nonexistent_user(self, auth_client):
"""Test login with non-existent user.""" """Test login with non-existent user."""
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "nonexistent", "password": "password"}, json={"username": "nonexistent", "password": "password"},
) )
@@ -53,24 +53,24 @@ class TestAuthLogout:
"""Tests for logout endpoint.""" """Tests for logout endpoint."""
@pytest.mark.integration @pytest.mark.integration
def test_logout_success(self, integration_client): def test_logout_success(self, auth_client):
"""Test successful logout.""" """Test successful logout."""
# First login # First login
login_response = integration_client.post( login_response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
assert login_response.status_code == 200 assert login_response.status_code == 200
# Then logout # Then logout
logout_response = integration_client.post("/api/v1/auth/logout") logout_response = auth_client.post("/api/v1/auth/logout")
assert logout_response.status_code == 200 assert logout_response.status_code == 200
assert "Logged out successfully" in logout_response.json()["message"] assert "Logged out successfully" in logout_response.json()["message"]
@pytest.mark.integration @pytest.mark.integration
def test_logout_without_session(self, integration_client): def test_logout_without_session(self, auth_client):
"""Test logout without being logged in.""" """Test logout without being logged in."""
response = integration_client.post("/api/v1/auth/logout") response = auth_client.post("/api/v1/auth/logout")
# Should succeed even without session # Should succeed even without session
assert response.status_code == 200 assert response.status_code == 200
@@ -79,15 +79,15 @@ class TestAuthMe:
"""Tests for get current user endpoint.""" """Tests for get current user endpoint."""
@pytest.mark.integration @pytest.mark.integration
def test_get_me_authenticated(self, integration_client): def test_get_me_authenticated(self, auth_client):
"""Test getting current user when authenticated.""" """Test getting current user when authenticated."""
# Login first # Login first
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
response = integration_client.get("/api/v1/auth/me") response = auth_client.get("/api/v1/auth/me")
assert response.status_code == 200 assert response.status_code == 200
data = response.json() data = response.json()
assert data["username"] == "admin" assert data["username"] == "admin"
@@ -96,12 +96,12 @@ class TestAuthMe:
assert "created_at" in data assert "created_at" in data
@pytest.mark.integration @pytest.mark.integration
def test_get_me_unauthenticated(self, integration_client): def test_get_me_unauthenticated(self, auth_client):
"""Test getting current user without authentication.""" """Test getting current user without authentication."""
# Clear any existing cookies # Clear any existing cookies
integration_client.cookies.clear() auth_client.cookies.clear()
response = integration_client.get("/api/v1/auth/me") response = auth_client.get("/api/v1/auth/me")
assert response.status_code == 401 assert response.status_code == 401
assert "Not authenticated" in response.json()["detail"] assert "Not authenticated" in response.json()["detail"]
@@ -110,53 +110,53 @@ class TestAuthChangePassword:
"""Tests for change password endpoint.""" """Tests for change password endpoint."""
@pytest.mark.integration @pytest.mark.integration
def test_change_password_success(self, integration_client): def test_change_password_success(self, auth_client):
"""Test successful password change.""" """Test successful password change."""
# Login first # Login first
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
# Change password # Change password
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/change-password", "/api/v1/auth/change-password",
json={"current_password": "changeme123", "new_password": "newpassword123"}, json={"current_password": "changeme123", "new_password": "newpassword123"},
) )
assert response.status_code == 200 assert response.status_code == 200
# Verify old password no longer works # Verify old password no longer works
integration_client.cookies.clear() auth_client.cookies.clear()
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
assert response.status_code == 401 assert response.status_code == 401
# Verify new password works # Verify new password works
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "newpassword123"}, json={"username": "admin", "password": "newpassword123"},
) )
assert response.status_code == 200 assert response.status_code == 200
# Reset password back to original for other tests # Reset password back to original for other tests
reset_response = integration_client.post( reset_response = auth_client.post(
"/api/v1/auth/change-password", "/api/v1/auth/change-password",
json={"current_password": "newpassword123", "new_password": "changeme123"}, json={"current_password": "newpassword123", "new_password": "changeme123"},
) )
assert reset_response.status_code == 200, "Failed to reset admin password back to default" assert reset_response.status_code == 200, "Failed to reset admin password back to default"
@pytest.mark.integration @pytest.mark.integration
def test_change_password_wrong_current(self, integration_client): def test_change_password_wrong_current(self, auth_client):
"""Test password change with wrong current password.""" """Test password change with wrong current password."""
# Login first # Login first
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/change-password", "/api/v1/auth/change-password",
json={"current_password": "wrongpassword", "new_password": "newpassword"}, json={"current_password": "wrongpassword", "new_password": "newpassword"},
) )
@@ -168,16 +168,16 @@ class TestAPIKeys:
"""Tests for API key management endpoints.""" """Tests for API key management endpoints."""
@pytest.mark.integration @pytest.mark.integration
def test_create_and_list_api_key(self, integration_client): def test_create_and_list_api_key(self, auth_client):
"""Test creating and listing API keys.""" """Test creating and listing API keys."""
# Login first # Login first
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
# Create API key # Create API key
create_response = integration_client.post( create_response = auth_client.post(
"/api/v1/auth/keys", "/api/v1/auth/keys",
json={"name": "test-key", "description": "Test API key"}, json={"name": "test-key", "description": "Test API key"},
) )
@@ -191,23 +191,23 @@ class TestAPIKeys:
api_key = data["key"] api_key = data["key"]
# List API keys # List API keys
list_response = integration_client.get("/api/v1/auth/keys") list_response = auth_client.get("/api/v1/auth/keys")
assert list_response.status_code == 200 assert list_response.status_code == 200
keys = list_response.json() keys = list_response.json()
assert any(k["id"] == key_id for k in keys) assert any(k["id"] == key_id for k in keys)
# Clean up - delete the key # Clean up - delete the key
integration_client.delete(f"/api/v1/auth/keys/{key_id}") auth_client.delete(f"/api/v1/auth/keys/{key_id}")
@pytest.mark.integration @pytest.mark.integration
def test_use_api_key_for_auth(self, integration_client): def test_use_api_key_for_auth(self, auth_client):
"""Test using API key for authentication.""" """Test using API key for authentication."""
# Login and create API key # Login and create API key
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
create_response = integration_client.post( create_response = auth_client.post(
"/api/v1/auth/keys", "/api/v1/auth/keys",
json={"name": "auth-test-key"}, json={"name": "auth-test-key"},
) )
@@ -215,8 +215,8 @@ class TestAPIKeys:
key_id = create_response.json()["id"] key_id = create_response.json()["id"]
# Clear cookies and use API key # Clear cookies and use API key
integration_client.cookies.clear() auth_client.cookies.clear()
response = integration_client.get( response = auth_client.get(
"/api/v1/auth/me", "/api/v1/auth/me",
headers={"Authorization": f"Bearer {api_key}"}, headers={"Authorization": f"Bearer {api_key}"},
) )
@@ -224,21 +224,21 @@ class TestAPIKeys:
assert response.json()["username"] == "admin" assert response.json()["username"] == "admin"
# Clean up # Clean up
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
integration_client.delete(f"/api/v1/auth/keys/{key_id}") auth_client.delete(f"/api/v1/auth/keys/{key_id}")
@pytest.mark.integration @pytest.mark.integration
def test_delete_api_key(self, integration_client): def test_delete_api_key(self, auth_client):
"""Test revoking an API key.""" """Test revoking an API key."""
# Login and create API key # Login and create API key
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
create_response = integration_client.post( create_response = auth_client.post(
"/api/v1/auth/keys", "/api/v1/auth/keys",
json={"name": "delete-test-key"}, json={"name": "delete-test-key"},
) )
@@ -246,12 +246,12 @@ class TestAPIKeys:
api_key = create_response.json()["key"] api_key = create_response.json()["key"]
# Delete the key # Delete the key
delete_response = integration_client.delete(f"/api/v1/auth/keys/{key_id}") delete_response = auth_client.delete(f"/api/v1/auth/keys/{key_id}")
assert delete_response.status_code == 200 assert delete_response.status_code == 200
# Verify key no longer works # Verify key no longer works
integration_client.cookies.clear() auth_client.cookies.clear()
response = integration_client.get( response = auth_client.get(
"/api/v1/auth/me", "/api/v1/auth/me",
headers={"Authorization": f"Bearer {api_key}"}, headers={"Authorization": f"Bearer {api_key}"},
) )
@@ -262,32 +262,32 @@ class TestAdminUserManagement:
"""Tests for admin user management endpoints.""" """Tests for admin user management endpoints."""
@pytest.mark.integration @pytest.mark.integration
def test_list_users(self, integration_client): def test_list_users(self, auth_client):
"""Test listing users as admin.""" """Test listing users as admin."""
# Login as admin # Login as admin
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
response = integration_client.get("/api/v1/admin/users") response = auth_client.get("/api/v1/admin/users")
assert response.status_code == 200 assert response.status_code == 200
users = response.json() users = response.json()
assert len(users) >= 1 assert len(users) >= 1
assert any(u["username"] == "admin" for u in users) assert any(u["username"] == "admin" for u in users)
@pytest.mark.integration @pytest.mark.integration
def test_create_user(self, integration_client): def test_create_user(self, auth_client):
"""Test creating a new user as admin.""" """Test creating a new user as admin."""
# Login as admin # Login as admin
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
# Create new user # Create new user
test_username = f"testuser_{uuid4().hex[:8]}" test_username = f"testuser_{uuid4().hex[:8]}"
response = integration_client.post( response = auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={ json={
"username": test_username, "username": test_username,
@@ -302,31 +302,31 @@ class TestAdminUserManagement:
assert data["is_admin"] is False assert data["is_admin"] is False
# Verify new user can login # Verify new user can login
integration_client.cookies.clear() auth_client.cookies.clear()
login_response = integration_client.post( login_response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": test_username, "password": "testpassword"}, json={"username": test_username, "password": "testpassword"},
) )
assert login_response.status_code == 200 assert login_response.status_code == 200
@pytest.mark.integration @pytest.mark.integration
def test_update_user(self, integration_client): def test_update_user(self, auth_client):
"""Test updating a user as admin.""" """Test updating a user as admin."""
# Login as admin # Login as admin
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
# Create a test user # Create a test user
test_username = f"updateuser_{uuid4().hex[:8]}" test_username = f"updateuser_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password"}, json={"username": test_username, "password": "password"},
) )
# Update the user # Update the user
response = integration_client.put( response = auth_client.put(
f"/api/v1/admin/users/{test_username}", f"/api/v1/admin/users/{test_username}",
json={"email": "updated@example.com", "is_admin": True}, json={"email": "updated@example.com", "is_admin": True},
) )
@@ -336,59 +336,59 @@ class TestAdminUserManagement:
assert data["is_admin"] is True assert data["is_admin"] is True
@pytest.mark.integration @pytest.mark.integration
def test_reset_user_password(self, integration_client): def test_reset_user_password(self, auth_client):
"""Test resetting a user's password as admin.""" """Test resetting a user's password as admin."""
# Login as admin # Login as admin
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
# Create a test user # Create a test user
test_username = f"resetuser_{uuid4().hex[:8]}" test_username = f"resetuser_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "oldpassword"}, json={"username": test_username, "password": "oldpassword"},
) )
# Reset password # Reset password
response = integration_client.post( response = auth_client.post(
f"/api/v1/admin/users/{test_username}/reset-password", f"/api/v1/admin/users/{test_username}/reset-password",
json={"new_password": "newpassword"}, json={"new_password": "newpassword"},
) )
assert response.status_code == 200 assert response.status_code == 200
# Verify new password works # Verify new password works
integration_client.cookies.clear() auth_client.cookies.clear()
login_response = integration_client.post( login_response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": test_username, "password": "newpassword"}, json={"username": test_username, "password": "newpassword"},
) )
assert login_response.status_code == 200 assert login_response.status_code == 200
@pytest.mark.integration @pytest.mark.integration
def test_non_admin_cannot_access_admin_endpoints(self, integration_client): def test_non_admin_cannot_access_admin_endpoints(self, auth_client):
"""Test that non-admin users cannot access admin endpoints.""" """Test that non-admin users cannot access admin endpoints."""
# Login as admin and create non-admin user # Login as admin and create non-admin user
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
test_username = f"nonadmin_{uuid4().hex[:8]}" test_username = f"nonadmin_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password", "is_admin": False}, json={"username": test_username, "password": "password", "is_admin": False},
) )
# Login as non-admin # Login as non-admin
integration_client.cookies.clear() auth_client.cookies.clear()
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": test_username, "password": "password"}, json={"username": test_username, "password": "password"},
) )
# Try to access admin endpoints # Try to access admin endpoints
response = integration_client.get("/api/v1/admin/users") response = auth_client.get("/api/v1/admin/users")
assert response.status_code == 403 assert response.status_code == 403
assert "Admin privileges required" in response.json()["detail"] assert "Admin privileges required" in response.json()["detail"]
@@ -397,28 +397,28 @@ class TestSecurityEdgeCases:
"""Tests for security edge cases and validation.""" """Tests for security edge cases and validation."""
@pytest.mark.integration @pytest.mark.integration
def test_login_inactive_user(self, integration_client): def test_login_inactive_user(self, auth_client):
"""Test that inactive users cannot login.""" """Test that inactive users cannot login."""
# Login as admin and create a user # Login as admin and create a user
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
test_username = f"inactive_{uuid4().hex[:8]}" test_username = f"inactive_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
# Deactivate the user # Deactivate the user
integration_client.put( auth_client.put(
f"/api/v1/admin/users/{test_username}", f"/api/v1/admin/users/{test_username}",
json={"is_active": False}, json={"is_active": False},
) )
# Try to login as inactive user # Try to login as inactive user
integration_client.cookies.clear() auth_client.cookies.clear()
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
@@ -426,14 +426,14 @@ class TestSecurityEdgeCases:
assert "Invalid username or password" in response.json()["detail"] assert "Invalid username or password" in response.json()["detail"]
@pytest.mark.integration @pytest.mark.integration
def test_password_too_short_on_create(self, integration_client): def test_password_too_short_on_create(self, auth_client):
"""Test that short passwords are rejected when creating users.""" """Test that short passwords are rejected when creating users."""
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
response = integration_client.post( response = auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": f"shortpw_{uuid4().hex[:8]}", "password": "short"}, json={"username": f"shortpw_{uuid4().hex[:8]}", "password": "short"},
) )
@@ -441,14 +441,14 @@ class TestSecurityEdgeCases:
assert "at least 8 characters" in response.json()["detail"] assert "at least 8 characters" in response.json()["detail"]
@pytest.mark.integration @pytest.mark.integration
def test_password_too_short_on_change(self, integration_client): def test_password_too_short_on_change(self, auth_client):
"""Test that short passwords are rejected when changing password.""" """Test that short passwords are rejected when changing password."""
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
response = integration_client.post( response = auth_client.post(
"/api/v1/auth/change-password", "/api/v1/auth/change-password",
json={"current_password": "changeme123", "new_password": "short"}, json={"current_password": "changeme123", "new_password": "short"},
) )
@@ -456,21 +456,21 @@ class TestSecurityEdgeCases:
assert "at least 8 characters" in response.json()["detail"] assert "at least 8 characters" in response.json()["detail"]
@pytest.mark.integration @pytest.mark.integration
def test_password_too_short_on_reset(self, integration_client): def test_password_too_short_on_reset(self, auth_client):
"""Test that short passwords are rejected when resetting password.""" """Test that short passwords are rejected when resetting password."""
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
# Create a test user first # Create a test user first
test_username = f"resetshort_{uuid4().hex[:8]}" test_username = f"resetshort_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
response = integration_client.post( response = auth_client.post(
f"/api/v1/admin/users/{test_username}/reset-password", f"/api/v1/admin/users/{test_username}/reset-password",
json={"new_password": "short"}, json={"new_password": "short"},
) )
@@ -478,23 +478,23 @@ class TestSecurityEdgeCases:
assert "at least 8 characters" in response.json()["detail"] assert "at least 8 characters" in response.json()["detail"]
@pytest.mark.integration @pytest.mark.integration
def test_duplicate_username_rejected(self, integration_client): def test_duplicate_username_rejected(self, auth_client):
"""Test that duplicate usernames are rejected.""" """Test that duplicate usernames are rejected."""
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
test_username = f"duplicate_{uuid4().hex[:8]}" test_username = f"duplicate_{uuid4().hex[:8]}"
# Create user first time # Create user first time
response1 = integration_client.post( response1 = auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
assert response1.status_code == 200 assert response1.status_code == 200
# Try to create same username again # Try to create same username again
response2 = integration_client.post( response2 = auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password456"}, json={"username": test_username, "password": "password456"},
) )
@@ -502,14 +502,14 @@ class TestSecurityEdgeCases:
assert "already exists" in response2.json()["detail"] assert "already exists" in response2.json()["detail"]
@pytest.mark.integration @pytest.mark.integration
def test_cannot_delete_other_users_api_key(self, integration_client): def test_cannot_delete_other_users_api_key(self, auth_client):
"""Test that users cannot delete API keys owned by other users.""" """Test that users cannot delete API keys owned by other users."""
# Login as admin and create an API key # Login as admin and create an API key
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
create_response = integration_client.post( create_response = auth_client.post(
"/api/v1/auth/keys", "/api/v1/auth/keys",
json={"name": "admin-key"}, json={"name": "admin-key"},
) )
@@ -517,65 +517,65 @@ class TestSecurityEdgeCases:
# Create a non-admin user # Create a non-admin user
test_username = f"nonadmin_{uuid4().hex[:8]}" test_username = f"nonadmin_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
# Login as non-admin # Login as non-admin
integration_client.cookies.clear() auth_client.cookies.clear()
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
# Try to delete admin's API key # Try to delete admin's API key
response = integration_client.delete(f"/api/v1/auth/keys/{admin_key_id}") response = auth_client.delete(f"/api/v1/auth/keys/{admin_key_id}")
assert response.status_code == 403 assert response.status_code == 403
assert "Cannot delete another user's API key" in response.json()["detail"] assert "Cannot delete another user's API key" in response.json()["detail"]
# Cleanup: login as admin and delete the key # Cleanup: login as admin and delete the key
integration_client.cookies.clear() auth_client.cookies.clear()
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
integration_client.delete(f"/api/v1/auth/keys/{admin_key_id}") auth_client.delete(f"/api/v1/auth/keys/{admin_key_id}")
@pytest.mark.integration @pytest.mark.integration
def test_sessions_invalidated_on_password_change(self, integration_client): def test_sessions_invalidated_on_password_change(self, auth_client):
"""Test that all sessions are invalidated when password is changed.""" """Test that all sessions are invalidated when password is changed."""
# Create a test user # Create a test user
integration_client.post( auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": "admin", "password": "changeme123"}, json={"username": "admin", "password": "changeme123"},
) )
test_username = f"sessiontest_{uuid4().hex[:8]}" test_username = f"sessiontest_{uuid4().hex[:8]}"
integration_client.post( auth_client.post(
"/api/v1/admin/users", "/api/v1/admin/users",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
# Login as test user # Login as test user
integration_client.cookies.clear() auth_client.cookies.clear()
login_response = integration_client.post( login_response = auth_client.post(
"/api/v1/auth/login", "/api/v1/auth/login",
json={"username": test_username, "password": "password123"}, json={"username": test_username, "password": "password123"},
) )
assert login_response.status_code == 200 assert login_response.status_code == 200
# Verify session works # Verify session works
me_response = integration_client.get("/api/v1/auth/me") me_response = auth_client.get("/api/v1/auth/me")
assert me_response.status_code == 200 assert me_response.status_code == 200
# Change password # Change password
integration_client.post( auth_client.post(
"/api/v1/auth/change-password", "/api/v1/auth/change-password",
json={"current_password": "password123", "new_password": "newpassword123"}, json={"current_password": "password123", "new_password": "newpassword123"},
) )
# Old session should be invalidated - try to access /me # Old session should be invalidated - try to access /me
# (note: the change-password call itself may have cleared the session cookie) # (note: the change-password call itself may have cleared the session cookie)
me_response2 = integration_client.get("/api/v1/auth/me") me_response2 = auth_client.get("/api/v1/auth/me")
# This should fail because all sessions were invalidated # This should fail because all sessions were invalidated
assert me_response2.status_code == 401 assert me_response2.status_code == 401