Skip to content

Commit fae10f6

Browse files
committed
Merge #217: Axum HTTP tracker: add unit tests for custom Axum extractors
b4ae67d docs(http): add mod description (Jose Celano) 743f869 refactor(http): move peer IP resolver to handlers mod (Jose Celano) 3420576 test(http): add tests for peer IP resolution (Jose Celano) 828065b test(http): add tests to Axum extractor for auth key (Jose Celano) 7b31622 test(http): add tests to axum extractor for scrape request (Jose Celano) 6fc6c14 test(http): add tests to axum extractor for announce request (Jose Celano) Pull request description: Top commit has no ACKs. Tree-SHA512: 7144f6266f5c685d5b05b400b694465e077afc95f5018957854c15cf37b89863c66d797bd833e92b9fe55d56c50b35f8c71c11f73cfc6e65719528c0eaa0fd38
2 parents 6d83d9b + b4ae67d commit fae10f6

File tree

12 files changed

+437
-121
lines changed

12 files changed

+437
-121
lines changed

src/http/axum_implementation/extractors/announce_request.rs

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,95 @@ where
1919
type Rejection = Response;
2020

2121
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
22-
let raw_query = parts.uri.query();
23-
24-
if raw_query.is_none() {
25-
return Err(responses::error::Error::from(ParseAnnounceQueryError::MissingParams {
26-
location: Location::caller(),
27-
})
28-
.into_response());
22+
match extract_announce_from(parts.uri.query()) {
23+
Ok(announce_request) => Ok(ExtractRequest(announce_request)),
24+
Err(error) => Err(error.into_response()),
2925
}
26+
}
27+
}
3028

31-
let query = raw_query.unwrap().parse::<Query>();
29+
fn extract_announce_from(maybe_raw_query: Option<&str>) -> Result<Announce, responses::error::Error> {
30+
if maybe_raw_query.is_none() {
31+
return Err(responses::error::Error::from(ParseAnnounceQueryError::MissingParams {
32+
location: Location::caller(),
33+
}));
34+
}
3235

33-
if let Err(error) = query {
34-
return Err(responses::error::Error::from(error).into_response());
35-
}
36+
let query = maybe_raw_query.unwrap().parse::<Query>();
3637

37-
let announce_request = Announce::try_from(query.unwrap());
38+
if let Err(error) = query {
39+
return Err(responses::error::Error::from(error));
40+
}
3841

39-
if let Err(error) = announce_request {
40-
return Err(responses::error::Error::from(error).into_response());
41-
}
42+
let announce_request = Announce::try_from(query.unwrap());
43+
44+
if let Err(error) = announce_request {
45+
return Err(responses::error::Error::from(error));
46+
}
47+
48+
Ok(announce_request.unwrap())
49+
}
50+
51+
#[cfg(test)]
52+
mod tests {
53+
use std::str::FromStr;
54+
55+
use super::extract_announce_from;
56+
use crate::http::axum_implementation::requests::announce::{Announce, Compact, Event};
57+
use crate::http::axum_implementation::responses::error::Error;
58+
use crate::protocol::info_hash::InfoHash;
59+
use crate::tracker::peer;
60+
61+
fn assert_error_response(error: &Error, error_message: &str) {
62+
assert!(
63+
error.failure_reason.contains(error_message),
64+
"Error response does not contain message: '{error_message}'. Error: {error:?}"
65+
);
66+
}
67+
68+
#[test]
69+
fn it_should_extract_the_announce_request_from_the_url_query_params() {
70+
let raw_query = "info_hash=%3B%24U%04%CF%5F%11%BB%DB%E1%20%1C%EAjk%F4Z%EE%1B%C0&peer_addr=2.137.87.41&downloaded=0&uploaded=0&peer_id=-qB00000000000000001&port=17548&left=0&event=completed&compact=0";
71+
72+
let announce = extract_announce_from(Some(raw_query)).unwrap();
73+
74+
assert_eq!(
75+
announce,
76+
Announce {
77+
info_hash: InfoHash::from_str("3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0").unwrap(),
78+
peer_id: peer::Id(*b"-qB00000000000000001"),
79+
port: 17548,
80+
downloaded: Some(0),
81+
uploaded: Some(0),
82+
left: Some(0),
83+
event: Some(Event::Completed),
84+
compact: Some(Compact::NotAccepted),
85+
}
86+
);
87+
}
88+
89+
#[test]
90+
fn it_should_reject_a_request_without_query_params() {
91+
let response = extract_announce_from(None).unwrap_err();
92+
93+
assert_error_response(
94+
&response,
95+
"Cannot parse query params for announce request: missing query params for announce request",
96+
);
97+
}
98+
99+
#[test]
100+
fn it_should_reject_a_request_with_a_query_that_cannot_be_parsed() {
101+
let invalid_query = "param1=value1=value2";
102+
let response = extract_announce_from(Some(invalid_query)).unwrap_err();
103+
104+
assert_error_response(&response, "Cannot parse query params");
105+
}
106+
107+
#[test]
108+
fn it_should_reject_a_request_with_a_query_that_cannot_be_parsed_into_an_announce_request() {
109+
let response = extract_announce_from(Some("param1=value1")).unwrap_err();
42110

43-
Ok(ExtractRequest(announce_request.unwrap()))
111+
assert_error_response(&response, "Cannot parse query params for announce request");
44112
}
45113
}
Lines changed: 70 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
//! Wrapper for Axum `Path` extractor to return custom errors.
12
use std::panic::Location;
23

