Skip to content

Commit fa60994

Browse files
committed
test(http): [#128] unit test for announce handler
1 parent 6ebcfcd commit fa60994

File tree

1 file changed

+257
-22
lines changed

1 file changed

+257
-22
lines changed

src/http/axum_implementation/handlers/announce.rs

Lines changed: 257 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,9 @@ use crate::http::axum_implementation::responses::{self, announce};
1616
use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources;
1717
use crate::http::axum_implementation::services::{self, peer_ip_resolver};
1818
use crate::protocol::clock::{Current, Time};
19+
use crate::tracker::auth::Key;
1920
use crate::tracker::peer::Peer;
20-
use crate::tracker::Tracker;
21-
22-
/* code-review: authentication, authorization and peer IP resolution could be moved
23-
from the handler (Axum) layer into the app layer `services::announce::invoke`.
24-
That would make the handler even simpler and the code more reusable and decoupled from Axum.
25-
*/
21+
use crate::tracker::{AnnounceData, Tracker};
2622

2723
#[allow(clippy::unused_async)]
2824
pub async fn handle_without_key(
@@ -32,14 +28,7 @@ pub async fn handle_without_key(
3228
) -> Response {
3329
debug!("http announce request: {:#?}", announce_request);
3430

35-
if tracker.requires_authentication() {
36-
return responses::error::Error::from(auth::Error::MissingAuthKey {
37-
location: Location::caller(),
38-
})
39-
.into_response();
40-
}
41-
42-
handle(&tracker, &announce_request, &client_ip_sources).await
31+
handle(&tracker, &announce_request, &client_ip_sources, None).await
4332
}
4433

4534
#[allow(clippy::unused_async)]
@@ -51,29 +40,67 @@ pub async fn handle_with_key(
5140
) -> Response {
5241
debug!("http announce request: {:#?}", announce_request);
5342

54-
match tracker.authenticate(&key).await {
55-
Ok(_) => (),
56-
Err(error) => return responses::error::Error::from(error).into_response(),
57-
}
43+
handle(&tracker, &announce_request, &client_ip_sources, Some(key)).await
44+
}
5845

59-
handle(&tracker, &announce_request, &client_ip_sources).await
46+
async fn handle(
47+
tracker: &Arc<Tracker>,
48+
announce_request: &Announce,
49+
client_ip_sources: &ClientIpSources,
50+
maybe_key: Option<Key>,
51+
) -> Response {
52+
let announce_data = match handle_announce(tracker, announce_request, client_ip_sources, maybe_key).await {
53+
Ok(announce_data) => announce_data,
54+
Err(error) => return error.into_response(),
55+
};
56+
build_response(announce_request, announce_data)
6057
}
6158

62-
async fn handle(tracker: &Arc<Tracker>, announce_request: &Announce, client_ip_sources: &ClientIpSources) -> Response {
59+
/* code-review: authentication, authorization and peer IP resolution could be moved
60+
from the handler (Axum) layer into the app layer `services::announce::invoke`.
61+
That would make the handler even simpler and the code more reusable and decoupled from Axum.
62+
*/
63+
64+
async fn handle_announce(
65+
tracker: &Arc<Tracker>,
66+
announce_request: &Announce,
67+
client_ip_sources: &ClientIpSources,
68+
maybe_key: Option<Key>,
69+
) -> Result<AnnounceData, responses::error::Error> {
70+
// Authentication
71+
if tracker.requires_authentication() {
72+
match maybe_key {
73+
Some(key) => match tracker.authenticate(&key).await {
74+
Ok(_) => (),
75+
Err(error) => return Err(responses::error::Error::from(error)),
76+
},
77+
None => {
78+
return Err(responses::error::Error::from(auth::Error::MissingAuthKey {
79+
location: Location::caller(),
80+
}))
81+
}
82+
}
83+
}
84+
85+
// Authorization
6386
match tracker.authorize(&announce_request.info_hash).await {
6487
Ok(_) => (),
65-
Err(error) => return responses::error::Error::from(error).into_response(),
88+
Err(error) => return Err(responses::error::Error::from(error)),
6689
}
6790

6891
let peer_ip = match peer_ip_resolver::invoke(tracker.config.on_reverse_proxy, client_ip_sources) {
6992
Ok(peer_ip) => peer_ip,
70-
Err(error) => return responses::error::Error::from(error).into_response(),
93+
Err(error) => return Err(responses::error::Error::from(error)),
7194
};
7295

7396
let mut peer = peer_from_request(announce_request, &peer_ip);
7497

7598
let announce_data = services::announce::invoke(tracker.clone(), announce_request.info_hash, &mut peer).await;
7699

100+
Ok(announce_data)
101+
}
102+
103+
fn build_response(announce_request: &Announce, announce_data: AnnounceData) -> Response {
77104
match &announce_request.compact {
78105
Some(compact) => match compact {
79106
Compact::Accepted => announce::Compact::from(announce_data).into_response(),
@@ -108,3 +135,211 @@ fn map_to_aquatic_event(event: &Option<Event>) -> AnnounceEvent {
108135
None => aquatic_udp_protocol::AnnounceEvent::None,
109136
}
110137
}
138+
139+
#[cfg(test)]
140+
mod tests {
141+
use std::sync::Arc;
142+
143+
use crate::config::{ephemeral_configuration, Configuration};
144+
use crate::http::axum_implementation::requests::announce::Announce;
145+
use crate::http::axum_implementation::responses;
146+
use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources;
147+
use crate::protocol::info_hash::InfoHash;
148+
use crate::tracker::mode::Mode;
149+
use crate::tracker::statistics::Keeper;
150+
use crate::tracker::{peer, Tracker};
151+
152+
fn private_tracker() -> Tracker {
153+
let mut configuration = ephemeral_configuration();
154+
configuration.mode = Mode::Private;
155+
tracker_factory(configuration)
156+
}
157+
158+
fn listed_tracker() -> Tracker {
159+
let mut configuration = ephemeral_configuration();
160+
configuration.mode = Mode::Listed;
161+
tracker_factory(configuration)
162+
}
163+
164+
fn tracker_on_reverse_proxy() -> Tracker {
165+
let mut configuration = ephemeral_configuration();
166+
configuration.on_reverse_proxy = true;
167+
tracker_factory(configuration)
168+
}
169+
170+
fn tracker_not_on_reverse_proxy() -> Tracker {
171+
let mut configuration = ephemeral_configuration();
172+
configuration.on_reverse_proxy = false;
173+
tracker_factory(configuration)
174+
}
175+
176+
fn tracker_factory(configuration: Configuration) -> Tracker {
177+
// code-review: the tracker initialization is duplicated in many places. Consider make this function public.
178+
179+
// Initialize stats tracker
180+
let (stats_event_sender, stats_repository) = Keeper::new_active_instance();
181+
182+
// Initialize Torrust tracker
183+
match Tracker::new(&Arc::new(configuration), Some(stats_event_sender), stats_repository) {
184+
Ok(tracker) => tracker,
185+
Err(error) => {
186+
panic!("{}", error)
187+
}
188+
}
189+
}
190+
191+
fn sample_announce_request() -> Announce {
192+
Announce {
193+
info_hash: "3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0".parse::<InfoHash>().unwrap(),
194+
peer_id: "-qB00000000000000001".parse::<peer::Id>().unwrap(),
195+
port: 17548,
196+
downloaded: None,
197+
uploaded: None,
198+
left: None,
199+
event: None,
200+
compact: None,
201+
}
202+
}
203+
204+
fn sample_client_ip_sources() -> ClientIpSources {
205+
ClientIpSources {
206+
right_most_x_forwarded_for: None,
207+
connection_info_ip: None,
208+
}
209+
}
210+
211+
fn assert_error_response(error: &responses::error::Error, error_message: &str) {
212+
assert!(
213+
error.failure_reason.contains(error_message),
214+
"Error response does not contain message: '{error_message}'. Error: {error:?}"
215+
);
216+
}
217+
218+
mod with_tracker_in_private_mode {
219+
220+
use std::str::FromStr;
221+
use std::sync::Arc;
222+
223+
use super::{private_tracker, sample_announce_request, sample_client_ip_sources};
224+
use crate::http::axum_implementation::handlers::announce::handle_announce;
225+
use crate::http::axum_implementation::handlers::announce::tests::assert_error_response;
226+
use crate::tracker::auth;
227+
228+
#[tokio::test]
229+
async fn it_should_fail_when_the_authentication_key_is_missing() {
230+
let tracker = Arc::new(private_tracker());
231+
232+
let maybe_key = None;
233+
234+
let response = handle_announce(&tracker, &sample_announce_request(), &sample_client_ip_sources(), maybe_key)
235+
.await
236+
.unwrap_err();
237+
238+
assert_error_response(
239+
&response,
240+
"Authentication error: Missing authentication key param for private tracker",
241+
);
242+
}
243+
244+
#[tokio::test]
245+
async fn it_should_fail_when_the_authentication_key_is_invalid() {
246+
let tracker = Arc::new(private_tracker());
247+
248+
let unregistered_key = auth::Key::from_str("YZSl4lMZupRuOpSRC3krIKR5BPB14nrJ").unwrap();
249+
250+
let maybe_key = Some(unregistered_key);
251+
252+
let response = handle_announce(&tracker, &sample_announce_request(), &sample_client_ip_sources(), maybe_key)
253+
.await
254+
.unwrap_err();
255+
256+
assert_error_response(&response, "Authentication error: Failed to read key");
257+
}
258+
}
259+
260+
mod with_tracker_in_listed_mode {
261+
262+
use std::sync::Arc;
263+
264+
use super::{listed_tracker, sample_announce_request, sample_client_ip_sources};
265+
use crate::http::axum_implementation::handlers::announce::handle_announce;
266+
use crate::http::axum_implementation::handlers::announce::tests::assert_error_response;
267+
268+
#[tokio::test]
269+
async fn it_should_fail_when_the_announced_torrent_is_not_whitelisted() {
270+
let tracker = Arc::new(listed_tracker());
271+
272+
let announce_request = sample_announce_request();
273+
274+
let response = handle_announce(&tracker, &announce_request, &sample_client_ip_sources(), None)
275+
.await
276+
.unwrap_err();
277+
278+
assert_error_response(
279+
&response,
280+
&format!(
281+
"Tracker error: The torrent: {}, is not whitelisted",
282+
announce_request.info_hash
283+
),
284+
);
285+
}
286+
}
287+
288+
mod with_tracker_on_reverse_proxy {
289+
290+
use std::sync::Arc;
291+
292+
use super::{sample_announce_request, tracker_on_reverse_proxy};
293+
use crate::http::axum_implementation::handlers::announce::handle_announce;
294+
use crate::http::axum_implementation::handlers::announce::tests::assert_error_response;
295+
use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources;
296+
297+
#[tokio::test]
298+
async fn it_should_fail_when_the_right_most_x_forwarded_for_header_ip_is_not_available() {
299+
let tracker = Arc::new(tracker_on_reverse_proxy());
300+
301+
let client_ip_sources = ClientIpSources {
302+
right_most_x_forwarded_for: None,
303+
connection_info_ip: None,
304+
};
305+
306+
let response = handle_announce(&tracker, &sample_announce_request(), &client_ip_sources, None)
307+
.await
308+
.unwrap_err();
309+
310+
assert_error_response(
311+
&response,
312+
"Error resolving peer IP: missing or invalid the right most X-Forwarded-For IP",
313+
);
314+
}
315+
}
316+
317+
mod with_tracker_not_on_reverse_proxy {
318+
319+
use std::sync::Arc;
320+
321+
use super::{sample_announce_request, tracker_not_on_reverse_proxy};
322+
use crate::http::axum_implementation::handlers::announce::handle_announce;
323+
use crate::http::axum_implementation::handlers::announce::tests::assert_error_response;
324+
use crate::http::axum_implementation::services::peer_ip_resolver::ClientIpSources;
325+
326+
#[tokio::test]
327+
async fn it_should_fail_when_the_client_ip_from_the_connection_info_is_not_available() {
328+
let tracker = Arc::new(tracker_not_on_reverse_proxy());
329+
330+
let client_ip_sources = ClientIpSources {
331+
right_most_x_forwarded_for: None,
332+
connection_info_ip: None,
333+
};
334+
335+
let response = handle_announce(&tracker, &sample_announce_request(), &client_ip_sources, None)
336+
.await
337+
.unwrap_err();
338+
339+
assert_error_response(
340+
&response,
341+
"Error resolving peer IP: cannot get the client IP from the connection info",
342+
);
343+
}
344+
}
345+
}

0 commit comments

Comments
 (0)