Some checks failed
Deploy quoter Microservice on push / deploy (push) Failing after 36m41s
### 🔒 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 начался до истечения, закончился после
400 lines
14 KiB
Rust
400 lines
14 KiB
Rust
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());
|
||
}
|
||
}
|