mod common;
use std::time::Duration;
use common::*;
use openidauthzen::*;
#[tokio::test]
async fn discover_should_reject_http_url() {
let (client, _) = make_client(vec![]);
let result = client.discover("http://pdp.example.com").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("https"));
}
#[tokio::test]
async fn discover_should_reject_url_with_query() {
let (client, _) = make_client(vec![]);
let result = client.discover("https://pdp.example.com?foo=bar").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("query"));
}
#[tokio::test]
async fn discover_should_reject_url_with_fragment() {
let (client, _) = make_client(vec![]);
let result = client.discover("https://pdp.example.com#frag").await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("fragment"));
}
#[tokio::test]
async fn discover_should_reject_invalid_url() {
let (client, _) = make_client(vec![]);
let result = client.discover("not a url").await;
assert!(result.is_err());
}
#[tokio::test]
async fn discover_should_accept_valid_https_url() {
let body = mock_pdp_config_json(PDP_ID);
let (client, _) = make_client(vec![ok_response(&body)]);
let config = client.discover(PDP_ID).await.unwrap();
assert_eq!(config.policy_decision_point, PDP_ID);
}
#[tokio::test]
async fn discover_should_construct_well_known_url() {
let body = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client(vec![ok_response(&body)]);
client.discover(PDP_ID).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].method, "GET");
assert_eq!(
reqs[0].url,
"https://pdp.example.com/.well-known/authzen-configuration"
);
}
#[tokio::test]
async fn discover_should_strip_trailing_slash_from_pdp_id() {
let pdp_with_slash = "https://pdp.example.com/";
let pdp_without_slash = "https://pdp.example.com";
let body = mock_pdp_config_json(pdp_without_slash);
let (client, requests) = make_client(vec![ok_response(&body)]);
client.discover(pdp_with_slash).await.unwrap();
let reqs = requests.read().await;
assert_eq!(
reqs[0].url,
"https://pdp.example.com/.well-known/authzen-configuration"
);
}
#[tokio::test]
async fn discover_should_succeed_when_pdp_matches() {
let body = mock_pdp_config_json(PDP_ID);
let (client, _) = make_client(vec![ok_response(&body)]);
let config = client.discover(PDP_ID).await.unwrap();
assert_eq!(config.policy_decision_point, PDP_ID);
}
#[tokio::test]
async fn discover_should_succeed_when_pdp_matches_with_trailing_slash() {
let body = mock_pdp_config_json("https://pdp.example.com/");
let (client, _) = make_client(vec![ok_response(&body)]);
let config = client.discover(PDP_ID).await.unwrap();
assert_eq!(config.policy_decision_point, "https://pdp.example.com/");
}
#[tokio::test]
async fn discover_should_fail_when_pdp_mismatches() {
let body = mock_pdp_config_json("https://other.example.com");
let (client, _) = make_client(vec![ok_response(&body)]);
let result = client.discover(PDP_ID).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("mismatch"));
}
#[tokio::test]
async fn discover_should_return_http_status_error_on_404() {
let (client, _) = make_client(vec![error_response(404, "Not Found")]);
let result = client.discover(PDP_ID).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("404"));
}
#[tokio::test]
async fn discover_should_return_invalid_response_on_bad_json() {
let (client, _) = make_client(vec![ok_response(b"not json")]);
let result = client.discover(PDP_ID).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid"));
}
#[tokio::test]
async fn discover_should_cache_configuration() {
let body = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client(vec![ok_response(&body)]);
client.discover(PDP_ID).await.unwrap();
let config = client.get_pdp_config(PDP_ID).await.unwrap();
assert_eq!(config.policy_decision_point, PDP_ID);
let reqs = requests.read().await;
assert_eq!(reqs.len(), 1);
}
#[tokio::test]
async fn get_pdp_config_should_return_cached_without_http_call() {
let body = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client(vec![ok_response(&body)]);
client.discover(PDP_ID).await.unwrap();
let config = client.get_pdp_config(PDP_ID).await.unwrap();
assert_eq!(config.policy_decision_point, PDP_ID);
let reqs = requests.read().await;
assert_eq!(reqs.len(), 1);
}
#[tokio::test]
async fn get_pdp_config_should_refetch_when_not_cached() {
let body = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client(vec![ok_response(&body)]);
let config = client.get_pdp_config(PDP_ID).await.unwrap();
assert_eq!(config.policy_decision_point, PDP_ID);
let reqs = requests.read().await;
assert_eq!(reqs.len(), 1);
assert_eq!(reqs[0].method, "GET");
}
#[tokio::test]
async fn cache_should_expire_after_ttl() {
let body1 = mock_pdp_config_json(PDP_ID);
let body2 = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client_with_ttl(
vec![ok_response(&body1), ok_response(&body2)],
Duration::from_millis(50),
);
client.discover(PDP_ID).await.unwrap();
tokio::time::sleep(Duration::from_millis(100)).await;
client.get_pdp_config(PDP_ID).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs.len(), 2);
}
#[tokio::test]
async fn discover_should_send_x_request_id_header() {
let body = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client(vec![ok_response(&body)]);
client.discover(PDP_ID).await.unwrap();
let reqs = requests.read().await;
let has_request_id = reqs[0].headers.iter().any(|(k, _)| k == "x-request-id");
assert!(has_request_id);
}
#[tokio::test]
async fn evaluate_should_send_x_request_id_header() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let eval_body = serde_json::to_vec(&EvaluationResponse { decision: true, context: None }).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&eval_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
client.evaluate(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
let eval_req = &reqs[1];
let has_request_id = eval_req.headers.iter().any(|(k, _)| k == "x-request-id");
assert!(has_request_id);
}
#[tokio::test]
async fn invalidate_pdp_config_should_return_true_when_cached() {
let body = mock_pdp_config_json(PDP_ID);
let (client, _) = make_client(vec![ok_response(&body)]);
client.discover(PDP_ID).await.unwrap();
assert!(client.invalidate_pdp_config(PDP_ID).await);
}
#[tokio::test]
async fn invalidate_pdp_config_should_return_false_when_not_cached() {
let (client, _) = make_client(vec![]);
assert!(!client.invalidate_pdp_config(PDP_ID).await);
}
#[tokio::test]
async fn invalidate_pdp_config_should_force_refetch() {
let body1 = mock_pdp_config_json(PDP_ID);
let body2 = mock_pdp_config_json(PDP_ID);
let (client, requests) = make_client(vec![ok_response(&body1), ok_response(&body2)]);
client.discover(PDP_ID).await.unwrap();
client.invalidate_pdp_config(PDP_ID).await;
client.get_pdp_config(PDP_ID).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs.len(), 2);
}
#[tokio::test]
async fn evaluate_should_post_to_access_evaluation_endpoint() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let eval_body = serde_json::to_vec(&EvaluationResponse { decision: true, context: None }).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&eval_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
client.evaluate(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].method, "POST");
assert_eq!(reqs[1].url, format!("{}/access/v1/evaluation", PDP_ID));
}
#[tokio::test]
async fn evaluate_should_send_bearer_token() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let eval_body = serde_json::to_vec(&EvaluationResponse { decision: true, context: None }).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&eval_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
client.evaluate(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
let auth_header = reqs[1].headers.iter().find(|(k, _)| k == "authorization").unwrap();
assert_eq!(auth_header.1, format!("Bearer {}", TOKEN));
}
#[tokio::test]
async fn evaluate_should_deserialize_permit_response() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let eval_body = serde_json::to_vec(&EvaluationResponse { decision: true, context: None }).unwrap();
let (client, _) = make_client(vec![ok_response(&discovery_body), ok_response(&eval_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
let resp = client.evaluate(PDP_ID, TOKEN, &req).await.unwrap();
assert!(resp.decision);
}
#[tokio::test]
async fn evaluate_should_deserialize_deny_response() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let eval_body = serde_json::to_vec(&EvaluationResponse {
decision: false,
context: Some(serde_json::json!({"reason": "denied"})),
}).unwrap();
let (client, _) = make_client(vec![ok_response(&discovery_body), ok_response(&eval_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
let resp = client.evaluate(PDP_ID, TOKEN, &req).await.unwrap();
assert!(!resp.decision);
assert_eq!(resp.context.as_ref().unwrap()["reason"], "denied");
}
#[tokio::test]
async fn evaluate_should_return_not_cached_without_discover() {
let (client, _) = make_client(vec![]);
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
let result = client.evaluate(PDP_ID, TOKEN, &req).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not cached"));
}
#[tokio::test]
async fn evaluate_should_return_http_status_error_on_403() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let (client, _) = make_client(vec![
ok_response(&discovery_body),
error_response(403, "Forbidden"),
]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
let result = client.evaluate(PDP_ID, TOKEN, &req).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("403"));
}
#[tokio::test]
async fn evaluate_should_return_invalid_response_on_bad_json() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let (client, _) = make_client(vec![
ok_response(&discovery_body),
ok_response(b"not json"),
]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationRequest {
subject: sample_subject(),
action: sample_action(),
resource: sample_resource(),
context: None,
};
let result = client.evaluate(PDP_ID, TOKEN, &req).await;
assert!(result.is_err());
}
#[tokio::test]
async fn evaluate_batch_should_post_to_evaluations_endpoint() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let batch_body = serde_json::to_vec(&EvaluationsResponse {
decision: None,
evaluations: vec![EvaluationResponse { decision: true, context: None }],
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&batch_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationsRequest {
subject: Some(sample_subject()),
action: Some(sample_action()),
resource: None,
context: None,
evaluations: vec![EvaluationItem {
subject: None,
action: None,
resource: Some(sample_resource()),
context: None,
}],
options: None,
};
client.evaluate_batch(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].method, "POST");
assert_eq!(reqs[1].url, format!("{}/access/v1/evaluations", PDP_ID));
}
#[tokio::test]
async fn evaluate_batch_should_use_default_path_when_endpoint_not_advertised() {
let discovery_body = mock_pdp_config_minimal_json(PDP_ID);
let batch_body = serde_json::to_vec(&EvaluationsResponse {
decision: None,
evaluations: vec![],
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&batch_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationsRequest {
subject: None, action: None, resource: None, context: None,
evaluations: vec![],
options: None,
};
client.evaluate_batch(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, format!("{}/access/v1/evaluations", PDP_ID));
}
#[tokio::test]
async fn evaluate_batch_should_deserialize_batch_response() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let batch_body = serde_json::to_vec(&EvaluationsResponse {
decision: None,
evaluations: vec![
EvaluationResponse { decision: true, context: None },
EvaluationResponse { decision: false, context: None },
],
}).unwrap();
let (client, _) = make_client(vec![ok_response(&discovery_body), ok_response(&batch_body)]);
client.discover(PDP_ID).await.unwrap();
let req = EvaluationsRequest {
subject: None, action: None, resource: None, context: None,
evaluations: vec![],
options: None,
};
let resp = client.evaluate_batch(PDP_ID, TOKEN, &req).await.unwrap();
assert_eq!(resp.evaluations.len(), 2);
assert!(resp.evaluations[0].decision);
assert!(!resp.evaluations[1].decision);
}
#[tokio::test]
async fn evaluate_batch_should_return_not_cached_without_discover() {
let (client, _) = make_client(vec![]);
let req = EvaluationsRequest {
subject: None, action: None, resource: None, context: None,
evaluations: vec![],
options: None,
};
let result = client.evaluate_batch(PDP_ID, TOKEN, &req).await;
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("not cached"));
}
#[tokio::test]
async fn search_subjects_should_post_to_search_subject_endpoint() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let search_body = serde_json::to_vec(&SubjectSearchResponse {
results: vec![sample_subject()],
page: None,
context: None,
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = SubjectSearchRequest {
subject: Subject { subject_type: "user".to_owned(), id: None, properties: None },
action: sample_action(),
resource: sample_resource(),
context: None,
page: None,
};
client.search_subjects(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].method, "POST");
assert_eq!(reqs[1].url, format!("{}/access/v1/search/subject", PDP_ID));
}
#[tokio::test]
async fn search_subjects_should_use_default_path_when_not_advertised() {
let discovery_body = mock_pdp_config_minimal_json(PDP_ID);
let search_body = serde_json::to_vec(&SubjectSearchResponse {
results: vec![],
page: None,
context: None,
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = SubjectSearchRequest {
subject: Subject { subject_type: "user".to_owned(), id: None, properties: None },
action: sample_action(),
resource: sample_resource(),
context: None,
page: None,
};
client.search_subjects(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, format!("{}/access/v1/search/subject", PDP_ID));
}
#[tokio::test]
async fn search_subjects_should_deserialize_response() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let search_body = serde_json::to_vec(&SubjectSearchResponse {
results: vec![
Subject { subject_type: "user".to_owned(), id: Some("alice@example.com".to_owned()), properties: None },
Subject { subject_type: "user".to_owned(), id: Some("bob@example.com".to_owned()), properties: None },
],
page: None,
context: None,
}).unwrap();
let (client, _) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = SubjectSearchRequest {
subject: Subject { subject_type: "user".to_owned(), id: None, properties: None },
action: sample_action(),
resource: sample_resource(),
context: None,
page: None,
};
let resp = client.search_subjects(PDP_ID, TOKEN, &req).await.unwrap();
assert_eq!(resp.results.len(), 2);
assert_eq!(resp.results[0].id.as_deref(), Some("alice@example.com"));
}
#[tokio::test]
async fn search_resources_should_post_to_search_resource_endpoint() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let search_body = serde_json::to_vec(&ResourceSearchResponse {
results: vec![],
page: None,
context: None,
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = ResourceSearchRequest {
subject: sample_subject(),
action: sample_action(),
resource: Resource { resource_type: "account".to_owned(), id: None, properties: None },
context: None,
page: None,
};
client.search_resources(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, format!("{}/access/v1/search/resource", PDP_ID));
}
#[tokio::test]
async fn search_resources_should_use_default_path_when_not_advertised() {
let discovery_body = mock_pdp_config_minimal_json(PDP_ID);
let search_body = serde_json::to_vec(&ResourceSearchResponse {
results: vec![],
page: None,
context: None,
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = ResourceSearchRequest {
subject: sample_subject(),
action: sample_action(),
resource: Resource { resource_type: "account".to_owned(), id: None, properties: None },
context: None,
page: None,
};
client.search_resources(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, format!("{}/access/v1/search/resource", PDP_ID));
}
#[tokio::test]
async fn search_resources_should_deserialize_response() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let search_body = serde_json::to_vec(&ResourceSearchResponse {
results: vec![
Resource { resource_type: "account".to_owned(), id: Some("123".to_owned()), properties: None },
],
page: None,
context: None,
}).unwrap();
let (client, _) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = ResourceSearchRequest {
subject: sample_subject(),
action: sample_action(),
resource: Resource { resource_type: "account".to_owned(), id: None, properties: None },
context: None,
page: None,
};
let resp = client.search_resources(PDP_ID, TOKEN, &req).await.unwrap();
assert_eq!(resp.results.len(), 1);
assert_eq!(resp.results[0].id.as_deref(), Some("123"));
}
#[tokio::test]
async fn search_actions_should_post_to_search_action_endpoint() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let search_body = serde_json::to_vec(&ActionSearchResponse {
results: vec![],
page: None,
context: None,
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = ActionSearchRequest {
subject: sample_subject(),
resource: sample_resource(),
context: None,
page: None,
};
client.search_actions(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, format!("{}/access/v1/search/action", PDP_ID));
}
#[tokio::test]
async fn search_actions_should_use_default_path_when_not_advertised() {
let discovery_body = mock_pdp_config_minimal_json(PDP_ID);
let search_body = serde_json::to_vec(&ActionSearchResponse {
results: vec![],
page: None,
context: None,
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = ActionSearchRequest {
subject: sample_subject(),
resource: sample_resource(),
context: None,
page: None,
};
client.search_actions(PDP_ID, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, format!("{}/access/v1/search/action", PDP_ID));
}
#[tokio::test]
async fn search_actions_should_deserialize_response() {
let discovery_body = mock_pdp_config_json(PDP_ID);
let search_body = serde_json::to_vec(&ActionSearchResponse {
results: vec![
Action { name: "can_read".to_owned(), properties: None },
Action { name: "can_write".to_owned(), properties: None },
],
page: None,
context: None,
}).unwrap();
let (client, _) = make_client(vec![ok_response(&discovery_body), ok_response(&search_body)]);
client.discover(PDP_ID).await.unwrap();
let req = ActionSearchRequest {
subject: sample_subject(),
resource: sample_resource(),
context: None,
page: None,
};
let resp = client.search_actions(PDP_ID, TOKEN, &req).await.unwrap();
assert_eq!(resp.results.len(), 2);
assert_eq!(resp.results[0].name, "can_read");
}
#[tokio::test]
async fn default_path_should_handle_pdp_with_subpath() {
let pdp_id = "https://pdp.example.com/api";
let discovery_body = mock_pdp_config_minimal_json(pdp_id);
let batch_body = serde_json::to_vec(&EvaluationsResponse {
decision: None,
evaluations: vec![],
}).unwrap();
let (client, requests) = make_client(vec![ok_response(&discovery_body), ok_response(&batch_body)]);
client.discover(pdp_id).await.unwrap();
let req = EvaluationsRequest {
subject: None, action: None, resource: None, context: None,
evaluations: vec![],
options: None,
};
client.evaluate_batch(pdp_id, TOKEN, &req).await.unwrap();
let reqs = requests.read().await;
assert_eq!(reqs[1].url, "https://pdp.example.com/api/access/v1/evaluations");
}