117 lines
4.2 KiB
Python
117 lines
4.2 KiB
Python
|
|
"""US-003: Authentication and security tests."""
|
||
|
|
import pytest
|
||
|
|
from fastapi.testclient import TestClient
|
||
|
|
|
||
|
|
|
||
|
|
class TestLogin:
|
||
|
|
def test_login_success(self, client: TestClient):
|
||
|
|
resp = client.post("/api/auth/login", json={
|
||
|
|
"username": "admin", "password": "admin123"
|
||
|
|
})
|
||
|
|
assert resp.status_code == 200
|
||
|
|
data = resp.json()
|
||
|
|
assert "access_token" in data
|
||
|
|
assert len(data["access_token"]) > 0
|
||
|
|
|
||
|
|
def test_login_wrong_password(self, client: TestClient):
|
||
|
|
resp = client.post("/api/auth/login", json={
|
||
|
|
"username": "admin", "password": "wrongpassword"
|
||
|
|
})
|
||
|
|
assert resp.status_code == 401
|
||
|
|
assert "Incorrect username or password" in resp.json()["detail"]
|
||
|
|
|
||
|
|
def test_login_nonexistent_user(self, client: TestClient):
|
||
|
|
resp = client.post("/api/auth/login", json={
|
||
|
|
"username": "nonexistent_user_xyz", "password": "password"
|
||
|
|
})
|
||
|
|
assert resp.status_code == 401
|
||
|
|
|
||
|
|
def test_login_empty_username(self, client: TestClient):
|
||
|
|
resp = client.post("/api/auth/login", json={
|
||
|
|
"username": "", "password": "password"
|
||
|
|
})
|
||
|
|
assert resp.status_code in (401, 422)
|
||
|
|
|
||
|
|
|
||
|
|
class TestRegister:
|
||
|
|
def test_register_new_user(self, client: TestClient):
|
||
|
|
resp = client.post("/api/auth/register", json={
|
||
|
|
"username": "testuser_001", "password": "testpass123"
|
||
|
|
})
|
||
|
|
assert resp.status_code == 201
|
||
|
|
assert resp.json()["username"] == "testuser_001"
|
||
|
|
|
||
|
|
def test_register_duplicate(self, client: TestClient):
|
||
|
|
client.post("/api/auth/register", json={
|
||
|
|
"username": "dup_user", "password": "testpass123"
|
||
|
|
})
|
||
|
|
resp = client.post("/api/auth/register", json={
|
||
|
|
"username": "dup_user", "password": "testpass123"
|
||
|
|
})
|
||
|
|
assert resp.status_code == 409
|
||
|
|
assert "already exists" in resp.json()["detail"]
|
||
|
|
|
||
|
|
def test_register_missing_password(self, client: TestClient):
|
||
|
|
resp = client.post("/api/auth/register", json={"username": "user"})
|
||
|
|
assert resp.status_code == 422
|
||
|
|
|
||
|
|
|
||
|
|
class TestTokenAuth:
|
||
|
|
def test_me_with_valid_token(self, client: TestClient, auth_token):
|
||
|
|
resp = client.get(
|
||
|
|
"/api/auth/me",
|
||
|
|
headers={"Authorization": f"Bearer {auth_token}"}
|
||
|
|
)
|
||
|
|
assert resp.status_code == 200
|
||
|
|
assert resp.json()["username"] == "admin"
|
||
|
|
|
||
|
|
def test_me_without_token(self, client: TestClient):
|
||
|
|
resp = client.get("/api/auth/me")
|
||
|
|
assert resp.status_code in (401, 403)
|
||
|
|
|
||
|
|
def test_me_with_invalid_token(self, client: TestClient):
|
||
|
|
resp = client.get(
|
||
|
|
"/api/auth/me",
|
||
|
|
headers={"Authorization": "Bearer invalid.token.here"}
|
||
|
|
)
|
||
|
|
assert resp.status_code in (401, 403)
|
||
|
|
|
||
|
|
def test_me_with_malformed_header(self, client: TestClient):
|
||
|
|
resp = client.get(
|
||
|
|
"/api/auth/me",
|
||
|
|
headers={"Authorization": "NotBearer token"}
|
||
|
|
)
|
||
|
|
assert resp.status_code in (401, 403)
|
||
|
|
|
||
|
|
def test_me_with_expired_token(self, client: TestClient):
|
||
|
|
"""Token with past expiration should be rejected."""
|
||
|
|
expired = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZG1pbiIsImV4cCI6MTcwMDAwMDAwMH0.fake"
|
||
|
|
resp = client.get(
|
||
|
|
"/api/auth/me",
|
||
|
|
headers={"Authorization": f"Bearer {expired}"}
|
||
|
|
)
|
||
|
|
assert resp.status_code in (401, 403)
|
||
|
|
|
||
|
|
def test_login_then_access_protected(self, client: TestClient):
|
||
|
|
login_resp = client.post("/api/auth/login", json={
|
||
|
|
"username": "admin", "password": "admin123"
|
||
|
|
})
|
||
|
|
token = login_resp.json()["access_token"]
|
||
|
|
|
||
|
|
resp = client.get(
|
||
|
|
"/api/auth/me",
|
||
|
|
headers={"Authorization": f"Bearer {token}"}
|
||
|
|
)
|
||
|
|
assert resp.status_code == 200
|
||
|
|
assert resp.json()["username"] == "admin"
|
||
|
|
|
||
|
|
|
||
|
|
class TestPasswordHashing:
|
||
|
|
def test_bcrypt_not_plaintext(self, client: TestClient):
|
||
|
|
"""Passwords should be hashed, not stored as plaintext."""
|
||
|
|
from auth.service import hash_password, verify_password
|
||
|
|
hashed = hash_password("test_password")
|
||
|
|
assert hashed != "test_password"
|
||
|
|
assert verify_password("test_password", hashed)
|
||
|
|
assert not verify_password("wrong_password", hashed)
|