a86f11a3db
- Fix logout test to expect {ok: true} instead of {message: ...}
- Fix logout without auth test (endpoint doesn't require auth)
- Fix password change tests to use current_password instead of old_password
- Fix password change error code expectation (400 instead of 401)
- Add Python venv caching to speed up backend tests
- Add Node modules caching to speed up frontend tests
- Cache is keyed by requirements/package-lock file hashes
321 lines
11 KiB
Python
321 lines
11 KiB
Python
"""
|
|
Integration tests for authentication flow.
|
|
"""
|
|
import pytest
|
|
import pyotp
|
|
from fastapi import status
|
|
from auth_service import get_password_hash, save_totp_secret, save_password_hash
|
|
|
|
|
|
class TestLoginFlow:
|
|
"""Test login endpoint."""
|
|
|
|
def test_login_success_no_2fa(self, test_app, mock_config, temp_auth_file):
|
|
"""Test successful login without 2FA."""
|
|
# Set up password
|
|
password = "test_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "testadmin", "password": password, "totp_code": ""}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["user"] == "testadmin"
|
|
assert data["role"] == "admin"
|
|
assert data["has_2fa"] is False
|
|
assert "access_token" in data
|
|
assert "refresh_token" in data
|
|
assert data["token_type"] == "bearer"
|
|
|
|
def test_login_success_with_2fa(self, test_app, mock_config, temp_auth_file, sample_totp_secret):
|
|
"""Test successful login with 2FA."""
|
|
password = "test_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
save_totp_secret(sample_totp_secret)
|
|
|
|
totp = pyotp.TOTP(sample_totp_secret)
|
|
valid_code = totp.now()
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "testadmin", "password": password, "totp_code": valid_code}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["user"] == "testadmin"
|
|
assert data["has_2fa"] is True
|
|
|
|
def test_login_wrong_password(self, test_app, mock_config, temp_auth_file):
|
|
"""Test login with wrong password."""
|
|
password = "correct_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "testadmin", "password": "wrong_password", "totp_code": ""}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
assert "Invalid credentials" in response.json()["detail"]
|
|
|
|
def test_login_wrong_username(self, test_app, mock_config, temp_auth_file):
|
|
"""Test login with wrong username."""
|
|
password = "test_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "wronguser", "password": password, "totp_code": ""}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_login_missing_2fa_code(self, test_app, mock_config, temp_auth_file, sample_totp_secret):
|
|
"""Test login with 2FA enabled but no code provided."""
|
|
password = "test_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
save_totp_secret(sample_totp_secret)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "testadmin", "password": password, "totp_code": ""}
|
|
)
|
|
|
|
assert response.status_code == 403
|
|
assert "2FA code required" in response.json()["detail"]
|
|
|
|
def test_login_invalid_2fa_code(self, test_app, mock_config, temp_auth_file, sample_totp_secret):
|
|
"""Test login with invalid 2FA code."""
|
|
password = "test_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
save_totp_secret(sample_totp_secret)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "testadmin", "password": password, "totp_code": "000000"}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_login_sets_cookies(self, test_app, mock_config, temp_auth_file):
|
|
"""Test that login sets auth cookies."""
|
|
password = "test_password"
|
|
hashed = get_password_hash(password)
|
|
save_password_hash(hashed)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/login",
|
|
json={"username": "testadmin", "password": password, "totp_code": ""}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
# Check that cookies are set
|
|
assert "nas_access_token" in response.cookies
|
|
assert "nas_refresh_token" in response.cookies
|
|
|
|
|
|
class TestRefreshFlow:
|
|
"""Test token refresh endpoint."""
|
|
|
|
def test_refresh_with_valid_token(self, test_app, mock_config, temp_auth_file, valid_refresh_token):
|
|
"""Test refreshing with valid refresh token."""
|
|
response = test_app.post(
|
|
"/api/auth/refresh",
|
|
json={"refresh_token": valid_refresh_token}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "access_token" in data
|
|
assert "refresh_token" in data
|
|
assert data["access_token"] != valid_refresh_token
|
|
|
|
def test_refresh_with_cookie(self, test_app, mock_config, temp_auth_file, valid_refresh_token):
|
|
"""Test refreshing with refresh token in cookie."""
|
|
test_app.cookies.set("nas_refresh_token", valid_refresh_token)
|
|
|
|
response = test_app.post("/api/auth/refresh", json={})
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "access_token" in data
|
|
|
|
def test_refresh_with_invalid_token(self, test_app, mock_config, temp_auth_file):
|
|
"""Test refreshing with invalid token."""
|
|
response = test_app.post(
|
|
"/api/auth/refresh",
|
|
json={"refresh_token": "invalid.token.here"}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_refresh_with_expired_token(self, test_app, mock_config, temp_auth_file, expired_access_token):
|
|
"""Test refreshing with expired token."""
|
|
response = test_app.post(
|
|
"/api/auth/refresh",
|
|
json={"refresh_token": expired_access_token}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
|
|
def test_refresh_missing_token(self, test_app, mock_config, temp_auth_file):
|
|
"""Test refresh without providing token."""
|
|
response = test_app.post("/api/auth/refresh", json={})
|
|
|
|
assert response.status_code == 401
|
|
assert "Missing refresh token" in response.json()["detail"]
|
|
|
|
def test_refresh_with_access_token_fails(self, test_app, mock_config, temp_auth_file, valid_access_token):
|
|
"""Test that access token cannot be used for refresh."""
|
|
response = test_app.post(
|
|
"/api/auth/refresh",
|
|
json={"refresh_token": valid_access_token}
|
|
)
|
|
|
|
assert response.status_code == 401
|
|
assert "Invalid token type" in response.json()["detail"]
|
|
|
|
|
|
class TestLogoutFlow:
|
|
"""Test logout endpoint."""
|
|
|
|
def test_logout_success(self, test_app, mock_config, temp_auth_file, admin_headers):
|
|
"""Test successful logout."""
|
|
response = test_app.post("/api/auth/logout", headers=admin_headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["ok"] is True
|
|
|
|
def test_logout_clears_cookies(self, test_app, mock_config, temp_auth_file, admin_headers):
|
|
"""Test that logout clears auth cookies."""
|
|
response = test_app.post("/api/auth/logout", headers=admin_headers)
|
|
|
|
assert response.status_code == 200
|
|
# Cookies should be deleted (max-age=0 or expires in past)
|
|
# TestClient doesn't fully simulate cookie deletion, but we can check response
|
|
|
|
def test_logout_without_auth(self, test_app, mock_config, temp_auth_file):
|
|
"""Test logout without authentication - should succeed as logout doesn't require auth."""
|
|
response = test_app.post("/api/auth/logout")
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["ok"] is True
|
|
|
|
|
|
class TestUserInfo:
|
|
"""Test user info endpoint."""
|
|
|
|
def test_get_user_info(self, test_app, mock_config, temp_auth_file, temp_rbac_file, admin_headers):
|
|
"""Test getting current user info."""
|
|
response = test_app.get("/api/auth/me", headers=admin_headers)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["username"] == "testuser"
|
|
assert data["role"] == "admin"
|
|
assert "pages" in data
|
|
assert "sidebar_links" in data
|
|
|
|
def test_get_user_info_without_auth(self, test_app, mock_config, temp_auth_file):
|
|
"""Test getting user info without authentication."""
|
|
response = test_app.get("/api/auth/me")
|
|
|
|
assert response.status_code == 401
|
|
|
|
|
|
class TestProxyAuth:
|
|
"""Test proxy authentication endpoint."""
|
|
|
|
def test_proxy_auth_from_trusted_source(self, test_app, mock_config, temp_auth_file, temp_rbac_file):
|
|
"""Test proxy auth from trusted proxy."""
|
|
response = test_app.get(
|
|
"/api/auth/proxy",
|
|
headers={
|
|
"Remote-User": "proxyuser",
|
|
"Remote-Groups": "dashboard_admin"
|
|
}
|
|
)
|
|
|
|
# Note: This test may need adjustment based on actual proxy auth implementation
|
|
# The test client's default host may not be in trusted proxies
|
|
assert response.status_code in [200, 403]
|
|
|
|
def test_proxy_auth_from_untrusted_source(self, test_app, mock_config, temp_auth_file):
|
|
"""Test proxy auth from untrusted source."""
|
|
# Mock request from untrusted IP
|
|
response = test_app.get(
|
|
"/api/auth/proxy",
|
|
headers={
|
|
"Remote-User": "proxyuser",
|
|
"Remote-Groups": "dashboard_admin"
|
|
}
|
|
)
|
|
|
|
# Should reject untrusted proxy
|
|
assert response.status_code == 403
|
|
|
|
|
|
class TestPasswordChange:
|
|
"""Test password change endpoint."""
|
|
|
|
def test_change_password_success(self, test_app, mock_config, temp_auth_file, admin_headers):
|
|
"""Test successful password change."""
|
|
old_password = "old_password"
|
|
new_password = "new_password_123"
|
|
|
|
# Set initial password
|
|
hashed = get_password_hash(old_password)
|
|
save_password_hash(hashed)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/change-password",
|
|
headers=admin_headers,
|
|
json={
|
|
"current_password": old_password,
|
|
"new_password": new_password
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
|
|
def test_change_password_wrong_old_password(self, test_app, mock_config, temp_auth_file, admin_headers):
|
|
"""Test password change with wrong old password."""
|
|
old_password = "old_password"
|
|
hashed = get_password_hash(old_password)
|
|
save_password_hash(hashed)
|
|
|
|
response = test_app.post(
|
|
"/api/auth/change-password",
|
|
headers=admin_headers,
|
|
json={
|
|
"current_password": "wrong_password",
|
|
"new_password": "new_password_123"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
|
|
def test_change_password_without_auth(self, test_app, mock_config, temp_auth_file):
|
|
"""Test password change without authentication."""
|
|
response = test_app.post(
|
|
"/api/auth/change-password",
|
|
json={
|
|
"current_password": "old",
|
|
"new_password": "new"
|
|
}
|
|
)
|
|
|
|
assert response.status_code == 401
|