JWT (JSON Web Token) — это сердце многих современных систем аутентификации.
Они позволяют нам убедиться, что пользователи имеют доступ только к разрешённым ресурсам, но дают и простор для ошибок:
- Токен может быть недействительным или истёкшим.
- Пользователь может попытаться подделать токен.
- Неавторизованный запрос должен корректно возвращать ошибку
401 Unauthorized.
Качественное тестирование JWT гарантирует, что аутентификация работает предсказуемо, безопасно и эффективно.
Что будем тестировать?
Защищённые эндпоинты в FastAPI требуют:
- Генерации токена при успешной аутентификации.
- Проверки валидности токена.
- Ограничения доступа к защищённым ресурсам без токена или с неверным токеном.
Наша задача — протестировать эти сценарии.
Подготовка к тестированию
Перед тем как писать тесты, нужно убедиться, что у нас есть рабочая аутентификация через JWT в нашем FastAPI-приложении.
Давайте создадим небольшую базовую реализацию.
Давайте приведём простейший пример аутентификации через JWT.
Вот как может выглядеть базовый код для генерации и проверки JWT:
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import OAuth2PasswordBearer
from jose import jwt, JWTError
from datetime import datetime, timedelta
app = FastAPI()
SECRET_KEY = "supersecretkey"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
def create_access_token(data: dict):
to_encode = data.copy()
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
to_encode.update({"exp": expire})
return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
def verify_token(token: str):
try:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise HTTPException(status_code=401, detail="Invalid token")
@app.post("/token")
async def login(username: str, password: str):
# Считаем, что любой username/password валидны для простоты (не делайте так в проде!)
access_token = create_access_token({"sub": username})
return {"access_token": access_token, "token_type": "bearer"}
@app.get("/protected")
async def protected_route(token: str = Depends(oauth2_scheme)):
verify_token(token)
return {"message": "You have access!"}
Этот код:
- Генерирует JWT при логине (
/token). - Проверяет JWT при доступе к защищённым ресурсам (
/protected).
Установка зависимостей
Для работы с JWT и тестирования понадобятся дополнительные библиотеки:
pip install python-jose
pip install pytest pytest-asyncio httpx
Теперь мы готовы к тестам!
Тестирование генерации токена
Первый шаг — убедиться, что /token возвращает доступный JWT-токен. Напишем тест:
from fastapi.testclient import TestClient
from main import app # Импортируем приложение FastAPI
client = TestClient(app)
def test_generate_token():
response = client.post("/token", data={"username": "user", "password": "pass"})
assert response.status_code == 200
json_data = response.json()
assert "access_token" in json_data
assert "token_type" in json_data
assert json_data["token_type"] == "bearer"
Здесь мы:
- Отправляем POST-запрос на
/token. - Проверяем, что возвращается статус 200.
- Убеждаемся, что в ответе есть
access_tokenиtoken_type.
Тестирование валидности токена
После получения токена нужно убедиться, что он работает.
Мы отправим запрос к защищённому эндпоинту /protected с токеном в заголовке.
def test_valid_token_access():
# Получаем токен
response = client.post("/token", data={"username": "user", "password": "pass"})
token = response.json().get("access_token")
# Используем токен для доступа к защищённому маршруту
headers = {"Authorization": f"Bearer {token}"}
protected_response = client.get("/protected", headers=headers)
assert protected_response.status_code == 200
assert protected_response.json() == {"message": "You have access!"}
Здесь мы:
- Логинимся и получаем токен.
- Используем его в заголовке
Authorization. - Убеждаемся, что доступ к
/protectedразрешён и возвращает правильный ответ.
Тестирование недействительных токенов
Теперь проверим, что API корректно обрабатывает случаи с отсутствием или поддельным токеном.
1. Без токена:
def test_no_token_access():
response = client.get("/protected")
assert response.status_code == 401
assert response.json() == {"detail": "Not authenticated"}
Здесь мы посылаем запрос без токена. Ожидаем статус 401 и сообщение об ошибке.
2. С поддельным токеном:
def test_invalid_token_access():
headers = {"Authorization": "Bearer invalidtoken"}
response = client.get("/protected", headers=headers)
assert response.status_code == 401
assert response.json() == {"detail": "Invalid token"}
В этом тесте мы используем поддельный токен и проверяем, что доступ запрещён.
Тестирование истечения срока действия токена
JWT имеет срок действия, после которого он становится недействительным.
Давайте протестируем этот случай.
Для этого нужно изменить срок действия токена на что-то короткое (например, 1 секунду).
Обновляем функцию create_access_token:
ACCESS_TOKEN_EXPIRE_MINUTES = 0.001 # 1 секунда
Теперь тест:
import time
def test_expired_token_access():
response = client.post("/token", data={"username": "user", "password": "pass"})
token = response.json().get("access_token")
# Ждём истечения срока действия токена
time.sleep(2)
headers = {"Authorization": f"Bearer {token}"}
response = client.get("/protected", headers=headers)
assert response.status_code == 401
assert response.json() == {"detail": "Invalid token"}
Мы:
- Генерируем токен.
- Ждём истечения его срока действия.
- Проверяем, что доступ запрещён.
Общая структура тестов
Вот как выглядит структура всех тестов:
tests/
├── test_authentication.py
Файл test_authentication.py содержит тесты для:
- Генерации токена.
- Доступа с валидным токеном.
- Доступа без токена.
- Доступа с поддельным токеном.
- Истечения срока действия токена.
Заключительные мысли на сегодня
Как видите, тестирование аутентификации через JWT включает много полезных сценариев, которые легко автоматизировать с помощью Pytest. Эти тесты не только подтверждают корректность вашей реализации, но и дают уверенность в безопасности приложения. Теперь вы готовы защищать свои API, как настоящий серверный супергерой!
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