test(web-gateway): add auth middleware and WebSocket token validation tests

- 6 auth middleware tests: missing header, malformed Bearer, invalid JWT,
  wrong secret, expired token, valid token passthrough
- 4 WebSocket token validation tests: valid token, invalid token,
  wrong secret, expired token
- Web-gateway now has 36 tests (up from 26)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-02-22 07:32:37 +01:00
parent 9630d915b4
commit af2a17a827
2 changed files with 213 additions and 0 deletions

View File

@@ -33,3 +33,145 @@ pub async fn auth_middleware(
request.extensions_mut().insert(token_data.claims);
Ok(next.run(request).await)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request as HttpRequest, StatusCode};
use axum::middleware;
use axum::response::IntoResponse;
use axum::routing::get;
use axum::Router;
use jsonwebtoken::{encode, EncodingKey, Header};
use tower::ServiceExt;
use crate::auth::claims::Claims;
fn test_config(secret: &str) -> Arc<GatewayConfig> {
Arc::new(GatewayConfig {
jwt_secret: secret.to_string(),
..GatewayConfig::default()
})
}
fn make_token(secret: &str, claims: &Claims) -> String {
let key = EncodingKey::from_secret(secret.as_bytes());
encode(&Header::default(), claims, &key).unwrap()
}
fn valid_claims() -> Claims {
Claims {
sub: "user-1".into(),
exp: u64::MAX,
iat: 1_700_000_000,
jti: "t1".into(),
roles: vec!["trader".into()],
permissions: vec!["read:data".into()],
}
}
fn test_app(config: Arc<GatewayConfig>) -> Router {
let handler = get(|| async { "ok" });
Router::new()
.route("/protected", handler)
.layer(middleware::from_fn_with_state(config.clone(), auth_middleware))
.with_state(config)
}
#[tokio::test]
async fn test_missing_auth_header_returns_401() {
let config = test_config("secret");
let app = test_app(config);
let req = HttpRequest::builder()
.uri("/protected")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_malformed_bearer_prefix_returns_401() {
let config = test_config("secret");
let app = test_app(config);
let req = HttpRequest::builder()
.uri("/protected")
.header("Authorization", "Token abc123")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_invalid_jwt_returns_401() {
let config = test_config("secret");
let app = test_app(config);
let req = HttpRequest::builder()
.uri("/protected")
.header("Authorization", "Bearer not.a.valid.jwt")
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_wrong_secret_returns_401() {
let config = test_config("correct-secret");
let app = test_app(config);
let token = make_token("wrong-secret", &valid_claims());
let req = HttpRequest::builder()
.uri("/protected")
.header("Authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_expired_token_returns_401() {
let config = test_config("secret");
let app = test_app(config);
let mut claims = valid_claims();
claims.exp = 1; // Expired in 1970
let token = make_token("secret", &claims);
let req = HttpRequest::builder()
.uri("/protected")
.header("Authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
}
#[tokio::test]
async fn test_valid_token_passes_through() {
let config = test_config("secret");
let app = test_app(config);
let token = make_token("secret", &valid_claims());
let req = HttpRequest::builder()
.uri("/protected")
.header("Authorization", format!("Bearer {}", token))
.body(Body::empty())
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
}

View File

@@ -126,3 +126,74 @@ async fn handle_ws_connection(socket: WebSocket, state: AppState) {
tracing::debug!("WebSocket connection closed");
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
use jsonwebtoken::{encode, EncodingKey, Header};
use crate::auth::claims::Claims;
fn test_config(secret: &str) -> Arc<GatewayConfig> {
Arc::new(GatewayConfig {
jwt_secret: secret.to_string(),
..GatewayConfig::default()
})
}
fn make_token(secret: &str, claims: &Claims) -> String {
let key = EncodingKey::from_secret(secret.as_bytes());
encode(&Header::default(), claims, &key).unwrap()
}
fn valid_claims() -> Claims {
Claims {
sub: "user-1".into(),
exp: u64::MAX,
iat: 1_700_000_000,
jti: "t1".into(),
roles: vec!["trader".into()],
permissions: vec!["read:data".into()],
}
}
#[test]
fn test_valid_ws_token_returns_claims() {
let config = test_config("ws-secret");
let claims = valid_claims();
let token = make_token("ws-secret", &claims);
let result = validate_ws_token(&token, &config);
assert!(result.is_ok());
let extracted = result.unwrap();
assert_eq!(extracted.sub, "user-1");
assert_eq!(extracted.roles, vec!["trader"]);
}
#[test]
fn test_invalid_ws_token_returns_unauthorized() {
let config = test_config("ws-secret");
let result = validate_ws_token("not.a.jwt", &config);
assert!(result.is_err());
}
#[test]
fn test_wrong_secret_ws_token_returns_unauthorized() {
let config = test_config("correct-secret");
let token = make_token("wrong-secret", &valid_claims());
let result = validate_ws_token(&token, &config);
assert!(result.is_err());
}
#[test]
fn test_expired_ws_token_returns_unauthorized() {
let config = test_config("ws-secret");
let mut claims = valid_claims();
claims.exp = 1; // Expired in 1970
let token = make_token("ws-secret", &claims);
let result = validate_ws_token(&token, &config);
assert!(result.is_err());
}
}