chore(release): v0.921 — Rustproof (Rust test coverage >30%)
Some checks failed
Stream Server CI / test (push) Failing after 0s

This commit is contained in:
senke 2026-03-02 12:28:20 +01:00
parent 72d40990c5
commit 12dbb5bbe4
8 changed files with 386 additions and 3 deletions

View file

@ -1 +1 @@
0.923
0.921

View file

@ -429,7 +429,7 @@ pub fn require_role(
}
}
fn extract_token_from_headers(headers: &HeaderMap) -> Option<String> {
pub(crate) fn extract_token_from_headers(headers: &HeaderMap) -> Option<String> {
let auth_header = headers.get("Authorization")?;
let auth_str = auth_header.to_str().ok()?;
@ -656,4 +656,90 @@ mod tests {
let err = AuthError::ConfigurationError("missing config".to_string());
assert!(format!("{}", err).contains("missing config"));
}
#[test]
fn test_has_permission() {
let config = Arc::new(Config::default());
let manager = AuthManager::new(config).unwrap();
let claims = Claims {
sub: "user-1".to_string(),
username: "test".to_string(),
email: None,
roles: vec![Role::User],
permissions: vec![Permission::StreamAudio, Permission::UploadAudio],
exp: 9999999999,
iat: 0,
iss: "test".to_string(),
aud: "test".to_string(),
session_id: "sess-1".to_string(),
};
assert!(manager.has_permission(&claims, Permission::StreamAudio));
assert!(!manager.has_permission(&claims, Permission::SystemAdmin));
}
#[test]
fn test_has_role() {
let config = Arc::new(Config::default());
let manager = AuthManager::new(config).unwrap();
let claims = Claims {
sub: "user-1".to_string(),
username: "admin".to_string(),
email: None,
roles: vec![Role::Admin, Role::User],
permissions: vec![],
exp: 9999999999,
iat: 0,
iss: "test".to_string(),
aud: "test".to_string(),
session_id: "sess-1".to_string(),
};
assert!(manager.has_role(&claims, Role::Admin));
assert!(!manager.has_role(&claims, Role::Moderator));
}
#[test]
fn test_has_any_role() {
let config = Arc::new(Config::default());
let manager = AuthManager::new(config).unwrap();
let claims = Claims {
sub: "user-1".to_string(),
username: "artist".to_string(),
email: None,
roles: vec![Role::Artist],
permissions: vec![],
exp: 9999999999,
iat: 0,
iss: "test".to_string(),
aud: "test".to_string(),
session_id: "sess-1".to_string(),
};
assert!(manager.has_any_role(&claims, &[Role::Admin, Role::Artist]));
assert!(!manager.has_any_role(&claims, &[Role::Admin, Role::Moderator]));
}
#[test]
fn test_extract_token_from_headers() {
let mut headers = HeaderMap::new();
headers.insert(
"Authorization",
HeaderValue::from_static("Bearer token123"),
);
let token = extract_token_from_headers(&headers);
assert_eq!(token.as_deref(), Some("token123"));
}
#[test]
fn test_extract_token_from_headers_no_bearer() {
let mut headers = HeaderMap::new();
headers.insert("Authorization", HeaderValue::from_static("Basic xyz"));
let token = extract_token_from_headers(&headers);
assert!(token.is_none());
}
#[test]
fn test_extract_token_from_headers_missing() {
let headers = HeaderMap::new();
let token = extract_token_from_headers(&headers);
assert!(token.is_none());
}
}

View file

@ -123,3 +123,24 @@ impl SessionRevocationStore for RedisRevocationStore {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_in_memory_revoke_and_check() {
let store = InMemoryRevocationStore::new();
assert!(!store.is_revoked("sess-1").await);
store.revoke("sess-1").await.unwrap();
assert!(store.is_revoked("sess-1").await);
}
#[tokio::test]
async fn test_in_memory_different_sessions() {
let store = InMemoryRevocationStore::new();
store.revoke("sess-a").await.unwrap();
assert!(store.is_revoked("sess-a").await);
assert!(!store.is_revoked("sess-b").await);
}
}

View file

@ -473,4 +473,90 @@ mod tests {
assert_eq!(token_info.track_id, "test_track_123");
assert_eq!(token_info.access_count, 0);
}
#[test]
fn test_generate_secure_url() {
let validator = TokenValidator::new(SignatureConfig {
secret_key: "test_secret".to_string(),
default_ttl: Duration::from_secs(3600),
max_ttl: Duration::from_secs(86400),
});
let url = validator
.generate_secure_url("track-456", None, Some(Uuid::new_v4()))
.unwrap();
assert_eq!(url.track_id, "track-456");
assert!(url.expires > 0);
assert!(!url.signature.is_empty());
assert!(url.user_id.is_some());
}
#[test]
fn test_secure_stream_url_build_url() {
let validator = TokenValidator::new(SignatureConfig {
secret_key: "test_secret".to_string(),
default_ttl: Duration::from_secs(3600),
max_ttl: Duration::from_secs(86400),
});
let url = validator
.generate_secure_url("track-789", None, None)
.unwrap();
let full_url = url.build_url("https://stream.example.com");
assert!(full_url.starts_with("https://stream.example.com/stream/track-789"));
assert!(full_url.contains("expires="));
assert!(full_url.contains("sig="));
}
#[test]
fn test_validate_signature_invalid_hex() {
let validator = TokenValidator::new(SignatureConfig {
secret_key: "test_secret".to_string(),
default_ttl: Duration::from_secs(3600),
max_ttl: Duration::from_secs(86400),
});
let expires = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600;
let result = validator.validate_signature(
"track",
expires,
"not-valid-hex!!",
None,
);
assert!(result.is_ok());
assert!(!result.unwrap());
}
#[tokio::test]
async fn test_get_token_stats() {
let validator = TokenValidator::new(SignatureConfig {
secret_key: "test_secret".to_string(),
default_ttl: Duration::from_secs(3600),
max_ttl: Duration::from_secs(86400),
});
let stats = validator.get_token_stats().await.unwrap();
assert_eq!(stats.total_tokens, 0);
assert_eq!(stats.active_last_hour, 0);
assert_eq!(stats.total_accesses, 0);
let expires = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600;
let request = StreamRequest {
track_id: "stats_test".to_string(),
expires,
sig: validator.generate_signature("stats_test", expires, None).unwrap(),
user_id: None,
};
validator.validate_and_register_token(&request).await.unwrap();
let stats2 = validator.get_token_stats().await.unwrap();
assert_eq!(stats2.total_tokens, 1);
}
}

