#![allow(dead_code, unused_imports)] use super::common::*; use std::sync::{Arc, Mutex}; use axum::{Json, Router, http::HeaderMap, routing::post}; use crank_core::{AuditError, AuditEvent, AuditSink, PlatformApiKeyKind, SecretKind}; use serde_json::{Value, json}; use serial_test::serial; use tokio::net::TcpListener; #[derive(Clone, Default)] struct RecordingAuditSink { events: Arc>>, } impl RecordingAuditSink { fn events(&self) -> Vec { self.events.lock().unwrap().clone() } } #[async_trait::async_trait] impl AuditSink for RecordingAuditSink { async fn record(&self, event: AuditEvent) -> Result<(), AuditError> { self.events.lock().unwrap().push(event); Ok(()) } } #[tokio::test(flavor = "multi_thread")] #[serial] async fn machine_key_raw_value_is_create_only_and_duplicate_name_is_typed_conflict() { let registry = test_registry().await; let storage_root = test_storage_root("credential_lifecycle_keys"); let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; let client = authorized_client(&base_url).await; let agent_id = create_agent(&client, &base_url, "credential-lifecycle-agent").await; let created = post_json( &client, format!("{base_url}/agents/{agent_id}/platform-api-keys"), json!({"name": "primary-mcp-key", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read", "write"]}), ) .await; let raw_key = created["secret"] .as_str() .expect("create response must disclose the raw key once") .to_owned(); let random_part = raw_key .strip_prefix("crk_") .expect("MCP key must use the MCP bearer marker"); assert_eq!(random_part.len(), 43, "32 raw bytes in unpadded base64url"); assert!( random_part .bytes() .all(|byte| { byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_') }) ); assert_eq!( created["connection"]["endpoint"], "http://localhost:3000/mcp/v1/default/credential-lifecycle-agent" ); assert!( created["connection"]["clients"] .as_array() .is_some_and(|clients| !clients.is_empty()) ); let approval = post_json( &client, format!("{base_url}/agents/{agent_id}/platform-api-keys"), json!({"name": "approval-without-mcp-config", "key_kind": PlatformApiKeyKind::Approval, "scopes": ["read_pending"]}), ) .await; assert!(approval.get("connection").is_none()); let listed_text = client .get(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .send() .await .unwrap() .text() .await .unwrap(); assert!(!listed_text.contains(&raw_key)); assert!(!listed_text.contains("secret_hash")); assert!(!listed_text.contains("\"secret\"")); let duplicate = client .post(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .json(&json!({ "name": "primary-mcp-key", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"] })) .send() .await .unwrap(); assert_eq!(duplicate.status(), reqwest::StatusCode::CONFLICT); let duplicate_body = duplicate.text().await.unwrap(); assert!(!duplicate_body.contains(&raw_key)); assert!(!duplicate_body.contains("secret_hash")); assert!(!duplicate_body.contains("duplicate key value")); let duplicate_json: Value = serde_json::from_str(&duplicate_body).unwrap(); assert_eq!( duplicate_json["error"]["code"], "platform_api_key_name_conflict" ); for payload in [ json!({"name": "n".repeat(129), "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"]}), json!({"name": "duplicate-scope", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read", "read"]}), json!({"name": "past-expiry", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"], "expires_at": "2020-01-01T00:00:00Z"}), ] { assert_post_error( &client, format!("{base_url}/agents/{agent_id}/platform-api-keys"), payload, reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; } let canonical = post_json( &client, format!("{base_url}/agents/{agent_id}/platform-api-keys"), json!({"name": "canonical-approval-origin", "key_kind": PlatformApiKeyKind::Approval, "scopes": ["read_pending"], "allowed_origins": ["HTTPS://Example.COM:443/", "http://EXAMPLE.com:80/"]}), ) .await; assert_eq!( canonical["api_key"]["api_key"]["allowed_origins"], json!(["https://example.com", "http://example.com"]) ); for (index, origin) in [ "https://[::1", "https://example.test:bad/", "https://user:pass@example.test/", "https://example.test/path", "https://example.test/?token=origin-canary", "https://example.test/#fragment", ] .into_iter() .enumerate() { assert_post_error( &client, format!("{base_url}/agents/{agent_id}/platform-api-keys"), json!({"name": format!("malformed-origin-{index}"), "key_kind": PlatformApiKeyKind::Approval, "scopes": ["read_pending"], "allowed_origins": [origin]}), reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; } assert_post_error( &client, format!("{base_url}/agents/{agent_id}/platform-api-keys"), json!({"name": "duplicate-canonical-origin", "key_kind": PlatformApiKeyKind::Approval, "scopes": ["read_pending"], "allowed_origins": ["https://EXAMPLE.test:443/", "https://example.test/"]}), reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; } #[tokio::test(flavor = "multi_thread")] #[serial] async fn deleted_machine_key_name_can_be_reused_and_terminal_state_cannot_regress() { let registry = test_registry().await; let storage_root = test_storage_root("credential_lifecycle_key_reuse"); let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; let client = authorized_client(&base_url).await; let agent_id = create_agent(&client, &base_url, "credential-key-reuse-agent").await; let first = assert_success_json( client .post(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .json(&json!({ "name": "replaceable-key", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"] })) .send() .await .unwrap(), ) .await; let first_key_id = first["api_key"]["api_key"]["id"].as_str().unwrap(); let delete_response = client .delete(format!( "{base_url}/agents/{agent_id}/platform-api-keys/{first_key_id}" )) .send() .await .unwrap(); assert_eq!(delete_response.status(), reqwest::StatusCode::NO_CONTENT); let revoke_deleted = client .post(format!( "{base_url}/agents/{agent_id}/platform-api-keys/{first_key_id}/revoke" )) .send() .await .unwrap(); assert_eq!(revoke_deleted.status(), reqwest::StatusCode::CONFLICT); let revoke_body: Value = serde_json::from_str(&revoke_deleted.text().await.unwrap()).unwrap(); assert_eq!(revoke_body["error"]["code"], "platform_api_key_not_active"); let second = assert_success_json( client .post(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .json(&json!({ "name": "replaceable-key", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"] })) .send() .await .unwrap(), ) .await; let second_key_id = second["api_key"]["api_key"]["id"].as_str().unwrap(); assert_ne!(first_key_id, second_key_id); } #[tokio::test(flavor = "multi_thread")] #[serial] async fn secret_plaintext_is_write_only_and_auth_profile_stores_reference() { let registry = test_registry().await; let storage_root = test_storage_root("credential_lifecycle_secrets"); let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; let client = authorized_client(&base_url).await; let canary = "secret-canary-1-10-do-not-leak"; let created_response = client .post(format!("{base_url}/secrets")) .json(&json!({ "name": "crm-bearer-token", "kind": SecretKind::Token, "value": { "token": canary } })) .send() .await .unwrap(); let created_text = created_response.text().await.unwrap(); assert!(!created_text.contains(canary)); assert!(!created_text.contains("ciphertext")); let created: Value = serde_json::from_str(&created_text).unwrap(); let secret_id = created["id"].as_str().unwrap(); let listed_text = client .get(format!("{base_url}/secrets")) .send() .await .unwrap() .text() .await .unwrap(); assert!(!listed_text.contains(canary)); assert!(!listed_text.contains("ciphertext")); let fetched_text = client .get(format!("{base_url}/secrets/{secret_id}")) .send() .await .unwrap() .text() .await .unwrap(); assert!(!fetched_text.contains(canary)); assert!(!fetched_text.contains("ciphertext")); let auth_profile_text = client .post(format!("{base_url}/auth-profiles")) .json(&json!({ "name": "crm-bearer", "kind": "bearer", "config": { "bearer": { "header_name": "Authorization", "secret_id": secret_id } } })) .send() .await .unwrap() .text() .await .unwrap(); assert!(!auth_profile_text.contains(canary)); assert!(!auth_profile_text.contains("ciphertext")); let auth_profile: Value = serde_json::from_str(&auth_profile_text).unwrap(); assert_eq!(auth_profile["config"]["bearer"]["secret_id"], secret_id); for (name, kind, value) in [ ("null-token", SecretKind::Token, Value::Null), ( "wrong-token-shape", SecretKind::Token, json!({"token": "token", "extra": "field"}), ), ( "wrong-header-shape", SecretKind::Header, json!({"header_name": "X-Token"}), ), ( "wrong-username-password-shape", SecretKind::UsernamePassword, json!({"username": "user"}), ), ] { assert_post_error( &client, format!("{base_url}/secrets"), json!({"name": name, "kind": kind, "value": value}), reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; } assert_post_error( &client, format!("{base_url}/secrets"), json!({"name": "oversized-secret", "kind": SecretKind::Generic, "value": "x".repeat(65_537)}), reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; for value in [ Value::Null, json!({"token": "token", "extra": "field"}), json!("x".repeat(65_537)), ] { assert_post_error( &client, format!("{base_url}/secrets/{secret_id}/rotate"), json!({"value": value}), reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; } let current = assert_success_json( client .get(format!("{base_url}/secrets/{secret_id}")) .send() .await .unwrap(), ) .await; assert_eq!(current["current_version"], 1); for payload in [ json!({"name": "", "kind": "bearer", "config": {"bearer": {"header_name": "Authorization", "secret_id": secret_id}}}), json!({"name": "n".repeat(129), "kind": "bearer", "config": {"bearer": {"header_name": "Authorization", "secret_id": secret_id}}}), json!({"name": "invalid-header-name", "kind": "bearer", "config": {"bearer": {"header_name": "Bad Header", "secret_id": secret_id}}}), json!({"name": "invalid-query-name", "kind": "api_key_query", "config": {"api_key_query": {"param_name": "bad name", "secret_id": secret_id}}}), ] { assert_post_error( &client, format!("{base_url}/auth-profiles"), payload, reqwest::StatusCode::BAD_REQUEST, "validation_error", ) .await; } } #[tokio::test(flavor = "multi_thread")] #[serial] async fn credential_mutations_emit_bounded_non_secret_audit_events() { let registry = test_registry().await; let storage_root = test_storage_root("credential_lifecycle_audit"); let audit_sink = RecordingAuditSink::default(); let base_url = spawn_admin_api(build_test_app_with_audit_sink( registry, storage_root, Arc::new(audit_sink.clone()), )) .await; let client = authorized_client(&base_url).await; let request_id = "req_credential_audit_1"; let traceparent = "00-11111111111111111111111111111111-2222222222222222-01"; let secret = assert_success_json( client .post(format!("{base_url}/secrets")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "name": "audit-token", "kind": SecretKind::Token, "value": { "token": "audit-secret-canary" } })) .send() .await .unwrap(), ) .await; let secret_id = secret["id"].as_str().unwrap(); assert_success_json( client .post(format!("{base_url}/secrets/{secret_id}/rotate")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "value": { "token": "audit-rotated-canary" } })) .send() .await .unwrap(), ) .await; let auth_profile = assert_success_json( client .post(format!("{base_url}/auth-profiles")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "name": "audit-profile", "kind": "bearer", "config": { "bearer": { "header_name": "Authorization", "secret_id": secret_id } } })) .send() .await .unwrap(), ) .await; let auth_profile_id = auth_profile["id"].as_str().unwrap(); let delete_denied = client .delete(format!("{base_url}/secrets/{secret_id}")) .header("x-request-id", request_id) .header("traceparent", traceparent) .send() .await .unwrap(); assert_eq!(delete_denied.status(), reqwest::StatusCode::CONFLICT); let created_agent = assert_success_json( client .post(format!("{base_url}/agents")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "slug": "credential-audit-agent", "display_name": "Credential Audit Agent", "description": "Agent for credential audit tests.", "instructions": {}, "tool_selection_policy": {} })) .send() .await .unwrap(), ) .await; let agent_id = created_agent["agent_id"].as_str().unwrap(); let created_key = assert_success_json( client .post(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "name": "audit-mcp-key", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"] })) .send() .await .unwrap(), ) .await; let raw_key = created_key["secret"].as_str().unwrap(); let key_id = created_key["api_key"]["api_key"]["id"].as_str().unwrap(); let duplicate_key = client .post(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "name": "audit-mcp-key", "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"] })) .send() .await .unwrap(); assert_eq!(duplicate_key.status(), reqwest::StatusCode::CONFLICT); let validation_key = client .post(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "name": "n".repeat(129), "key_kind": PlatformApiKeyKind::McpClient, "scopes": ["read"] })) .send() .await .unwrap(); assert_eq!(validation_key.status(), reqwest::StatusCode::BAD_REQUEST); let invalid_profile = client .post(format!("{base_url}/auth-profiles")) .header("x-request-id", request_id) .header("traceparent", traceparent) .json(&json!({ "name": "audit-invalid-profile", "kind": "bearer", "config": { "bearer": { "header_name": "Authorization", "secret_id": "secret_missing_for_audit" } } })) .send() .await .unwrap(); assert_eq!(invalid_profile.status(), reqwest::StatusCode::NOT_FOUND); client .post(format!( "{base_url}/agents/{agent_id}/platform-api-keys/{key_id}/revoke" )) .header("x-request-id", request_id) .header("traceparent", traceparent) .send() .await .unwrap(); let delete_response = client .delete(format!( "{base_url}/agents/{agent_id}/platform-api-keys/{key_id}" )) .header("x-request-id", request_id) .header("traceparent", traceparent) .send() .await .unwrap(); assert_eq!(delete_response.status(), reqwest::StatusCode::NO_CONTENT); let listed_after_delete = assert_success_json( client .get(format!("{base_url}/agents/{agent_id}/platform-api-keys")) .send() .await .unwrap(), ) .await; let deleted_metadata = listed_after_delete["items"] .as_array() .unwrap() .iter() .find(|item| item["api_key"]["id"] == key_id) .expect("delete must preserve key metadata for provenance"); assert_eq!(deleted_metadata["api_key"]["status"], "deleted"); let listed_after_delete_text = serde_json::to_string(&listed_after_delete).unwrap(); assert!(!listed_after_delete_text.contains(raw_key)); let events = audit_sink.events(); let actions: Vec<&str> = events.iter().map(|event| event.action.as_str()).collect(); for expected in [ "credential.secret.created", "credential.secret.rotated", "credential.auth_profile.created", "credential.secret.delete_denied", "credential.platform_api_key.created", "credential.platform_api_key.create_failed", "credential.auth_profile.create_failed", "credential.platform_api_key.revoked", "credential.platform_api_key.deleted", ] { assert!( actions.contains(&expected), "missing audit action {expected}" ); } let audit_json = serde_json::to_string(&events).unwrap(); assert!(audit_json.contains(request_id)); assert!(audit_json.contains("11111111111111111111111111111111")); assert!(audit_json.contains(auth_profile_id)); assert!(!audit_json.contains("audit-secret-canary")); assert!(!audit_json.contains("audit-rotated-canary")); assert!(!audit_json.contains(raw_key)); assert!(!audit_json.contains("secret_hash")); assert!(!audit_json.contains("ciphertext")); assert!(!audit_json.contains("Bearer ")); for event in &events { let reason = event.payload["reason"] .as_str() .expect("credential audit events must contain a reason code"); assert!(!reason.is_empty()); assert!(reason.len() <= 128, "audit reason must remain bounded"); } assert!(events.iter().any(|event| { event.action == "credential.platform_api_key.create_failed" && event.payload["reason"] == "validation_error" })); assert!(events.iter().any(|event| { event.action == "credential.platform_api_key.create_failed" && event.payload["reason"] == "platform_api_key_name_conflict" })); } #[tokio::test(flavor = "multi_thread")] #[serial] async fn disabled_secret_fails_closed_for_reference_rotation_and_execution() { let registry = test_registry().await; let registry_control = registry.clone(); let storage_root = test_storage_root("credential_lifecycle_disabled_secret"); let (upstream_base_url, observed_authorizations) = spawn_auth_capture_upstream().await; let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; let client = authorized_client(&base_url).await; let secret = assert_success_json( client .post(format!("{base_url}/secrets")) .json(&json!({ "name": "disabled-token", "kind": SecretKind::Token, "value": { "token": "disabled-secret-canary" } })) .send() .await .unwrap(), ) .await; let secret_id = secret["id"].as_str().unwrap(); let auth_profile = assert_success_json( client .post(format!("{base_url}/auth-profiles")) .json(&json!({ "name": "disabled-profile", "kind": "bearer", "config": { "bearer": { "header_name": "Authorization", "secret_id": secret_id } } })) .send() .await .unwrap(), ) .await; let auth_profile_id = auth_profile["id"].as_str().unwrap(); let mut operation_payload = serde_json::to_value(test_operation_payload( &upstream_base_url, "disabled_secret_execution", )) .unwrap(); operation_payload["execution_config"]["auth_profile_ref"] = json!(auth_profile_id); let created_operation = assert_success_json( client .post(format!("{base_url}/operations")) .json(&operation_payload) .send() .await .unwrap(), ) .await; let operation_id = created_operation["operation_id"].as_str().unwrap(); sqlx::query("update secrets set status = 'disabled' where id = $1") .bind(secret_id) .execute(registry_control.pool()) .await .unwrap(); let ref_disabled = client .post(format!("{base_url}/auth-profiles")) .json(&json!({ "name": "disabled-profile-new-ref", "kind": "bearer", "config": { "bearer": { "header_name": "Authorization", "secret_id": secret_id } } })) .send() .await .unwrap(); assert_eq!(ref_disabled.status(), reqwest::StatusCode::CONFLICT); let ref_disabled_body: Value = serde_json::from_str(&ref_disabled.text().await.unwrap()).unwrap(); assert_eq!(ref_disabled_body["error"]["code"], "secret_not_active"); let rotate_disabled = client .post(format!("{base_url}/secrets/{secret_id}/rotate")) .json(&json!({ "value": { "token": "should-not-rotate" } })) .send() .await .unwrap(); assert_eq!(rotate_disabled.status(), reqwest::StatusCode::CONFLICT); let rotate_body: Value = serde_json::from_str(&rotate_disabled.text().await.unwrap()).unwrap(); assert_eq!(rotate_body["error"]["code"], "secret_not_active"); let test_run = assert_success_json( client .post(format!("{base_url}/operations/{operation_id}/test-runs")) .json(&json!({ "version": 1, "input": { "email": "disabled@example.com" } })) .send() .await .unwrap(), ) .await; assert_eq!(test_run["ok"], false); let body = serde_json::to_string(&test_run).unwrap(); assert!(!body.contains("disabled-secret-canary")); assert!(observed_authorizations.lock().unwrap().is_empty()); let operation_get = client .get(format!("{base_url}/operations/{operation_id}")) .send() .await .unwrap(); let operation_etag = operation_get .headers() .get(reqwest::header::ETAG) .unwrap() .to_str() .unwrap() .to_owned(); let publish_disabled = client .post(format!("{base_url}/operations/{operation_id}/publish")) .header(reqwest::header::IF_MATCH, operation_etag) .json(&json!({ "version": 1 })) .send() .await .unwrap(); assert_eq!(publish_disabled.status(), reqwest::StatusCode::CONFLICT); let publish_body: Value = serde_json::from_str(&publish_disabled.text().await.unwrap()).unwrap(); assert_eq!(publish_body["error"]["code"], "secret_not_active"); assert!(observed_authorizations.lock().unwrap().is_empty()); } #[tokio::test(flavor = "multi_thread")] #[serial] async fn concurrent_secret_rotation_serializes_versions_without_storage_conflict() { let registry = test_registry().await; let storage_root = test_storage_root("credential_lifecycle_concurrent_rotation"); let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; let client = authorized_client(&base_url).await; let secret = assert_success_json( client .post(format!("{base_url}/secrets")) .json(&json!({ "name": "concurrent-token", "kind": SecretKind::Token, "value": { "token": "initial-concurrent-token" } })) .send() .await .unwrap(), ) .await; let secret_id = secret["id"].as_str().unwrap().to_owned(); let first = client .post(format!("{base_url}/secrets/{secret_id}/rotate")) .json(&json!({ "value": { "token": "concurrent-token-a" } })) .send(); let second = client .post(format!("{base_url}/secrets/{secret_id}/rotate")) .json(&json!({ "value": { "token": "concurrent-token-b" } })) .send(); let (first, second) = tokio::join!(first, second); let first = assert_success_json(first.unwrap()).await; let second = assert_success_json(second.unwrap()).await; assert!(first["current_version"].as_u64().unwrap() >= 2); assert!(second["current_version"].as_u64().unwrap() >= 2); let current = assert_success_json( client .get(format!("{base_url}/secrets/{secret_id}")) .send() .await .unwrap(), ) .await; assert_eq!(current["current_version"], 3); } #[tokio::test(flavor = "multi_thread")] #[serial] async fn auth_profile_secret_rotation_changes_next_admin_test_execution() { let registry = test_registry().await; let storage_root = test_storage_root("credential_lifecycle_rotation"); let (upstream_base_url, observed_authorizations) = spawn_auth_capture_upstream().await; let base_url = spawn_admin_api(build_test_app(registry, storage_root)).await; let client = authorized_client(&base_url).await; let secret = assert_success_json( client .post(format!("{base_url}/secrets")) .json(&json!({ "name": "crm-rotating-token", "kind": SecretKind::Token, "value": { "token": "initial-rotation-token" } })) .send() .await .unwrap(), ) .await; let secret_id = secret["id"].as_str().unwrap(); let auth_profile = assert_success_json( client .post(format!("{base_url}/auth-profiles")) .json(&json!({ "name": "crm-rotating-bearer", "kind": "bearer", "config": { "bearer": { "header_name": "Authorization", "secret_id": secret_id } } })) .send() .await .unwrap(), ) .await; let auth_profile_id = auth_profile["id"].as_str().unwrap(); let mut operation_payload = serde_json::to_value(test_operation_payload( &upstream_base_url, "crm_auth_rotation", )) .unwrap(); operation_payload["execution_config"]["auth_profile_ref"] = json!(auth_profile_id); let created_operation = assert_success_json( client .post(format!("{base_url}/operations")) .json(&operation_payload) .send() .await .unwrap(), ) .await; let operation_id = created_operation["operation_id"].as_str().unwrap(); let first_run = assert_success_json( client .post(format!("{base_url}/operations/{operation_id}/test-runs")) .json(&json!({ "version": 1, "input": { "email": "first@example.com" } })) .send() .await .unwrap(), ) .await; assert_eq!(first_run["ok"], true); let rotated = assert_success_json( client .post(format!("{base_url}/secrets/{secret_id}/rotate")) .json(&json!({ "value": { "token": "rotated-rotation-token" } })) .send() .await .unwrap(), ) .await; assert_eq!(rotated["current_version"], 2); let second_run = assert_success_json( client .post(format!("{base_url}/operations/{operation_id}/test-runs")) .json(&json!({ "version": 1, "input": { "email": "second@example.com" } })) .send() .await .unwrap(), ) .await; assert_eq!(second_run["ok"], true); let captured = observed_authorizations.lock().unwrap().clone(); assert_eq!( captured, vec![ Some("Bearer initial-rotation-token".to_owned()), Some("Bearer rotated-rotation-token".to_owned()) ] ); let auth_profile_after = client .get(format!("{base_url}/auth-profiles/{auth_profile_id}")) .send() .await .unwrap() .text() .await .unwrap(); assert!(auth_profile_after.contains(secret_id)); assert!(!auth_profile_after.contains("initial-rotation-token")); assert!(!auth_profile_after.contains("rotated-rotation-token")); } async fn spawn_auth_capture_upstream() -> (String, Arc>>>) { let observed_authorizations = Arc::new(Mutex::new(Vec::new())); let captured = Arc::clone(&observed_authorizations); let app = Router::new().route( "/crm/leads", post(move |headers: HeaderMap| { let captured = Arc::clone(&captured); async move { let authorization = headers .get("authorization") .and_then(|value| value.to_str().ok()) .map(str::to_owned); captured.lock().unwrap().push(authorization); Json(json!({ "id": "lead_123" })) } }), ); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let address = listener.local_addr().unwrap(); tokio::spawn(async move { axum::serve(listener, app).await.unwrap(); }); (format!("http://{address}"), observed_authorizations) } async fn assert_error_code(response: reqwest::Response, status: reqwest::StatusCode, code: &str) { assert_eq!(response.status(), status); let body = response.text().await.unwrap(); let json: Value = serde_json::from_str(&body).unwrap(); assert_eq!(json["error"]["code"], code, "unexpected error body: {body}"); } async fn post_json(client: &reqwest::Client, url: String, payload: Value) -> Value { assert_success_json(client.post(url).json(&payload).send().await.unwrap()).await } async fn create_agent(client: &reqwest::Client, base_url: impl AsRef, slug: &str) -> String { let base_url = base_url.as_ref(); post_json( client, format!("{base_url}/agents"), json!({"slug": slug, "display_name": slug, "description": "Credential lifecycle test agent.", "instructions": {}, "tool_selection_policy": {}}), ) .await["agent_id"] .as_str() .unwrap() .to_owned() } async fn assert_post_error( client: &reqwest::Client, url: String, payload: Value, status: reqwest::StatusCode, code: &str, ) { assert_error_code( client.post(url).json(&payload).send().await.unwrap(), status, code, ) .await; }