terrazzo-terminal 0.2.7

A simple web-based terminal emulator built on Terrazzo.
#![cfg(test)]

use std::time::Duration;
use std::time::SystemTime;

use jsonwebtoken::Header;
use terrazzo::axum::body::Body;
use terrazzo::axum::body::to_bytes;
use terrazzo::axum::response::IntoResponse;
use terrazzo::http;
use terrazzo::http::Request;
use terrazzo::http::Response;
use terrazzo::http::StatusCode;
use terrazzo::http::header::AUTHORIZATION;

use super::AuthConfig;
use super::Claims;
use super::jwt_timestamp::Timestamp;

#[tokio::test]
async fn missing_authorization_header() {
    let auth_config = AuthConfig::random();
    let request = make_request(|b| b);
    let response = auth_config
        .validate(request.headers())
        .unwrap_err()
        .into_response();
    assert_eq!(StatusCode::UNAUTHORIZED, response.status());
    assert_eq!("Missing access token", get_body(response).await.unwrap());
}

#[tokio::test]
async fn missing_bearer_token() {
    let auth_config = AuthConfig::random();
    let request = make_request(|b| b.header(AUTHORIZATION, "blabla"));
    let response = auth_config
        .validate(request.headers())
        .unwrap_err()
        .into_response();
    assert_eq!(StatusCode::UNAUTHORIZED, response.status());
    assert_eq!(
        "The 'authorization' header does not contain a bearer token",
        get_body(response).await.unwrap()
    );
}

#[tokio::test]
async fn invalid_bearer_token() {
    let auth_config = AuthConfig::random();
    let request = make_request(|b| b.header(AUTHORIZATION, "Bearer blabla"));
    let response = auth_config
        .validate(request.headers())
        .unwrap_err()
        .into_response();
    assert_eq!(StatusCode::UNAUTHORIZED, response.status());
    assert_eq!("InvalidToken", get_body(response).await.unwrap());
}

#[tokio::test]
async fn valid_token() {
    let auth_config = AuthConfig::random();
    let token = jsonwebtoken::encode(
        &Header::default(),
        &Claims {
            nbf: Duration::from_secs(60),
            exp: Duration::from_secs(3600),
        }
        .into_timestamps(),
        &auth_config.encoding_key,
    )
    .unwrap();

    let request = make_request(|b| b.header(AUTHORIZATION, format!("Bearer {token}")));
    let _token_data: jsonwebtoken::TokenData<Claims> =
        auth_config.validate(request.headers()).unwrap();
}

#[tokio::test]
async fn early_token() {
    let auth_config = AuthConfig::random();
    let now = SystemTime::now();
    let token = jsonwebtoken::encode(
        &Header::default(),
        &Claims {
            nbf: Timestamp::from(now + Duration::from_secs(60)),
            exp: Timestamp::from(now + Duration::from_secs(3600)),
        },
        &auth_config.encoding_key,
    )
    .unwrap();

    let request = make_request(|b| b.header(AUTHORIZATION, format!("Bearer {token}")));
    let response = auth_config
        .validate(request.headers())
        .unwrap_err()
        .into_response();
    assert_eq!(StatusCode::UNAUTHORIZED, response.status());
    assert_eq!("ImmatureSignature", get_body(response).await.unwrap());
}

#[tokio::test]
async fn expired_token() {
    let auth_config = AuthConfig::random();
    let now = SystemTime::now();
    let token = jsonwebtoken::encode(
        &Header::default(),
        &Claims {
            nbf: Timestamp::from(now - Duration::from_secs(3600)),
            exp: Timestamp::from(now - Duration::from_secs(60)),
        },
        &auth_config.encoding_key,
    )
    .unwrap();

    let request = make_request(|b| b.header(AUTHORIZATION, format!("Bearer {token}")));
    let response = auth_config
        .validate(request.headers())
        .unwrap_err()
        .into_response();
    assert_eq!(StatusCode::UNAUTHORIZED, response.status());
    assert_eq!("ExpiredSignature", get_body(response).await.unwrap());
}

#[tokio::test]
async fn bad_signature_token() {
    let auth_config = AuthConfig::random();
    let auth_config2 = AuthConfig::random();
    let now = SystemTime::now();
    let token = jsonwebtoken::encode(
        &Header::default(),
        &Claims {
            nbf: Timestamp::from(now - Duration::from_secs(3600)),
            exp: Timestamp::from(now - Duration::from_secs(60)),
        },
        &auth_config2.encoding_key,
    )
    .unwrap();

    let request = make_request(|b| b.header(AUTHORIZATION, format!("Bearer {token}")));
    let response = auth_config
        .validate(request.headers())
        .unwrap_err()
        .into_response();
    assert_eq!(StatusCode::UNAUTHORIZED, response.status());
    assert_eq!("InvalidSignature", get_body(response).await.unwrap());
}

fn make_request(f: impl FnOnce(http::request::Builder) -> http::request::Builder) -> Request<Body> {
    f(Request::builder()
        .method("GET")
        .uri("http://localhost/authenticated"))
    .body(Body::empty())
    .unwrap()
}

async fn get_body(response: Response<Body>) -> Result<String, Box<dyn std::error::Error>> {
    let bytes = to_bytes(response.into_body(), 1024).await?;
    Ok(String::from_utf8(bytes.to_vec())?)
}