feat(phase-5): WS1 — Rust SDK
Implements the sentryagent-idp Rust SDK crate (sdk-rust/) with: - TokenManager with Arc<Mutex<TokenCache>> for thread-safe token caching - AgentIdPClient with full method coverage: agents, oauth2, credentials, audit, marketplace, delegation - Error hierarchy via thiserror (AgentIdPError enum) - All model types with serde derive - 429 RateLimited handling with Retry-After parsing; zero unwrap() calls - Unit tests (mockito), doc tests, and integration tests (#[ignore]) - quickstart example, full README, cargo doc clean Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
369
sdk-rust/tests/integration_test.rs
Normal file
369
sdk-rust/tests/integration_test.rs
Normal file
@@ -0,0 +1,369 @@
|
||||
//! Integration tests for the SentryAgent.ai AgentIdP Rust SDK.
|
||||
//!
|
||||
//! These tests run against a real API instance. They are marked `#[ignore]`
|
||||
//! and will not execute in CI unless explicitly opted in with:
|
||||
//!
|
||||
//! ```bash
|
||||
//! AGENTIDP_API_URL=https://api.sentryagent.ai \
|
||||
//! AGENTIDP_CLIENT_ID=... \
|
||||
//! AGENTIDP_CLIENT_SECRET=... \
|
||||
//! cargo test -- --ignored
|
||||
//! ```
|
||||
|
||||
use sentryagent_idp::{
|
||||
AgentIdPClient, AuditLogFilters, DelegateRequest, MarketplaceFilters, RegisterAgentRequest,
|
||||
UpdateAgentRequest,
|
||||
};
|
||||
|
||||
/// Helper — build a client from environment variables, skipping the test when
|
||||
/// any required variable is unset (rather than panicking).
|
||||
fn client_from_env() -> Option<AgentIdPClient> {
|
||||
AgentIdPClient::from_env().ok()
|
||||
}
|
||||
|
||||
// ─── Agent CRUD ───────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_register_and_delete_agent() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let agent = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "integration-test-agent".to_owned(),
|
||||
description: Some("Created by integration test".to_owned()),
|
||||
agent_type: "worker".to_owned(),
|
||||
capabilities: vec!["read:data".to_owned()],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register_agent should succeed");
|
||||
|
||||
assert!(!agent.id.is_empty());
|
||||
assert_eq!(agent.name, "integration-test-agent");
|
||||
|
||||
client
|
||||
.delete_agent(&agent.id)
|
||||
.await
|
||||
.expect("delete_agent should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_get_agent() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let created = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "get-test-agent".to_owned(),
|
||||
description: None,
|
||||
agent_type: "worker".to_owned(),
|
||||
capabilities: vec![],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register_agent should succeed");
|
||||
|
||||
let fetched = client
|
||||
.get_agent(&created.id)
|
||||
.await
|
||||
.expect("get_agent should succeed");
|
||||
|
||||
assert_eq!(fetched.id, created.id);
|
||||
assert_eq!(fetched.name, "get-test-agent");
|
||||
|
||||
client.delete_agent(&created.id).await.ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_list_agents() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let list = client
|
||||
.list_agents(Some(1), Some(10))
|
||||
.await
|
||||
.expect("list_agents should succeed");
|
||||
|
||||
// Must return a valid pagination envelope.
|
||||
assert!(list.page >= 1);
|
||||
assert!(list.per_page > 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_update_agent() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let agent = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "update-test-agent".to_owned(),
|
||||
description: None,
|
||||
agent_type: "worker".to_owned(),
|
||||
capabilities: vec![],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register_agent should succeed");
|
||||
|
||||
let updated = client
|
||||
.update_agent(
|
||||
&agent.id,
|
||||
UpdateAgentRequest {
|
||||
name: Some("updated-name".to_owned()),
|
||||
description: Some("Updated description".to_owned()),
|
||||
capabilities: None,
|
||||
is_public: None,
|
||||
metadata: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("update_agent should succeed");
|
||||
|
||||
assert_eq!(updated.name, "updated-name");
|
||||
|
||||
client.delete_agent(&agent.id).await.ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_get_agent_not_found() {
|
||||
use sentryagent_idp::AgentIdPError;
|
||||
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let result = client
|
||||
.get_agent("00000000-0000-0000-0000-000000000000")
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
matches!(result, Err(AgentIdPError::NotFound(_))),
|
||||
"Expected NotFound error, got: {:?}",
|
||||
result
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Credentials ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_generate_and_rotate_credentials() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let agent = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "creds-test-agent".to_owned(),
|
||||
description: None,
|
||||
agent_type: "worker".to_owned(),
|
||||
capabilities: vec![],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register_agent should succeed");
|
||||
|
||||
let creds = client
|
||||
.generate_credentials(&agent.id)
|
||||
.await
|
||||
.expect("generate_credentials should succeed");
|
||||
|
||||
assert!(!creds.client_id.is_empty());
|
||||
assert!(!creds.client_secret.is_empty());
|
||||
|
||||
let rotated = client
|
||||
.rotate_credentials(&agent.id)
|
||||
.await
|
||||
.expect("rotate_credentials should succeed");
|
||||
|
||||
// Rotated secret must differ from the original.
|
||||
assert_ne!(rotated.client_secret, creds.client_secret);
|
||||
|
||||
client.delete_agent(&agent.id).await.ok();
|
||||
}
|
||||
|
||||
// ─── OAuth2 ──────────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_issue_token() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let agent = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "token-test-agent".to_owned(),
|
||||
description: None,
|
||||
agent_type: "worker".to_owned(),
|
||||
capabilities: vec![],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register_agent should succeed");
|
||||
|
||||
let token = client
|
||||
.issue_token(&agent.id, &["agents:read"])
|
||||
.await
|
||||
.expect("issue_token should succeed");
|
||||
|
||||
assert!(!token.access_token.is_empty());
|
||||
assert_eq!(token.token_type.to_lowercase(), "bearer");
|
||||
|
||||
client.delete_agent(&agent.id).await.ok();
|
||||
}
|
||||
|
||||
// ─── Audit logs ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_list_audit_logs() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let logs = client
|
||||
.list_audit_logs(AuditLogFilters {
|
||||
agent_id: None,
|
||||
event_type: None,
|
||||
from: None,
|
||||
to: None,
|
||||
page: 1,
|
||||
per_page: 20,
|
||||
})
|
||||
.await
|
||||
.expect("list_audit_logs should succeed");
|
||||
|
||||
assert!(logs.page >= 1);
|
||||
assert!(logs.per_page > 0);
|
||||
}
|
||||
|
||||
// ─── Marketplace ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_list_public_agents() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let results = client
|
||||
.list_public_agents(MarketplaceFilters {
|
||||
q: None,
|
||||
capability: None,
|
||||
publisher: None,
|
||||
page: 1,
|
||||
per_page: 10,
|
||||
})
|
||||
.await
|
||||
.expect("list_public_agents should succeed");
|
||||
|
||||
assert!(results.page >= 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_marketplace_search() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let results = client
|
||||
.list_public_agents(MarketplaceFilters {
|
||||
q: Some("agent".to_owned()),
|
||||
capability: None,
|
||||
publisher: None,
|
||||
page: 1,
|
||||
per_page: 5,
|
||||
})
|
||||
.await
|
||||
.expect("list_public_agents with query should succeed");
|
||||
|
||||
// Result may be empty but must be a valid envelope.
|
||||
assert!(results.per_page > 0);
|
||||
}
|
||||
|
||||
// ─── Delegation ──────────────────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_delegate_and_verify() {
|
||||
let client = client_from_env().expect("AGENTIDP_* env vars must be set");
|
||||
|
||||
let delegator = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "delegator-agent".to_owned(),
|
||||
description: None,
|
||||
agent_type: "orchestrator".to_owned(),
|
||||
capabilities: vec!["agents:write".to_owned()],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register delegator should succeed");
|
||||
|
||||
let delegatee = client
|
||||
.register_agent(RegisterAgentRequest {
|
||||
name: "delegatee-agent".to_owned(),
|
||||
description: None,
|
||||
agent_type: "worker".to_owned(),
|
||||
capabilities: vec!["agents:read".to_owned()],
|
||||
metadata: None,
|
||||
})
|
||||
.await
|
||||
.expect("register delegatee should succeed");
|
||||
|
||||
let delegation = client
|
||||
.delegate(DelegateRequest {
|
||||
delegatee_agent_id: delegatee.id.clone(),
|
||||
scopes: vec!["agents:read".to_owned()],
|
||||
ttl_seconds: 3600,
|
||||
})
|
||||
.await
|
||||
.expect("delegate should succeed");
|
||||
|
||||
assert!(!delegation.delegation_token.is_empty());
|
||||
assert!(!delegation.chain_id.is_empty());
|
||||
|
||||
let verification = client
|
||||
.verify_delegation(&delegation.delegation_token)
|
||||
.await
|
||||
.expect("verify_delegation should succeed");
|
||||
|
||||
assert!(verification.valid);
|
||||
assert_eq!(
|
||||
verification.delegatee_agent_id.as_deref(),
|
||||
Some(delegatee.id.as_str())
|
||||
);
|
||||
|
||||
client.delete_agent(&delegator.id).await.ok();
|
||||
client.delete_agent(&delegatee.id).await.ok();
|
||||
}
|
||||
|
||||
// ─── Token manager concurrency ────────────────────────────────────────────────
|
||||
|
||||
#[tokio::test]
|
||||
#[ignore]
|
||||
async fn test_token_manager_concurrent_calls() {
|
||||
use std::sync::Arc;
|
||||
use sentryagent_idp::TokenManager;
|
||||
|
||||
let api_url = std::env::var("AGENTIDP_API_URL").expect("AGENTIDP_API_URL must be set");
|
||||
let client_id =
|
||||
std::env::var("AGENTIDP_CLIENT_ID").expect("AGENTIDP_CLIENT_ID must be set");
|
||||
let client_secret =
|
||||
std::env::var("AGENTIDP_CLIENT_SECRET").expect("AGENTIDP_CLIENT_SECRET must be set");
|
||||
|
||||
let tm = Arc::new(TokenManager::new(&api_url, &client_id, &client_secret));
|
||||
|
||||
let handles: Vec<_> = (0..50)
|
||||
.map(|_| {
|
||||
let tm_clone = Arc::clone(&tm);
|
||||
tokio::spawn(async move { tm_clone.get_token().await })
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut tokens = Vec::with_capacity(50);
|
||||
for handle in handles {
|
||||
let token = handle
|
||||
.await
|
||||
.expect("task did not panic")
|
||||
.expect("get_token succeeded");
|
||||
tokens.push(token);
|
||||
}
|
||||
|
||||
// All 50 calls must return the same token (single fetch, all from cache).
|
||||
let first = &tokens[0];
|
||||
for t in &tokens[1..] {
|
||||
assert_eq!(t, first, "all concurrent calls must return the same token");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user