View file

@ -780,4 +780,31 @@ mod tests {
assert!(!config.url.is_empty());
assert!(config.max_retries >= 1);
}
#[test]
fn test_config_validate_weak_secret() {
let mut config = Config::default();
config.secret_key = "short".to_string();
let result = config.validate();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ConfigError::WeakSecretKey));
}
#[test]
fn test_config_validate_invalid_port() {
let mut config = Config::default();
config.port = 0;
let result = config.validate();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ConfigError::InvalidPort));
}
#[test]
fn test_config_validate_empty_audio_dir() {
let mut config = Config::default();
config.audio_dir = "".to_string();
let result = config.validate();
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), ConfigError::InvalidAudioDir));
}
}

View file

@ -687,4 +687,97 @@ mod tests {
_ => panic!("Expected SerializationError variant"),
}
}
#[test]
fn test_app_error_config_display() {
let err = AppError::ConfigError {
message: "bad config".to_string(),
};
assert!(format!("{}", err).contains("bad config"));
}
#[test]
fn test_app_error_network_display() {
let err = AppError::NetworkError {
message: "timeout".to_string(),
};
assert!(format!("{}", err).contains("timeout"));
}
#[test]
fn test_app_error_unauthorized_into_response() {
let err = AppError::Unauthorized;
let response = err.into_response();
assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
}
#[test]
fn test_app_error_forbidden_into_response() {
let err = AppError::Forbidden;
let response = err.into_response();
assert_eq!(response.status(), StatusCode::FORBIDDEN);
}
#[test]
fn test_app_error_not_found_into_response() {
let err = AppError::NotFound {
resource: "track".to_string(),
};
let response = err.into_response();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn test_app_error_bad_request_variants() {
let err1 = AppError::InvalidData {
message: "invalid".to_string(),
};
assert_eq!(err1.into_response().status(), StatusCode::BAD_REQUEST);
let err2 = AppError::UnsupportedCodec {
codec: "xyz".to_string(),
};
assert_eq!(err2.into_response().status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_app_error_too_many_requests() {
let err = AppError::LimitExceeded {
limit: "streams".to_string(),
current: 100,
};
assert_eq!(err.into_response().status(), StatusCode::TOO_MANY_REQUESTS);
}
#[test]
fn test_app_error_conflict() {
let err = AppError::AlreadyRunning;
assert_eq!(err.into_response().status(), StatusCode::CONFLICT);
}
#[test]
fn test_sqlx_error_conversion() {
let pool_err = sqlx::Error::PoolTimedOut;
let app_err: AppError = pool_err.into();
match app_err {
AppError::ConnectionTimeout => {}
_ => panic!("Expected ConnectionTimeout"),
}
}
#[tokio::test]
async fn test_tokio_elapsed_conversion() {
use tokio::time::Duration;
let elapsed_err = tokio::time::timeout(
Duration::from_nanos(0),
async { futures::future::pending::<()>().await },
)
.await
.unwrap_err();
let app_err: AppError = elapsed_err.into();
match app_err {
AppError::ConnectionTimeout => {}
_ => panic!("Expected ConnectionTimeout"),
}
}
}

View file

@ -49,7 +49,7 @@ pub async fn rate_limit_middleware(
Ok(response)
}
fn extract_client_ip(headers: &HeaderMap) -> String {
pub(crate) fn extract_client_ip(headers: &HeaderMap) -> String {
if let Some(forwarded_for) = headers.get("x-forwarded-for") {
if let Ok(forwarded_str) = forwarded_for.to_str() {
if let Some(first_ip) = forwarded_str.split(',').next() {
@ -79,3 +79,40 @@ fn check_rate_limit(_state: &AppState, client_ip: &str) -> bool {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::HeaderValue;
#[test]
fn test_extract_client_ip_x_forwarded_for() {
let mut headers = HeaderMap::new();
headers.insert(
"x-forwarded-for",
HeaderValue::from_static("10.0.0.1, 192.168.1.1"),
);
assert_eq!(extract_client_ip(&headers), "10.0.0.1");
}
#[test]
fn test_extract_client_ip_x_real_ip() {
let mut headers = HeaderMap::new();
headers.insert("x-real-ip", HeaderValue::from_static("203.0.113.50"));
assert_eq!(extract_client_ip(&headers), "203.0.113.50");
}
#[test]
fn test_extract_client_ip_fallback_unknown() {
let headers = HeaderMap::new();
assert_eq!(extract_client_ip(&headers), "unknown");
}
#[test]
fn test_extract_client_ip_forwarded_takes_precedence() {
let mut headers = HeaderMap::new();
headers.insert("x-forwarded-for", HeaderValue::from_static("1.2.3.4"));
headers.insert("x-real-ip", HeaderValue::from_static("5.6.7.8"));
assert_eq!(extract_client_ip(&headers), "1.2.3.4");
}
}

View file

@ -993,4 +993,37 @@ mod tests {
assert!(json.is_ok(), "Failed to serialize {:?}", level);
}
}
#[test]
fn test_websocket_event_roundtrip() {
let event = WebSocketEvent::ServerMessage {
message: "test".to_string(),
level: MessageLevel::Info,
};
let json = serde_json::to_string(&event).unwrap();
let restored: WebSocketEvent = serde_json::from_str(&json).unwrap();
match (&event, &restored) {
(
WebSocketEvent::ServerMessage { message: m1, level: l1 },
WebSocketEvent::ServerMessage { message: m2, level: l2 },
) => {
assert_eq!(m1, m2);
assert!(std::mem::discriminant(l1) == std::mem::discriminant(l2));
}
_ => panic!("Unexpected variant"),
}
}
#[test]
fn test_live_track_stats_serialize() {
let stats = LiveTrackStats {
track_id: "t1".to_string(),
title: "Song".to_string(),
artist: "Artist".to_string(),
current_listeners: 42,
};
let json = serde_json::to_string(&stats).unwrap();
assert!(json.contains("t1"));
assert!(json.contains("Song"));
}
}