bd26b09827
- /auth/refresh now allows a short grace window past expiry and checks the blacklist, so an active session's silent refresh no longer fails the instant its own token expires - normal manager /approve flow now creates Jira tickets for the campaign and its already-linked tests, matching the admin-only /activate path - GenerateFromActorPayload now accepts start_date and threads it through to the new campaign instead of silently discarding it
187 lines
5.9 KiB
Python
187 lines
5.9 KiB
Python
"""Tests for authentication endpoints."""
|
|
|
|
import uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
import jwt
|
|
import pytest
|
|
|
|
from app.config import settings
|
|
|
|
|
|
def _make_token(username: str, *, expired_seconds_ago: int | None = None) -> str:
|
|
"""Build a raw JWT mirroring ``create_access_token`` with a controllable exp."""
|
|
if expired_seconds_ago is None:
|
|
expire = datetime.now(timezone.utc) + timedelta(minutes=30)
|
|
else:
|
|
expire = datetime.now(timezone.utc) - timedelta(seconds=expired_seconds_ago)
|
|
payload = {"sub": username, "exp": expire, "jti": str(uuid.uuid4())}
|
|
return jwt.encode(payload, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
|
|
|
|
|
def test_login_success(client, admin_user):
|
|
"""Test successful login returns a token."""
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "admin", "password": "admin123"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert "access_token" in data
|
|
assert data["token_type"] == "bearer"
|
|
|
|
|
|
def test_login_wrong_password(client, admin_user):
|
|
"""Test login with wrong password returns 400."""
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "admin", "password": "wrongpassword"},
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_login_nonexistent_user(client):
|
|
"""Test login with non-existent user returns 400."""
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "nobody", "password": "password"},
|
|
)
|
|
assert response.status_code == 400
|
|
|
|
|
|
def test_login_inactive_user(client, db):
|
|
"""Test login with inactive user returns 400."""
|
|
from app.auth import hash_password
|
|
from app.models.user import User
|
|
|
|
user = User(
|
|
username="inactive",
|
|
hashed_password=hash_password("password"),
|
|
role="viewer",
|
|
is_active=False,
|
|
)
|
|
db.add(user)
|
|
db.commit()
|
|
|
|
response = client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "inactive", "password": "password"},
|
|
)
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_get_me_with_token(client, admin_user, admin_token):
|
|
"""Test /auth/me returns current user with valid token."""
|
|
response = client.get(
|
|
"/api/v1/auth/me",
|
|
headers={"Authorization": f"Bearer {admin_token}"},
|
|
)
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["username"] == "admin"
|
|
assert data["role"] == "admin"
|
|
|
|
|
|
def test_get_me_without_token(client):
|
|
"""Test /auth/me returns 401 without token."""
|
|
response = client.get("/api/v1/auth/me")
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_get_me_invalid_token(client):
|
|
"""Test /auth/me returns 401 with invalid token."""
|
|
response = client.get(
|
|
"/api/v1/auth/me",
|
|
headers={"Authorization": "Bearer invalidtoken"},
|
|
)
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_logout_revokes_token(client, admin_user):
|
|
"""After logout, the same JWT must be rejected (Redis blacklist).
|
|
|
|
Prefer Authorization over the HttpOnly cookie so logout blacklists the
|
|
same token the client sends on the next request; clear cookies to avoid
|
|
stale jar state across calls.
|
|
"""
|
|
login = client.post(
|
|
"/api/v1/auth/login",
|
|
data={"username": "admin", "password": "admin123"},
|
|
)
|
|
assert login.status_code == 200
|
|
token = login.json()["access_token"]
|
|
|
|
client.cookies.clear()
|
|
out = client.post(
|
|
"/api/v1/auth/logout",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert out.status_code == 200
|
|
|
|
import jwt
|
|
|
|
from app.config import settings
|
|
from app.infrastructure.redis_client import get_redis_blacklist
|
|
|
|
payload = jwt.decode(
|
|
token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM],
|
|
)
|
|
jti = payload.get("jti")
|
|
assert jti
|
|
assert get_redis_blacklist().exists(f"blacklist:{jti}")
|
|
|
|
client.cookies.clear()
|
|
assert not len(client.cookies), "cookie jar must be empty to force Authorization Bearer"
|
|
me = client.get(
|
|
"/api/v1/auth/me",
|
|
headers={"Authorization": f"Bearer {token}"},
|
|
)
|
|
assert me.status_code == 401
|
|
assert me.json()["detail"] == "Token has been revoked"
|
|
|
|
|
|
def test_refresh_without_cookie_fails(client):
|
|
response = client.post("/api/v1/auth/refresh")
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_refresh_valid_token_succeeds(client, admin_user):
|
|
token = _make_token("admin")
|
|
client.cookies.set("aegis_token", token)
|
|
response = client.post("/api/v1/auth/refresh")
|
|
assert response.status_code == 200
|
|
assert "access_token" in response.json()
|
|
|
|
|
|
def test_refresh_recently_expired_token_succeeds_within_grace(client, admin_user):
|
|
"""A token that expired moments ago must still be refreshable.
|
|
|
|
This is the core of the refresh-token bug fix: without a grace window,
|
|
`/auth/refresh` decodes with the same strict expiry check as every
|
|
other endpoint, so a 401-triggered refresh attempt always also fails.
|
|
"""
|
|
token = _make_token("admin", expired_seconds_ago=60)
|
|
client.cookies.set("aegis_token", token)
|
|
response = client.post("/api/v1/auth/refresh")
|
|
assert response.status_code == 200
|
|
assert "access_token" in response.json()
|
|
|
|
|
|
def test_refresh_long_expired_token_fails(client, admin_user):
|
|
token = _make_token("admin", expired_seconds_ago=60 * 60)
|
|
client.cookies.set("aegis_token", token)
|
|
response = client.post("/api/v1/auth/refresh")
|
|
assert response.status_code == 403
|
|
|
|
|
|
def test_refresh_blacklisted_token_fails_even_if_not_expired(client, admin_user):
|
|
from app.auth import blacklist_token
|
|
|
|
token = _make_token("admin")
|
|
payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM])
|
|
blacklist_token(payload["jti"], float(payload["exp"]))
|
|
|
|
client.cookies.set("aegis_token", token)
|
|
response = client.post("/api/v1/auth/refresh")
|
|
assert response.status_code == 403
|