port-sdk 0.1.0

Rust SDK for Port APIs.
Documentation
use port_sdk::auth::{AuthStrategy, ClientCredentialsOptions};
use serde_json::json;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use tokio::runtime::Runtime;
use url::Url;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};

fn runtime() -> Runtime {
    Runtime::new().expect("tokio runtime")
}

#[test]
fn static_token_provider_clones_token() {
    let provider =
        AuthStrategy::StaticToken("secret".to_string()).into_provider().expect("provider");
    let token1 = runtime().block_on(provider.bearer_token()).unwrap();
    let token2 = runtime().block_on(provider.bearer_token()).unwrap();
    assert_eq!(token1, token2);
}

#[test]
fn client_credentials_provider_caches_tokens() {
    let rt = runtime();
    let server = rt.block_on(MockServer::start());
    let attempts = Arc::new(AtomicUsize::new(0));
    let attempts_for_mock = Arc::clone(&attempts);

    rt.block_on(async {
        Mock::given(method("POST"))
            .and(path("/oauth/token"))
            .respond_with(move |_: &wiremock::Request| {
                attempts_for_mock.fetch_add(1, Ordering::SeqCst);
                ResponseTemplate::new(200).set_body_json(json!({
                    "access_token": "token-1",
                    "expires_in": 3600
                }))
            })
            .mount(&server)
            .await;
    });

    let options = ClientCredentialsOptions {
        client_id: "id".into(),
        client_secret: "secret".into(),
        token_url: Url::parse(&format!("{}/oauth/token", server.uri())).unwrap(),
        minimum_ttl: Duration::from_secs(30),
    };

    let provider = AuthStrategy::ClientCredentials(options).into_provider().expect("provider");

    let token1 = rt.block_on(provider.bearer_token()).unwrap();
    let token2 = rt.block_on(provider.bearer_token()).unwrap();

    assert_eq!(token1, "token-1");
    assert_eq!(token2, "token-1");
    assert_eq!(attempts.load(Ordering::SeqCst), 1);
}

#[test]
fn client_credentials_provider_refreshes_expired_tokens() {
    let rt = runtime();
    let server = rt.block_on(MockServer::start());
    let attempts = Arc::new(AtomicUsize::new(0));
    let attempts_for_mock = Arc::clone(&attempts);

    rt.block_on(async {
        Mock::given(method("POST"))
            .and(path("/oauth/token"))
            .respond_with(move |_: &wiremock::Request| {
                let attempt = attempts_for_mock.fetch_add(1, Ordering::SeqCst);
                if attempt == 0 {
                    ResponseTemplate::new(200).set_body_json(json!({
                        "access_token": "token-1",
                        "expires_in": 5
                    }))
                } else {
                    ResponseTemplate::new(200).set_body_json(json!({
                        "access_token": "token-2",
                        "expires_in": 3600
                    }))
                }
            })
            .mount(&server)
            .await;
    });

    let options = ClientCredentialsOptions {
        client_id: "id".into(),
        client_secret: "secret".into(),
        token_url: Url::parse(&format!("{}/oauth/token", server.uri())).unwrap(),
        minimum_ttl: Duration::from_secs(10),
    };

    let provider = AuthStrategy::ClientCredentials(options).into_provider().expect("provider");

    let token1 = rt.block_on(provider.bearer_token()).unwrap();
    assert_eq!(token1, "token-1");

    let token2 = rt.block_on(provider.bearer_token()).unwrap();
    assert_eq!(token2, "token-2");
    assert_eq!(attempts.load(Ordering::SeqCst), 2);
}