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>
370 lines
11 KiB
Rust
370 lines
11 KiB
Rust
//! 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");
|
|
}
|
|
}
|