openauth-plugins 0.0.4

Official OpenAuth plugin modules.
Documentation
use super::*;

use std::sync::atomic::{AtomicBool, Ordering};

#[tokio::test]
async fn device_code_route_generates_codes_and_persists_record(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(
        adapter.clone(),
        DeviceAuthorizationOptions::new()
            .expires_in(Duration::minutes(5))
            .interval(Duration::seconds(2)),
    )?;

    let response = create_device_code(&router, "test-client", Some("read write")).await?;

    assert!(string_field(&response, "device_code").len() >= 40);
    assert_eq!(string_field(&response, "user_code").len(), 8);
    assert_eq!(response["expires_in"], 300);
    assert_eq!(response["interval"], 2);
    assert!(string_field(&response, "verification_uri").contains("/device"));
    assert!(
        string_field(&response, "verification_uri_complete").contains(&format!(
            "user_code={}",
            string_field(&response, "user_code")
        ))
    );

    let record = device_record(&adapter)
        .await
        .ok_or("missing device record")?;
    assert_eq!(
        record.get("clientId"),
        Some(&DbValue::String("test-client".to_owned()))
    );
    assert_eq!(
        record.get("scope"),
        Some(&DbValue::String("read write".to_owned()))
    );
    assert_eq!(record.get("pollingInterval"), Some(&DbValue::Number(2000)));
    Ok(())
}

#[tokio::test]
async fn device_code_route_rejects_invalid_client() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(
        adapter,
        DeviceAuthorizationOptions::new()
            .validate_client(|client_id| async move { Ok(client_id == "valid-client") }),
    )?;

    let response = router
        .handle_async(json_request(
            Method::POST,
            "/api/auth/device/code",
            r#"{"client_id":"invalid-client"}"#,
            None,
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::BAD_REQUEST);
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["error"], "invalid_client");
    assert_eq!(body["error_description"], "Invalid client ID");
    Ok(())
}

#[tokio::test]
async fn device_code_route_uses_custom_generators() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(
        adapter,
        DeviceAuthorizationOptions::new()
            .generate_device_code(|| "custom-device-code".to_owned())
            .generate_user_code(|| "CUSTOM12".to_owned()),
    )?;

    let response = create_device_code(&router, "test-client", None).await?;

    assert_eq!(response["device_code"], "custom-device-code");
    assert_eq!(response["user_code"], "CUSTOM12");
    Ok(())
}

#[tokio::test]
async fn device_code_route_uses_async_custom_generators() -> Result<(), Box<dyn std::error::Error>>
{
    let adapter = Arc::new(TestAdapter::default());
    let router = router(
        adapter,
        DeviceAuthorizationOptions::new()
            .generate_device_code_async(|| async { "async-device-code".to_owned() })
            .generate_user_code_async(|| async { "ASYNC123".to_owned() }),
    )?;

    let response = create_device_code(&router, "test-client", None).await?;

    assert_eq!(response["device_code"], "async-device-code");
    assert_eq!(response["user_code"], "ASYNC123");
    Ok(())
}

#[tokio::test]
async fn device_code_route_accepts_valid_client() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(
        adapter,
        DeviceAuthorizationOptions::new()
            .validate_client(|client_id| async move { Ok(client_id == "valid-client") }),
    )?;

    let response = create_device_code(&router, "valid-client", None).await?;

    assert_eq!(response["interval"], 5);
    Ok(())
}

#[tokio::test]
async fn device_code_route_runs_auth_request_hook() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let called = Arc::new(AtomicBool::new(false));
    let hook_called = Arc::clone(&called);
    let router = router(
        adapter,
        DeviceAuthorizationOptions::new().on_device_auth_request(move |client_id, scope| {
            let hook_called = Arc::clone(&hook_called);
            async move {
                hook_called.store(
                    client_id == "test-client" && scope.as_deref() == Some("read"),
                    Ordering::SeqCst,
                );
                Ok(())
            }
        }),
    )?;

    create_device_code(&router, "test-client", Some("read")).await?;

    assert!(called.load(Ordering::SeqCst));
    Ok(())
}

#[tokio::test]
async fn verification_uri_complete_encodes_user_code_with_dashes(
) -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(
        adapter,
        DeviceAuthorizationOptions::new().generate_user_code(|| "ABC-123".to_owned()),
    )?;

    let response = create_device_code(&router, "test-client", None).await?;

    assert_eq!(response["user_code"], "ABC-123");
    assert!(string_field(&response, "verification_uri_complete").contains("ABC-123"));
    Ok(())
}

#[tokio::test]
async fn verification_uri_supports_relative_absolute_and_existing_query(
) -> Result<(), Box<dyn std::error::Error>> {
    let cases = [
        (
            "/auth/device-verify",
            "http://localhost:3000/auth/device-verify",
            "http://localhost:3000/auth/device-verify?user_code=ABC12345",
        ),
        (
            "https://myapp.com/device",
            "https://myapp.com/device",
            "https://myapp.com/device?user_code=ABC12345",
        ),
        (
            "/device?lang=en",
            "http://localhost:3000/device?lang=en",
            "http://localhost:3000/device?lang=en&user_code=ABC12345",
        ),
    ];

    for (input, expected_uri, expected_complete) in cases {
        let adapter = Arc::new(TestAdapter::default());
        let router = router(
            adapter,
            DeviceAuthorizationOptions::new()
                .verification_uri(input)
                .generate_user_code(|| "ABC12345".to_owned()),
        )?;

        let response = create_device_code(&router, "test-client", None).await?;

        assert_eq!(response["verification_uri"], expected_uri);
        assert_eq!(response["verification_uri_complete"], expected_complete);
    }
    Ok(())
}

#[tokio::test]
async fn device_code_route_accepts_form_body() -> Result<(), Box<dyn std::error::Error>> {
    let adapter = Arc::new(TestAdapter::default());
    let router = router(adapter, DeviceAuthorizationOptions::default())?;

    let response = router
        .handle_async(form_request(
            Method::POST,
            "/api/auth/device/code",
            "client_id=test-client&scope=read+write",
            None,
        )?)
        .await?;

    assert_eq!(response.status(), StatusCode::OK);
    assert_eq!(
        response.headers().get(header::CACHE_CONTROL),
        Some(&http::HeaderValue::from_static("no-store"))
    );
    let body: Value = serde_json::from_slice(response.body())?;
    assert_eq!(body["interval"], 5);
    Ok(())
}