#[cfg(test)]
mod credential_ordering_tests {
use super::*;
use uuid::Uuid;
#[test]
fn private_endpoints_require_host_posture_and_local_identity() {
let file = std::env::temp_dir().join(format!("mcp-test-{}.json", Uuid::new_v4()));
let registry = McpRegistry::new_with_state_file(file.clone());
let local = TenantContext::local_implicit();
let explicit = TenantContext::explicit_user_workspace(
"org-a",
"workspace-a",
None,
"alice",
);
assert!(
!registry.allow_private_endpoint_for(&local),
"local identity without verified host posture must fail closed"
);
registry.set_standalone_private_endpoint_access(true);
assert!(registry.allow_private_endpoint_for(&local));
assert!(
!registry.allow_private_endpoint_for(&explicit),
"host posture must not grant private egress to an explicit hosted tenant"
);
registry.set_strict_tenant_enforcement(true);
assert!(
!registry.allow_private_endpoint_for(&local),
"hosted strict-tenant mode must fail closed even on a loopback bind"
);
registry.set_strict_tenant_enforcement(false);
registry.set_standalone_private_endpoint_access(false);
assert!(!registry.allow_private_endpoint_for(&local));
let _ = std::fs::remove_file(file);
}
#[tokio::test]
async fn resolved_private_target_rechecks_authorization_before_send() {
let file = std::env::temp_dir().join(format!("mcp-test-{}.json", Uuid::new_v4()));
let registry = McpRegistry::new_with_state_file(file.clone());
let tenant = TenantContext::local_implicit();
registry.set_standalone_private_endpoint_access(true);
let authorization = McpEndpointAuthorization::for_registry(®istry, &tenant);
let target = resolve_mcp_http_target("http://127.0.0.1:39731/mcp", &authorization)
.await
.expect("resolve authorized private target");
target
.ensure_authorized(&authorization)
.expect("authorization remains live");
registry.set_standalone_private_endpoint_access(false);
let error = target
.ensure_authorized(&authorization)
.expect_err("revocation after resolution must block the send boundary");
assert!(error.contains("authorization was revoked"));
let _ = std::fs::remove_file(file);
}
#[tokio::test]
async fn auth_clear_and_replacement_share_one_mutation_order() {
let _provider_auth_guard = super::tests::provider_auth_test_guard().await;
let file = std::env::temp_dir().join(format!("mcp-test-{}.json", Uuid::new_v4()));
let registry = McpRegistry::new_with_state_file(file.clone());
let tenant = TenantContext::explicit_user_workspace(
format!("credential-order-org-{}", Uuid::new_v4()),
"workspace-a",
None,
"alice",
);
registry
.add_or_update(
"notion".to_string(),
"https://example.com/mcp".to_string(),
HashMap::new(),
true,
)
.await;
registry
.set_bearer_token_for_tenant("notion", "old-token", &tenant)
.await
.expect("set initial token");
let mutation_guard = registry.credential_mutation_lock.lock().await;
let clear_registry = registry.clone();
let clear_tenant = tenant.clone();
let clear_task = tokio::spawn(async move {
clear_registry
.clear_auth_material_for_tenant("notion", &clear_tenant)
.await
});
let replace_registry = registry.clone();
let replace_tenant = tenant.clone();
let replace_task = tokio::spawn(async move {
replace_registry
.set_bearer_token_for_tenant("notion", "replacement-token", &replace_tenant)
.await
});
tokio::task::yield_now().await;
assert!(
!clear_task.is_finished() && !replace_task.is_finished(),
"clear and replacement must both wait for the credential mutation order"
);
drop(mutation_guard);
assert!(
clear_task.await.expect("clear task"),
"clear must find the tenant connection"
);
assert!(
replace_task
.await
.expect("replacement task")
.expect("replace token"),
"replacement must find the MCP server"
);
let connection_id = registry.connection_id_for_tenant("notion", &tenant);
let connections = registry.list_connections().await;
let connection = connections
.get(&connection_id)
.expect("tenant connection remains present");
if let Some(secret_ref) = connection.secret_headers.get("Authorization") {
assert_eq!(
resolve_secret_ref_value(secret_ref, &tenant).as_deref(),
Some("Bearer replacement-token"),
"a surviving replacement ref must retain its backing credential"
);
} else {
assert!(
connection.credential_ref.is_none(),
"a winning clear must leave no credential reference"
);
}
let _ = registry
.clear_auth_material_for_tenant("notion", &tenant)
.await;
let _ = std::fs::remove_file(file);
}
}