feat: complete Epic 1 production foundation
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use axum::{
|
||||
Json, Router,
|
||||
@@ -9,7 +9,8 @@ use axum::{
|
||||
};
|
||||
use crank_adapter_rest::{OutboundHttpPolicy, RestAdapter, RestAdapterError, RestRequest};
|
||||
use crank_core::{
|
||||
HttpMethod, PreparedRequest, ProtocolAdapter, RestTarget, RuntimeRequestContext, Target,
|
||||
DispatchEvidence, HttpMethod, PreparedRequest, ProtocolAdapter, ProtocolAdapterError,
|
||||
RestTarget, RuntimeRequestContext, Target,
|
||||
};
|
||||
use opentelemetry::{
|
||||
global,
|
||||
@@ -38,6 +39,7 @@ async fn executes_rest_request_and_normalizes_json_response() {
|
||||
headers: BTreeMap::from([("x-trace-id".to_owned(), "trace-123".to_owned())]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
};
|
||||
|
||||
let response = adapter.execute(&target, &request).await.unwrap();
|
||||
@@ -55,6 +57,77 @@ async fn executes_rest_request_and_normalizes_json_response() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn prepared_auth_profile_header_is_allowed_after_runtime_resolution() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = test_adapter();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/users/{user_id}".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::from([("user_id".to_owned(), "42".to_owned())]),
|
||||
query_params: BTreeMap::new(),
|
||||
headers: BTreeMap::from([(
|
||||
"authorization".to_owned(),
|
||||
"Bearer resolved-auth-profile-token".to_owned(),
|
||||
)]),
|
||||
trusted_header_names: BTreeSet::from(["authorization".to_owned()]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
};
|
||||
|
||||
let response = adapter.execute(&target, &request).await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
response.body["authorization"],
|
||||
"Bearer resolved-auth-profile-token"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn untrusted_prepared_credential_header_is_rejected_before_dispatch() {
|
||||
let base_url = spawn_test_server().await;
|
||||
let adapter = test_adapter();
|
||||
let target = RestTarget {
|
||||
base_url,
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/users/{user_id}".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::from([("user_id".to_owned(), "42".to_owned())]),
|
||||
query_params: BTreeMap::new(),
|
||||
headers: BTreeMap::from([(
|
||||
"authorization".to_owned(),
|
||||
"Bearer user-mapped-secret".to_owned(),
|
||||
)]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
};
|
||||
|
||||
let error = adapter.execute(&target, &request).await.unwrap_err();
|
||||
|
||||
assert!(matches!(error, RestAdapterError::InvalidHeaderName { .. }));
|
||||
assert!(!format!("{error:?} {error}").contains("user-mapped-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transport_error_debug_is_redacted() {
|
||||
let error = RestAdapterError::Transport {
|
||||
timeout: false,
|
||||
connect: true,
|
||||
};
|
||||
let rendered = format!("{error:?} {error}");
|
||||
|
||||
assert!(!rendered.contains("http://"));
|
||||
assert!(!rendered.contains("story19-transport-canary"));
|
||||
assert!(rendered.contains("Transport"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn protocol_context_overrides_mapped_correlation_headers() {
|
||||
let base_url = spawn_test_server().await;
|
||||
@@ -134,6 +207,7 @@ async fn current_trace_context_overrides_mapped_traceparent() {
|
||||
)]),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
};
|
||||
|
||||
let response = test_adapter()
|
||||
@@ -171,6 +245,7 @@ async fn user_configured_propagation_headers_are_removed_without_trusted_context
|
||||
headers: BTreeMap::new(),
|
||||
body: Some(json!({ "name": "Ada" })),
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
};
|
||||
|
||||
let response = test_adapter().execute(&target, &request).await.unwrap();
|
||||
@@ -196,17 +271,54 @@ async fn returns_unexpected_status_with_normalized_body() {
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
};
|
||||
|
||||
let error = adapter.execute(&target, &request).await.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
&error,
|
||||
RestAdapterError::UnexpectedStatus {
|
||||
status: 502,
|
||||
body: Value::Object(_)
|
||||
}
|
||||
));
|
||||
assert!(matches!(
|
||||
ProtocolAdapterError::from(error),
|
||||
ProtocolAdapterError::UnexpectedStatus {
|
||||
status: 502,
|
||||
dispatch: DispatchEvidence::MayHaveDispatched,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn connect_failure_is_known_not_dispatched() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
drop(listener);
|
||||
let target = RestTarget {
|
||||
base_url: format!("http://{address}"),
|
||||
method: HttpMethod::Post,
|
||||
path_template: "/write".to_owned(),
|
||||
static_headers: BTreeMap::new(),
|
||||
};
|
||||
let request = RestRequest {
|
||||
path_params: BTreeMap::new(),
|
||||
query_params: BTreeMap::new(),
|
||||
headers: BTreeMap::new(),
|
||||
body: Some(json!({"value": 1})),
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
};
|
||||
|
||||
let error = test_adapter().execute(&target, &request).await.unwrap_err();
|
||||
assert!(matches!(
|
||||
ProtocolAdapterError::from(error),
|
||||
ProtocolAdapterError::Transport {
|
||||
dispatch: DispatchEvidence::NotDispatched,
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -244,10 +356,7 @@ async fn does_not_follow_redirects() {
|
||||
.await
|
||||
.unwrap_err();
|
||||
|
||||
assert!(matches!(
|
||||
error,
|
||||
RestAdapterError::UnexpectedStatus { status: 303, .. }
|
||||
));
|
||||
assert!(matches!(error, RestAdapterError::RedirectNotAllowed));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -281,6 +390,7 @@ fn empty_request() -> RestRequest {
|
||||
headers: BTreeMap::new(),
|
||||
body: None,
|
||||
timeout_ms: 1_000,
|
||||
..RestRequest::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,6 +444,9 @@ async fn create_user(
|
||||
.get("tracestate")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
let baggage = headers.get("baggage").and_then(|value| value.to_str().ok());
|
||||
let authorization = headers
|
||||
.get("authorization")
|
||||
.and_then(|value| value.to_str().ok());
|
||||
|
||||
let mut response = json!({
|
||||
"id": user_id,
|
||||
@@ -370,6 +483,12 @@ async fn create_user(
|
||||
if let Some(baggage) = baggage {
|
||||
response.insert("baggage".to_owned(), Value::String(baggage.to_owned()));
|
||||
}
|
||||
if let Some(authorization) = authorization {
|
||||
response.insert(
|
||||
"authorization".to_owned(),
|
||||
Value::String(authorization.to_owned()),
|
||||
);
|
||||
}
|
||||
|
||||
Json(Value::Object(response.clone()))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user