use std::sync::Arc;
use cpex_core::cmf::constants::{ENTITY_HTTP, ENTITY_NAME_GLOBAL, HOOK_CMF_HTTP_REQUEST};
use cpex_core::cmf::enums::Role;
use cpex_core::cmf::{CmfHook, Message, MessagePayload};
use cpex_core::extensions::{Extensions, HttpExtension, MetaExtension};
use cpex_core::manager::PluginManager;
use apl_cmf::constants::{DETAIL_HTTP_BODY, DETAIL_HTTP_HEADERS, DETAIL_HTTP_STATUS};
use apl_cpex::{register_apl, AplOptions};
async fn manager_with(yaml: &str) -> Arc<PluginManager> {
let mgr = Arc::new(PluginManager::default());
register_apl(&mgr, AplOptions::in_process());
mgr.load_config_yaml(yaml).expect("load_config_yaml");
mgr.initialize().await.expect("initialize");
mgr
}
fn http_request(method: &str) -> Extensions {
let mut meta = MetaExtension::default();
meta.entity_type = Some(ENTITY_HTTP.to_string());
meta.entity_name = Some(ENTITY_NAME_GLOBAL.to_string());
let http = HttpExtension {
method: Some(method.to_string()),
..Default::default()
};
Extensions {
meta: Some(Arc::new(meta)),
http: Some(Arc::new(http)),
..Default::default()
}
}
fn payload() -> MessagePayload {
MessagePayload {
message: Message::text(Role::User, "hi"),
}
}
fn tool_request(name: &str) -> Extensions {
let mut meta = MetaExtension::default();
meta.entity_type = Some("tool".to_string());
meta.entity_name = Some(name.to_string());
Extensions {
meta: Some(Arc::new(meta)),
..Default::default()
}
}
fn tool_request_with_http(name: &str, method: &str) -> Extensions {
let mut meta = MetaExtension::default();
meta.entity_type = Some("tool".to_string());
meta.entity_name = Some(name.to_string());
let http = HttpExtension {
method: Some(method.to_string()),
..Default::default()
};
Extensions {
meta: Some(Arc::new(meta)),
http: Some(Arc::new(http)),
..Default::default()
}
}
const GET_ONLY: &str = r#"
plugin_settings:
routing_enabled: true
global:
apl:
pre_invocation:
- "http.method != 'GET': deny"
"#;
#[tokio::test]
async fn global_policy_allows_matching_http_request() {
let mgr = manager_with(GET_ONLY).await;
let (res, _bg) = mgr
.invoke_named::<CmfHook>(HOOK_CMF_HTTP_REQUEST, payload(), http_request("GET"), None)
.await;
assert!(
res.continue_processing,
"GET must be allowed by the global policy; violation = {:?}",
res.violation
);
}
#[tokio::test]
async fn global_policy_denies_nonmatching_http_request() {
let mgr = manager_with(GET_ONLY).await;
let (res, _bg) = mgr
.invoke_named::<CmfHook>(HOOK_CMF_HTTP_REQUEST, payload(), http_request("POST"), None)
.await;
assert!(
!res.continue_processing,
"POST must be denied by the global policy"
);
}
#[tokio::test]
async fn global_policy_deny_carries_custom_response() {
const YAML: &str = r#"
plugin_settings:
routing_enabled: true
global:
apl:
pre_invocation:
- "http.method != 'GET': deny"
response:
status: 403
body: "{\"error\":\"forbidden\"}"
headers:
X-Reason: "method-not-allowed"
"#;
let mgr = manager_with(YAML).await;
let (res, _bg) = mgr
.invoke_named::<CmfHook>(
HOOK_CMF_HTTP_REQUEST,
payload(),
http_request("DELETE"),
None,
)
.await;
assert!(!res.continue_processing, "DELETE must be denied");
let v = res.violation.expect("deny must surface a violation");
assert_eq!(
v.details.get(DETAIL_HTTP_STATUS),
Some(&serde_json::json!(403))
);
assert_eq!(
v.details.get(DETAIL_HTTP_BODY),
Some(&serde_json::json!("{\"error\":\"forbidden\"}"))
);
assert_eq!(
v.details.get(DETAIL_HTTP_HEADERS),
Some(&serde_json::json!({ "X-Reason": "method-not-allowed" }))
);
}
#[tokio::test]
async fn global_response_does_not_leak_onto_entity_denial() {
const YAML: &str = r#"
plugin_settings:
routing_enabled: true
global:
apl:
pre_invocation:
- "require(authenticated)"
response:
status: 403
body: "{\"error\":\"global\"}"
routes:
- tool: locked
apl:
pre_invocation:
- "require(authenticated)"
"#;
let mgr = manager_with(YAML).await;
let (res, _bg) = mgr
.invoke_named::<CmfHook>(
"cmf.tool_pre_invoke",
payload(),
tool_request("locked"),
None,
)
.await;
assert!(!res.continue_processing, "tool policy must deny");
let v = res.violation.expect("deny must surface a violation");
assert!(
!v.details.contains_key(DETAIL_HTTP_STATUS)
&& !v.details.contains_key(DETAIL_HTTP_BODY)
&& !v.details.contains_key(DETAIL_HTTP_HEADERS),
"global response leaked onto entity denial: {:?}",
v.details
);
}
#[tokio::test]
async fn entity_route_reads_http_attributes() {
const YAML: &str = r#"
plugin_settings:
routing_enabled: true
routes:
- tool: echo
apl:
pre_invocation:
- "http.method == 'GET': deny"
"#;
let mgr = manager_with(YAML).await;
let (denied, _bg) = mgr
.invoke_named::<CmfHook>(
"cmf.tool_pre_invoke",
payload(),
tool_request_with_http("echo", "GET"),
None,
)
.await;
assert!(
!denied.continue_processing,
"GET tool request must be denied — proves http.method reached the entity bag (read_headers granted)",
);
let (allowed, _bg) = mgr
.invoke_named::<CmfHook>(
"cmf.tool_pre_invoke",
payload(),
tool_request_with_http("echo", "POST"),
None,
)
.await;
assert!(
allowed.continue_processing,
"POST tool request must pass (http.method == 'GET' is false); violation = {:?}",
allowed.violation,
);
}
#[tokio::test]
async fn route_scoped_response_still_decorates_entity_denial() {
const YAML: &str = r#"
plugin_settings:
routing_enabled: true
routes:
- tool: locked
apl:
pre_invocation:
- "require(authenticated)"
response:
status: 401
body: "route"
"#;
let mgr = manager_with(YAML).await;
let (res, _bg) = mgr
.invoke_named::<CmfHook>(
"cmf.tool_pre_invoke",
payload(),
tool_request("locked"),
None,
)
.await;
assert!(!res.continue_processing, "tool policy must deny");
let v = res.violation.expect("deny must surface a violation");
assert_eq!(
v.details.get(DETAIL_HTTP_STATUS),
Some(&serde_json::json!(401))
);
assert_eq!(
v.details.get(DETAIL_HTTP_BODY),
Some(&serde_json::json!("route"))
);
}