Files
quoter/tests/auth_integration_test.rs
Untone 86ad1f1695
Some checks failed
Deploy quoter Microservice on push / deploy (push) Failing after 36m41s
[0.6.10] - 2025-10-04
### 🔒 FIX: JWT Token Grace Period
- ** Добавлен grace period для истекших токенов**: 60 секунд
  - Изменена логика проверки JWT `exp` в `auth.rs`
  - Токены принимаются в течение 60 секунд после истечения
  - Это даёт клиенту время автоматически обновить токен через `refreshToken()`
  - Логирование разделено: `info` для grace period, `warn` для полного истечения
  - Решает проблему "Invalid or expired token" при параллельных запросах
  - Формула: `if exp + 60 < current_time` → reject, иначе accept
  - Предотвращает race condition: upload начался до истечения, закончился после
2025-10-05 09:12:53 +03:00

400 lines
14 KiB
Rust
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use actix_web::test;
use quoter::auth::{
Author, authenticate_request, extract_token_from_request, secure_token_validation,
};
use std::time::Duration;
/// Тест извлечения токена из различных источников
#[test]
async fn test_extract_token_from_request() {
// Тест Bearer токена в Authorization header
let req = test::TestRequest::default()
.insert_header(("authorization", "Bearer test-jwt-token-123"))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, Some("test-jwt-token-123".to_string()));
// Тест кастомного заголовка X-Session-Token
let req = test::TestRequest::default()
.insert_header(("x-session-token", "custom-session-token-456"))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, Some("custom-session-token-456".to_string()));
// Тест Cookie
let req = test::TestRequest::default()
.insert_header(("cookie", "session_token=cookie-token-789; other=value"))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, Some("cookie-token-789".to_string()));
// Тест отсутствия токена
let req = test::TestRequest::default().to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, None);
}
/// Тест валидации формата токена
#[test]
async fn test_token_format_validation() {
// Тест пустого токена
let result = secure_token_validation("", None, Duration::from_secs(5)).await;
assert!(result.is_err());
// Тест слишком короткого токена
let result = secure_token_validation("short", None, Duration::from_secs(5)).await;
assert!(result.is_err());
// Тест невалидного JWT (не 3 части)
let result = secure_token_validation("invalid.jwt", None, Duration::from_secs(5)).await;
assert!(result.is_err());
}
/// Тест создания валидного JWT токена для тестов
fn create_test_jwt_token(user_id: &str, username: Option<&str>) -> String {
use jsonwebtoken::{EncodingKey, Header, encode};
use serde::Serialize;
#[derive(Serialize)]
struct Claims {
user_id: String,
username: Option<String>,
exp: usize,
iat: usize,
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
let claims = Claims {
user_id: user_id.to_string(),
username: username.map(|s| s.to_string()),
exp: now + 3600, // Истекает через час
iat: now,
};
let secret = std::env::var("JWT_SECRET_KEY")
.or_else(|_| std::env::var("JWT_SECRET_KEY"))
.unwrap_or_else(|_| "your-secret-key".to_string());
let key = EncodingKey::from_secret(secret.as_ref());
encode(&Header::default(), &claims, &key).unwrap()
}
/// Тест валидации валидного JWT токена без Redis
#[tokio::test]
async fn test_valid_jwt_token_without_redis() {
let token = create_test_jwt_token("test-user-123", Some("testuser"));
let result = secure_token_validation(&token, None, Duration::from_secs(5)).await;
assert!(result.is_ok());
let author = result.unwrap();
assert_eq!(author.user_id, "test-user-123");
assert_eq!(author.username, Some("testuser".to_string()));
assert_eq!(author.token_type, Some("jwt".to_string()));
assert_eq!(author.auth_data, Some("jwt_only".to_string()));
}
/// Тест валидации истекшего JWT токена
#[tokio::test]
async fn test_expired_jwt_token() {
use jsonwebtoken::{EncodingKey, Header, encode};
use serde::Serialize;
#[derive(Serialize)]
struct Claims {
user_id: String,
username: Option<String>,
exp: usize,
iat: usize,
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
let claims = Claims {
user_id: "test-user-123".to_string(),
username: Some("testuser".to_string()),
exp: now - 3600, // Истек час назад
iat: now - 7200, // Создан 2 часа назад
};
let secret = std::env::var("JWT_SECRET_KEY")
.or_else(|_| std::env::var("JWT_SECRET_KEY"))
.unwrap_or_else(|_| "your-secret-key".to_string());
let key = EncodingKey::from_secret(secret.as_ref());
let token = encode(&Header::default(), &claims, &key).unwrap();
let result = secure_token_validation(&token, None, Duration::from_secs(5)).await;
assert!(result.is_err());
}
/// Тест универсальной функции аутентификации
#[tokio::test]
async fn test_authenticate_request() {
let token = create_test_jwt_token("test-user-456", Some("anotheruser"));
// Тест с Bearer токеном
let req = test::TestRequest::default()
.insert_header(("authorization", format!("Bearer {}", token)))
.to_http_request();
let result = authenticate_request(&req, None, Duration::from_secs(5)).await;
assert!(result.is_ok());
let author = result.unwrap();
assert_eq!(author.user_id, "test-user-456");
assert_eq!(author.username, Some("anotheruser".to_string()));
// Тест без токена
let req = test::TestRequest::default().to_http_request();
let result = authenticate_request(&req, None, Duration::from_secs(5)).await;
assert!(result.is_err());
}
/// Тест производительности аутентификации
#[tokio::test]
async fn test_authentication_performance() {
use std::time::Instant;
let token = create_test_jwt_token("perf-user", Some("perfuser"));
let iterations = 1000;
let start = Instant::now();
for _ in 0..iterations {
let req = test::TestRequest::default()
.insert_header(("authorization", format!("Bearer {}", token)))
.to_http_request();
let result = authenticate_request(&req, None, Duration::from_secs(1)).await;
assert!(result.is_ok());
}
let duration = start.elapsed();
let avg_time = duration.as_micros() as f64 / iterations as f64;
println!(
"Authentication performance: {} operations in {:?}, avg: {:.2} μs per auth",
iterations, duration, avg_time
);
// Проверяем, что аутентификация достаточно быстрая (< 1ms на операцию)
assert!(
avg_time < 1000.0,
"Authentication too slow: {:.2} μs per operation",
avg_time
);
}
/// Тест обработки различных заголовков
#[test]
async fn test_header_variations() {
// Тест с пробелами в Bearer токене
let req = test::TestRequest::default()
.insert_header(("authorization", "Bearer token-with-spaces "))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, Some("token-with-spaces".to_string()));
// Тест с Authorization без Bearer
let req = test::TestRequest::default()
.insert_header(("authorization", "Basic dGVzdDp0ZXN0"))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, None);
// Тест с несколькими cookies
let req = test::TestRequest::default()
.insert_header((
"cookie",
"first=value1; session_token=my-token; last=value2",
))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, Some("my-token".to_string()));
}
/// Тест граничных случаев
#[tokio::test]
async fn test_edge_cases() {
// Тест с очень длинным токеном
let long_token = "a".repeat(5000);
let result = secure_token_validation(&long_token, None, Duration::from_secs(1)).await;
assert!(result.is_err()); // Должен быть невалидным JWT
// Тест с нулевым таймаутом
let token = create_test_jwt_token("timeout-user", None);
let result = secure_token_validation(&token, None, Duration::from_secs(0)).await;
// Должен работать, так как JWT валидация не требует Redis
assert!(result.is_ok());
// Тест с очень большим таймаутом
let result = secure_token_validation(&token, None, Duration::from_secs(3600)).await;
assert!(result.is_ok());
}
/// Тест сериализации Author структуры
#[test]
async fn test_author_serialization() {
let author = Author {
user_id: "test-123".to_string(),
username: Some("testuser".to_string()),
token_type: Some("jwt".to_string()),
created_at: Some("1640995200".to_string()),
last_activity: Some("1640995260".to_string()),
auth_data: Some("jwt_only".to_string()),
device_info: None,
};
// Тест JSON сериализации
let json = serde_json::to_string(&author).unwrap();
assert!(json.contains("test-123"));
assert!(json.contains("testuser"));
assert!(json.contains("jwt"));
// Тест десериализации
let deserialized: Author = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_id, author.user_id);
assert_eq!(deserialized.username, author.username);
assert_eq!(deserialized.token_type, author.token_type);
}
/// Тест безопасности токенов
#[test]
async fn test_token_security() {
// Тест с подозрительными символами
let suspicious_tokens = vec![
"'; DROP TABLE users; --",
"<script>alert('xss')</script>",
"../../../etc/passwd",
"\\x00\\x01\\x02",
"SELECT * FROM users WHERE id = 1",
];
for suspicious_token in suspicious_tokens {
let req = test::TestRequest::default()
.insert_header(("authorization", format!("Bearer {}", suspicious_token)))
.to_http_request();
let token = extract_token_from_request(&req);
assert_eq!(token, Some(suspicious_token.to_string()));
// Токен должен быть отклонен при валидации
let result = secure_token_validation(suspicious_token, None, Duration::from_secs(1)).await;
assert!(
result.is_err(),
"Suspicious token should be rejected: {}",
suspicious_token
);
}
}
/// Интеграционный тест с мокированным Redis
#[tokio::test]
async fn test_integration_with_mock_redis() {
// Этот тест демонстрирует, как можно тестировать с мокированным Redis
// В реальном проекте здесь был бы мок Redis соединения
let token = create_test_jwt_token("integration-user", Some("integrationuser"));
let req = test::TestRequest::default()
.insert_header(("authorization", format!("Bearer {}", token)))
.to_http_request();
// Тестируем без Redis (None)
let result = authenticate_request(&req, None, Duration::from_secs(5)).await;
assert!(result.is_ok());
let author = result.unwrap();
assert_eq!(author.user_id, "integration-user");
assert_eq!(author.username, Some("integrationuser".to_string()));
assert_eq!(author.auth_data, Some("jwt_only".to_string()));
}
/// Тест обработки ошибок аутентификации
#[tokio::test]
async fn test_authentication_error_handling() {
// Тест с невалидным JWT
let req = test::TestRequest::default()
.insert_header(("authorization", "Bearer invalid.jwt.token"))
.to_http_request();
let result = authenticate_request(&req, None, Duration::from_secs(5)).await;
assert!(result.is_err());
// Проверяем, что ошибка имеет правильный тип
match result {
Err(e) => {
let response = e.error_response();
assert_eq!(response.status(), actix_web::http::StatusCode::UNAUTHORIZED);
}
Ok(_) => panic!("Expected error, got success"),
}
}
/// Тест множественных токенов в одном запросе
#[test]
async fn test_multiple_token_sources() {
// Если есть несколько источников токенов, должен использоваться первый найденный
let req = test::TestRequest::default()
.insert_header(("authorization", "Bearer bearer-token"))
.insert_header(("x-session-token", "session-token"))
.insert_header(("cookie", "session_token=cookie-token"))
.to_http_request();
let token = extract_token_from_request(&req);
// Должен вернуть Bearer токен (приоритет)
assert_eq!(token, Some("bearer-token".to_string()));
}
/// Тест валидации с различными алгоритмами JWT
#[tokio::test]
async fn test_jwt_algorithm_validation() {
// Тест создания токена с неправильным алгоритмом
use jsonwebtoken::{Algorithm, EncodingKey, Header, encode};
use serde::Serialize;
#[derive(Serialize)]
struct Claims {
user_id: String,
exp: usize,
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs() as usize;
let claims = Claims {
user_id: "test-user".to_string(),
exp: now + 3600,
};
// Создаем токен с RS256 вместо HS256
let header = Header {
alg: Algorithm::RS256,
..Default::default()
};
let secret = "wrong-secret";
let key = EncodingKey::from_secret(secret.as_ref());
// Этот токен должен быть отклонен, так как мы используем неправильный алгоритм
if let Ok(token) = encode(&header, &claims, &key) {
let result = secure_token_validation(&token, None, Duration::from_secs(5)).await;
assert!(result.is_err());
}
}