Skip to content

Commit 9490b33

Browse files
authored
Merge pull request #944 from Onomebello/chore/address-review-findings-request-id-docs-magic-numbers
Add request_id middleware test, doc comments, and validation constants
2 parents 0f17747 + f75ac23 commit 9490b33

3 files changed

Lines changed: 98 additions & 13 deletions

File tree

contracts/router-core/src/lib.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1033,6 +1033,23 @@ impl RouterCore {
10331033
/// The dependency is stored as a direct prerequisite for `route`. The
10341034
/// dependency must already exist, and adding the edge must not introduce a
10351035
/// cycle. Caller must be the admin.
1036+
///
1037+
/// # Arguments
1038+
/// * `env` - The Soroban environment.
1039+
/// * `caller` - The address initiating the call; must be the admin.
1040+
/// * `route` - The route that will depend on `depends_on`.
1041+
/// * `depends_on` - The route that must exist and resolve before `route`.
1042+
///
1043+
/// # Returns
1044+
/// `Ok(())` on success.
1045+
///
1046+
/// # Errors
1047+
/// * [`RouterError::Unauthorized`] — if `caller` is not the admin.
1048+
/// * [`RouterError::RouteNotFound`] — if `route` or `depends_on` does not exist.
1049+
/// * [`RouterError::CircularDependency`] — if `route` and `depends_on` are the
1050+
/// same, or if adding the edge would create a dependency cycle.
1051+
/// * [`RouterError::RecursionLimitExceeded`] — if the existing dependency
1052+
/// graph is too deep to safely validate for cycles.
10361053
pub fn set_route_dependency(
10371054
env: Env,
10381055
caller: Address,
@@ -1081,6 +1098,17 @@ impl RouterCore {
10811098
}
10821099

10831100
/// Return the direct dependencies for a route.
1101+
///
1102+
/// # Arguments
1103+
/// * `env` - The Soroban environment.
1104+
/// * `route` - The name of the route to look up.
1105+
///
1106+
/// # Returns
1107+
/// A [`Vec<String>`] of the route names that `route` directly depends on,
1108+
/// in the order they were added. Empty if `route` has no dependencies.
1109+
///
1110+
/// # Errors
1111+
/// * [`RouterError::RouteNotFound`] — if `route` does not exist.
10841112
pub fn get_route_dependencies(env: Env, route: String) -> Result<Vec<String>, RouterError> {
10851113
router_common::extend_instance_ttl(&env, INSTANCE_TTL_THRESHOLD, INSTANCE_TTL_EXTEND_TO);
10861114
if !env.storage().instance().has(&DataKey::Route(route.clone())) {
@@ -1091,6 +1119,26 @@ impl RouterCore {
10911119
}
10921120

10931121
/// Resolve a route together with all of its dependencies in dependency-first order.
1122+
///
1123+
/// Walks the dependency graph rooted at `name` depth-first and returns each
1124+
/// resolved route paired with its address, ordered so that every
1125+
/// dependency appears before the routes that depend on it.
1126+
///
1127+
/// # Arguments
1128+
/// * `env` - The Soroban environment.
1129+
/// * `name` - The name of the route to resolve, along with its dependencies.
1130+
///
1131+
/// # Returns
1132+
/// A [`Vec<(String, Address)>`] of `(route_name, address)` pairs in
1133+
/// dependency-first order, including `name` itself last.
1134+
///
1135+
/// # Errors
1136+
/// * [`RouterError::RouterPaused`] — if the router is globally paused.
1137+
/// * [`RouterError::RouteNotFound`] — if `name` or any of its dependencies
1138+
/// does not exist.
1139+
/// * [`RouterError::RoutePaused`] — if `name` or any of its dependencies is paused.
1140+
/// * [`RouterError::CircularDependency`] — if the dependency graph contains a cycle.
1141+
/// * [`RouterError::RecursionLimitExceeded`] — if the dependency graph is too deep.
10941142
pub fn resolve_with_dependencies(
10951143
env: Env,
10961144
name: String,

metrics/src/server.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,37 @@ mod tests {
305305
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
306306
}
307307

308+
#[tokio::test]
309+
async fn test_request_id_middleware_sets_response_header() {
310+
let registry = Registry::new();
311+
let state = AppState { registry };
312+
let app = Router::new()
313+
.route("/health", get(health_handler))
314+
.layer(middleware::from_fn(request_id_middleware))
315+
.with_state(state);
316+
317+
let response = app
318+
.oneshot(
319+
Request::builder()
320+
.uri("/health")
321+
.body(Body::empty())
322+
.unwrap(),
323+
)
324+
.await
325+
.unwrap();
326+
327+
assert_eq!(response.status(), StatusCode::OK);
328+
let request_id = response
329+
.headers()
330+
.get("x-request-id")
331+
.and_then(|v| v.to_str().ok())
332+
.unwrap_or("");
333+
assert!(
334+
!request_id.is_empty(),
335+
"expected non-empty x-request-id header"
336+
);
337+
}
338+
308339
#[tokio::test]
309340
async fn test_ready_returns_200_when_router_up_is_one() {
310341
let registry = Registry::new();

metrics/src/validation.rs

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -35,15 +35,21 @@ impl ValidationError {
3535

3636
// ── Validation rules ──────────────────────────────────────────────────────────
3737

38-
/// Validate a contract ID: must be a 56-character alphanumeric Stellar address.
38+
/// Required length of a Stellar contract ID (a base32-style strkey/address string).
39+
const STELLAR_CONTRACT_ID_LEN: usize = 56;
40+
41+
/// Maximum allowed length of a route name.
42+
const MAX_ROUTE_NAME_LEN: usize = 64;
43+
44+
/// Validate a contract ID: must be a `STELLAR_CONTRACT_ID_LEN`-character alphanumeric Stellar address.
3945
pub fn validate_contract_id(id: &str) -> Result<(), ValidationError> {
4046
if id.is_empty() {
4147
return Err(ValidationError::new("contract_id must not be empty"));
4248
}
43-
if id.len() != 56 {
44-
return Err(ValidationError::new(
45-
"contract_id must be exactly 56 characters",
46-
));
49+
if id.len() != STELLAR_CONTRACT_ID_LEN {
50+
return Err(ValidationError::new(format!(
51+
"contract_id must be exactly {STELLAR_CONTRACT_ID_LEN} characters"
52+
)));
4753
}
4854
if !id.chars().all(|c| c.is_ascii_alphanumeric()) {
4955
return Err(ValidationError::new(
@@ -53,7 +59,7 @@ pub fn validate_contract_id(id: &str) -> Result<(), ValidationError> {
5359
Ok(())
5460
}
5561

56-
/// Validate a route name: non-empty, max 64 chars, alphanumeric + underscore/hyphen.
62+
/// Validate a route name: non-empty, max `MAX_ROUTE_NAME_LEN` chars, alphanumeric + underscore/hyphen.
5763
///
5864
/// Not yet called from any handler — kept for validating route-name query
5965
/// params once an endpoint accepts one.
@@ -62,10 +68,10 @@ pub fn validate_route_name(name: &str) -> Result<(), ValidationError> {
6268
if name.is_empty() {
6369
return Err(ValidationError::new("route name must not be empty"));
6470
}
65-
if name.len() > 64 {
66-
return Err(ValidationError::new(
67-
"route name must be 64 characters or fewer",
68-
));
71+
if name.len() > MAX_ROUTE_NAME_LEN {
72+
return Err(ValidationError::new(format!(
73+
"route name must be {MAX_ROUTE_NAME_LEN} characters or fewer"
74+
)));
6975
}
7076
if !name
7177
.chars()
@@ -108,7 +114,7 @@ mod tests {
108114

109115
#[test]
110116
fn valid_contract_id() {
111-
let id = "A".repeat(56);
117+
let id = "A".repeat(STELLAR_CONTRACT_ID_LEN);
112118
assert!(validate_contract_id(&id).is_ok());
113119
}
114120

@@ -124,7 +130,7 @@ mod tests {
124130

125131
#[test]
126132
fn contract_id_with_special_chars_rejected() {
127-
let id = format!("{}!", "A".repeat(55));
133+
let id = format!("{}!", "A".repeat(STELLAR_CONTRACT_ID_LEN - 1));
128134
assert!(validate_contract_id(&id).is_err());
129135
}
130136

@@ -140,7 +146,7 @@ mod tests {
140146

141147
#[test]
142148
fn long_route_name_rejected() {
143-
assert!(validate_route_name(&"a".repeat(65)).is_err());
149+
assert!(validate_route_name(&"a".repeat(MAX_ROUTE_NAME_LEN + 1)).is_err());
144150
}
145151

146152
#[test]

0 commit comments

Comments
 (0)