use port_sdk::apis::entities;
use port_sdk::auth::AuthStrategy;
use port_sdk::client::PortClient;
use port_sdk::config::{PortConfigBuilder, RetryConfig};
use port_sdk::error::PortError;
use port_sdk::types::entities::{EntityRequest, EntityResponse};
use serde_json::json;
use serial_test::serial;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use url::Url;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
fn test_retry_config() -> RetryConfig {
RetryConfig {
max_attempts: 3,
max_elapsed_time: Some(Duration::from_secs(2)),
initial_interval: Duration::from_millis(10),
multiplier: 1.0,
max_interval: Duration::from_millis(20),
retry_on_statuses: vec![429, 500],
}
}
async fn test_client(base_url: Url) -> Result<PortClient, PortError> {
let config = PortConfigBuilder::default()
.base_url(base_url)
.auth(AuthStrategy::StaticToken("test-token".into()))
.timeout(Duration::from_secs(5))
.retry(Some(test_retry_config()))
.build()?;
PortClient::from_config(config)
}
#[tokio::test]
#[serial]
async fn client_retries_on_429() -> Result<(), PortError> {
let server = MockServer::start().await;
let base_url = Url::parse(&server.uri()).unwrap();
let attempts = Arc::new(AtomicUsize::new(0));
let attempts_for_mock = Arc::clone(&attempts);
Mock::given(method("GET"))
.and(path("/entities"))
.respond_with(move |_: &wiremock::Request| {
let attempt = attempts_for_mock.fetch_add(1, Ordering::SeqCst);
if attempt == 0 {
ResponseTemplate::new(429).insert_header("Retry-After", "0")
} else {
ResponseTemplate::new(200).set_body_json(json!({"ok": true}))
}
})
.mount(&server)
.await;
let client = test_client(base_url).await?;
let response: serde_json::Value = client.get("/entities").await?;
assert_eq!(response["ok"], true);
assert_eq!(attempts.load(Ordering::SeqCst), 2);
Ok(())
}
#[tokio::test]
#[serial]
async fn entity_lifecycle_tracks_resources() -> Result<(), PortError> {
let server = MockServer::start().await;
let base_url = Url::parse(&server.uri()).unwrap();
let entity_response = EntityResponse {
identifier: "entity-1".into(),
blueprint: "blueprint-1".into(),
title: Some("Entity".into()),
properties: None,
};
Mock::given(method("POST"))
.and(path("/blueprints/blueprint-1/entities"))
.respond_with(ResponseTemplate::new(200).set_body_json(&entity_response))
.expect(1)
.mount(&server)
.await;
Mock::given(method("PUT"))
.and(path("/blueprints/blueprint-1/entities/entity-1"))
.respond_with(ResponseTemplate::new(200).set_body_json(&entity_response))
.expect(1)
.mount(&server)
.await;
Mock::given(method("DELETE"))
.and(path("/blueprints/blueprint-1/entities/entity-1"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({"deleted": true})))
.expect(1)
.mount(&server)
.await;
let client = test_client(base_url).await?;
let create_request = EntityRequest::new("entity-1", "blueprint-1", None);
let _: EntityResponse = entities::create(&client, "blueprint-1", &create_request).await?;
let outstanding = client.tracker().outstanding();
assert_eq!(outstanding.get("entity").unwrap().len(), 1);
let _: EntityResponse =
entities::upsert(&client, "blueprint-1", "entity-1", &create_request).await?;
let _: serde_json::Value = entities::delete(&client, "blueprint-1", "entity-1").await?;
assert!(client.tracker().outstanding().is_empty());
Ok(())
}