34
use axum::async_trait;
5+
use axum::extract::rejection::PathRejection;
46
use axum::extract::{FromRequestParts, Path};
57
use axum::http::request::Parts;
68
use axum::response::{IntoResponse, Response};
@@ -19,37 +21,74 @@ where
1921
type Rejection = Response;
2022

2123
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
22-
match Path::<KeyParam>::from_request_parts(parts, state).await {
23-
Ok(key_param) => {
24-
let Ok(key) = key_param.0.value().parse::<Key>() else {
25-
return Err(responses::error::Error::from(
26-
auth::Error::InvalidKeyFormat {
27-
location: Location::caller()
28-
})
29-
.into_response())
30-
};
31-
Ok(Extract(key))
32-
}
33-
Err(rejection) => match rejection {
34-
axum::extract::rejection::PathRejection::FailedToDeserializePathParams(_) => {
35-
return Err(responses::error::Error::from(auth::Error::InvalidKeyFormat {
36-
location: Location::caller(),
37-
})
38-
.into_response())
39-
}
40-
axum::extract::rejection::PathRejection::MissingPathParams(_) => {
41-
return Err(responses::error::Error::from(auth::Error::MissingAuthKey {
42-
location: Location::caller(),
43-
})
44-
.into_response())
45-
}
46-
_ => {
47-
return Err(responses::error::Error::from(auth::Error::CannotExtractKeyParam {
48-
location: Location::caller(),
49-
})
50-
.into_response())
51-
}
52-
},
24+
// Extract `key` from URL path with Axum `Path` extractor
25+
let maybe_path_with_key = Path::<KeyParam>::from_request_parts(parts, state).await;
26+
27+
match extract_key(maybe_path_with_key) {
28+
Ok(key) => Ok(Extract(key)),
29+
Err(error) => Err(error.into_response()),
30+
}
31+
}
32+
}
33+
34+
fn extract_key(path_extractor_result: Result<Path<KeyParam>, PathRejection>) -> Result<Key, responses::error::Error> {
35+
match path_extractor_result {
36+
Ok(key_param) => match parse_key(&key_param.0.value()) {
37+
Ok(key) => Ok(key),
38+
Err(error) => Err(error),
39+
},
40+
Err(path_rejection) => Err(custom_error(&path_rejection)),
41+
}
42+
}
43+
44+
fn parse_key(key: &str) -> Result<Key, responses::error::Error> {
45+
let key = key.parse::<Key>();
46+
47+
match key {
48+
Ok(key) => Ok(key),
49+
Err(_parse_key_error) => Err(responses::error::Error::from(auth::Error::InvalidKeyFormat {
50+
location: Location::caller(),
51+
})),
52+
}
53+
}
54+
55+
fn custom_error(rejection: &PathRejection) -> responses::error::Error {
56+
match rejection {
57+
axum::extract::rejection::PathRejection::FailedToDeserializePathParams(_) => {
58+
responses::error::Error::from(auth::Error::InvalidKeyFormat {
59+
location: Location::caller(),
60+
})
61+
}
62+
axum::extract::rejection::PathRejection::MissingPathParams(_) => {
63+
responses::error::Error::from(auth::Error::MissingAuthKey {
64+
location: Location::caller(),
65+
})
5366
}
67+
_ => responses::error::Error::from(auth::Error::CannotExtractKeyParam {
68+
location: Location::caller(),
69+
}),
70+
}
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
76+
use super::parse_key;
77+
use crate::http::axum_implementation::responses::error::Error;
78+
79+
fn assert_error_response(error: &Error, error_message: &str) {
80+
assert!(
81+
error.failure_reason.contains(error_message),
82+
"Error response does not contain message: '{error_message}'. Error: {error:?}"
83+
);
84+
}
85+
86+
#[test]
87+
fn it_should_return_an_authentication_error_if_the_key_cannot_be_parsed() {
88+
let invalid_key = "invalid_key";
89+
90+
let response = parse_key(invalid_key).unwrap_err();
91+
92+
assert_error_response(&response, "Authentication error: Invalid format for authentication key param");
5493
}
5594
}
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
pub mod announce_request;
22
pub mod key;
3-
pub mod peer_ip;
43
pub mod remote_client_ip;
54
pub mod scrape_request;

src/http/axum_implementation/extractors/peer_ip.rs

Lines changed: 0 additions & 54 deletions
This file was deleted.

src/http/axum_implementation/extractors/remote_client_ip.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
//! Wrapper for two Axum extractors to get the relevant information
2+
//! to resolve the remote client IP.
13
use std::net::{IpAddr, SocketAddr};
24

35
use axum::async_trait;
@@ -18,7 +20,7 @@ use serde::{Deserialize, Serialize};
1820
/// `right_most_x_forwarded_for` = 126.0.0.2
1921
/// `connection_info_ip` = 126.0.0.3
2022
///
21-
/// More info about inner extractors :<https://github.com/imbolc/axum-client-ip>
23+
/// More info about inner extractors: <https://github.com/imbolc/axum-client-ip>
2224
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
2325
pub struct RemoteClientIp {
2426
pub right_most_x_forwarded_for: Option<IpAddr>,

0 commit comments

Comments
 (0)