Skip to content

Commit 2a8a114

Browse files
committed
Merge #1161: Log assertions: add assertions for API log errors
5d49d48 test: [#1152] write assertions when API writes errors into logs (Jose Celano) cc2840f feat: add an option to pass the request id in the API client (Jose Celano) 88d3d49 feat: add server socket address to logs in API (Jose Celano) Pull request description: It adds test assertions for log errors in the context of the tracker API. Sample `ERROR` log: ```console cargo test -- --nocapture servers::api::v1::contract::authentication::should_not_authenticate_requests_when_the_token_is_empty ``` ```output 2024-12-26T11:13:00.203024Z ERROR API: response latency_ms=0 status_code=500 Internal Server Error server_socket_addr=127.0.0.1:35409 request_id=922cffeb-ed64-4d56-bbd0-400d6ea6f8d2 ``` We are using the `request_id` to find the log line because the tracing setup is global (shared by all tests). When the tracker API request is sent, the `request_id` is injected as an HTTP header. Top commit has no ACKs. Tree-SHA512: 0430296e2a9ca39b29fbd2cf34e73086c1084a80d58a8ac3994ee834713fe48c710b168d47929ef18240ef37e3fc98480b6ca15ba7313161981c2f0f5118dbbd
2 parents ce9f388 + 5d49d48 commit 2a8a114

File tree

9 files changed

+504
-154
lines changed

9 files changed

+504
-154
lines changed

src/servers/apis/routes.rs

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
//!
66
//! All the API routes have the `/api` prefix and the version number as the
77
//! first path segment. For example: `/api/v1/torrents`.
8+
use std::net::SocketAddr;
89
use std::sync::Arc;
910
use std::time::Duration;
1011

@@ -35,7 +36,7 @@ use crate::servers::logging::Latency;
3536
/// Add all API routes to the router.
3637
#[allow(clippy::needless_pass_by_value)]
3738
#[instrument(skip(tracker, access_tokens))]
38-
pub fn router(tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>) -> Router {
39+
pub fn router(tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>, server_socket_addr: SocketAddr) -> Router {
3940
let router = Router::new();
4041

4142
let api_url_prefix = "/api";
@@ -68,7 +69,7 @@ pub fn router(tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>) -> Router
6869
target: API_LOG_TARGET,
6970
tracing::Level::INFO, %method, %uri, %request_id, "request");
7071
})
71-
.on_response(|response: &Response, latency: Duration, span: &Span| {
72+
.on_response(move |response: &Response, latency: Duration, span: &Span| {
7273
let latency_ms = latency.as_millis();
7374
let status_code = response.status();
7475
let request_id = response
@@ -82,20 +83,20 @@ pub fn router(tracker: Arc<Tracker>, access_tokens: Arc<AccessTokens>) -> Router
8283
if status_code.is_server_error() {
8384
tracing::event!(
8485
target: API_LOG_TARGET,
85-
tracing::Level::ERROR, %latency_ms, %status_code, %request_id, "response");
86+
tracing::Level::ERROR, %latency_ms, %status_code, %server_socket_addr, %request_id, "response");
8687
} else {
8788
tracing::event!(
8889
target: API_LOG_TARGET,
89-
tracing::Level::INFO, %latency_ms, %status_code, %request_id, "response");
90+
tracing::Level::INFO, %latency_ms, %status_code, %server_socket_addr, %request_id, "response");
9091
}
9192
})
9293
.on_failure(
93-
|failure_classification: ServerErrorsFailureClass, latency: Duration, _span: &Span| {
94+
move |failure_classification: ServerErrorsFailureClass, latency: Duration, _span: &Span| {
9495
let latency = Latency::new(LatencyUnit::Millis, latency);
9596

9697
tracing::event!(
9798
target: API_LOG_TARGET,
98-
tracing::Level::ERROR, %failure_classification, %latency, "response failed");
99+
tracing::Level::ERROR, %failure_classification, %latency, %server_socket_addr, "response failed");
99100
},
100101
),
101102
)

src/servers/apis/server.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -243,10 +243,11 @@ impl Launcher {
243243
tx_start: Sender<Started>,
244244
rx_halt: Receiver<Halted>,
245245
) -> BoxFuture<'static, ()> {
246-
let router = router(tracker, access_tokens);
247246
let socket = std::net::TcpListener::bind(self.bind_to).expect("Could not bind tcp_listener to address.");
248247
let address = socket.local_addr().expect("Could not get local_addr from tcp_listener.");
249248

249+
let router = router(tracker, access_tokens, address);
250+
250251
let handle = Handle::new();
251252

252253
tokio::task::spawn(graceful_shutdown(

tests/servers/api/v1/client.rs

+78-55
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
use hyper::HeaderMap;
12
use reqwest::Response;
23
use serde::Serialize;
4+
use uuid::Uuid;
35

46
use crate::common::http::{Query, QueryParam, ReqwestQuery};
57
use crate::servers::api::connection_info::ConnectionInfo;
@@ -18,90 +20,102 @@ impl Client {
1820
}
1921
}
2022

21-
pub async fn generate_auth_key(&self, seconds_valid: i32) -> Response {
22-
self.post_empty(&format!("key/{}", &seconds_valid)).await
23+
pub async fn generate_auth_key(&self, seconds_valid: i32, headers: Option<HeaderMap>) -> Response {
24+
self.post_empty(&format!("key/{}", &seconds_valid), headers).await
2325
}
2426

25-
pub async fn add_auth_key(&self, add_key_form: AddKeyForm) -> Response {
26-
self.post_form("keys", &add_key_form).await
27+
pub async fn add_auth_key(&self, add_key_form: AddKeyForm, headers: Option<HeaderMap>) -> Response {
28+
self.post_form("keys", &add_key_form, headers).await
2729
}
2830

29-
pub async fn delete_auth_key(&self, key: &str) -> Response {
30-
self.delete(&format!("key/{}", &key)).await
31+
pub async fn delete_auth_key(&self, key: &str, headers: Option<HeaderMap>) -> Response {
32+
self.delete(&format!("key/{}", &key), headers).await
3133
}
3234

33-
pub async fn reload_keys(&self) -> Response {
34-
self.get("keys/reload", Query::default()).await
35+
pub async fn reload_keys(&self, headers: Option<HeaderMap>) -> Response {
36+
self.get("keys/reload", Query::default(), headers).await
3537
}
3638

37-
pub async fn whitelist_a_torrent(&self, info_hash: &str) -> Response {
38-
self.post_empty(&format!("whitelist/{}", &info_hash)).await
39+
pub async fn whitelist_a_torrent(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
40+
self.post_empty(&format!("whitelist/{}", &info_hash), headers).await
3941
}
4042

41-
pub async fn remove_torrent_from_whitelist(&self, info_hash: &str) -> Response {
42-
self.delete(&format!("whitelist/{}", &info_hash)).await
43+
pub async fn remove_torrent_from_whitelist(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
44+
self.delete(&format!("whitelist/{}", &info_hash), headers).await
4345
}
4446

45-
pub async fn reload_whitelist(&self) -> Response {
46-
self.get("whitelist/reload", Query::default()).await
47+
pub async fn reload_whitelist(&self, headers: Option<HeaderMap>) -> Response {
48+
self.get("whitelist/reload", Query::default(), headers).await
4749
}
4850

49-
pub async fn get_torrent(&self, info_hash: &str) -> Response {
50-
self.get(&format!("torrent/{}", &info_hash), Query::default()).await
51+
pub async fn get_torrent(&self, info_hash: &str, headers: Option<HeaderMap>) -> Response {
52+
self.get(&format!("torrent/{}", &info_hash), Query::default(), headers).await
5153
}
5254

53-
pub async fn get_torrents(&self, params: Query) -> Response {
54-
self.get("torrents", params).await
55+
pub async fn get_torrents(&self, params: Query, headers: Option<HeaderMap>) -> Response {
56+
self.get("torrents", params, headers).await
5557
}
5658

57-
pub async fn get_tracker_statistics(&self) -> Response {
58-
self.get("stats", Query::default()).await
59+
pub async fn get_tracker_statistics(&self, headers: Option<HeaderMap>) -> Response {
60+
self.get("stats", Query::default(), headers).await
5961
}
6062

61-
pub async fn get(&self, path: &str, params: Query) -> Response {
63+
pub async fn get(&self, path: &str, params: Query, headers: Option<HeaderMap>) -> Response {
6264
let mut query: Query = params;
6365

6466
if let Some(token) = &self.connection_info.api_token {
6567
query.add_param(QueryParam::new("token", token));
6668
};
6769

68-
self.get_request_with_query(path, query).await
70+
self.get_request_with_query(path, query, headers).await
6971
}
7072

71-
pub async fn post_empty(&self, path: &str) -> Response {
72-
reqwest::Client::new()
73+
pub async fn post_empty(&self, path: &str, headers: Option<HeaderMap>) -> Response {
74+
let builder = reqwest::Client::new()
7375
.post(self.base_url(path).clone())
74-
.query(&ReqwestQuery::from(self.query_with_token()))
75-
.send()
76-
.await
77-
.unwrap()
76+
.query(&ReqwestQuery::from(self.query_with_token()));
77+
78+
let builder = match headers {
79+
Some(headers) => builder.headers(headers),
80+
None => builder,
81+
};
82+
83+
builder.send().await.unwrap()
7884
}
7985

80-
pub async fn post_form<T: Serialize + ?Sized>(&self, path: &str, form: &T) -> Response {
81-
reqwest::Client::new()
86+
pub async fn post_form<T: Serialize + ?Sized>(&self, path: &str, form: &T, headers: Option<HeaderMap>) -> Response {
87+
let builder = reqwest::Client::new()
8288
.post(self.base_url(path).clone())
8389
.query(&ReqwestQuery::from(self.query_with_token()))
84-
.json(&form)
85-
.send()
86-
.await
87-
.unwrap()
90+
.json(&form);
91+
92+
let builder = match headers {
93+
Some(headers) => builder.headers(headers),
94+
None => builder,
95+
};
96+
97+
builder.send().await.unwrap()
8898
}
8999

90-
async fn delete(&self, path: &str) -> Response {
91-
reqwest::Client::new()
100+
async fn delete(&self, path: &str, headers: Option<HeaderMap>) -> Response {
101+
let builder = reqwest::Client::new()
92102
.delete(self.base_url(path).clone())
93-
.query(&ReqwestQuery::from(self.query_with_token()))
94-
.send()
95-
.await
96-
.unwrap()
103+
.query(&ReqwestQuery::from(self.query_with_token()));
104+
105+
let builder = match headers {
106+
Some(headers) => builder.headers(headers),
107+
None => builder,
108+
};
109+
110+
builder.send().await.unwrap()
97111
}
98112

99-
pub async fn get_request_with_query(&self, path: &str, params: Query) -> Response {
100-
get(&self.base_url(path), Some(params)).await
113+
pub async fn get_request_with_query(&self, path: &str, params: Query, headers: Option<HeaderMap>) -> Response {
114+
get(&self.base_url(path), Some(params), headers).await
101115
}
102116

103117
pub async fn get_request(&self, path: &str) -> Response {
104-
get(&self.base_url(path), None).await
118+
get(&self.base_url(path), None, None).await
105119
}
106120

107121
fn query_with_token(&self) -> Query {
@@ -116,18 +130,27 @@ impl Client {
116130
}
117131
}
118132

119-
pub async fn get(path: &str, query: Option<Query>) -> Response {
120-
match query {
121-
Some(params) => reqwest::Client::builder()
122-
.build()
123-
.unwrap()
124-
.get(path)
125-
.query(&ReqwestQuery::from(params))
126-
.send()
127-
.await
128-
.unwrap(),
129-
None => reqwest::Client::builder().build().unwrap().get(path).send().await.unwrap(),
130-
}
133+
pub async fn get(path: &str, query: Option<Query>, headers: Option<HeaderMap>) -> Response {
134+
let builder = reqwest::Client::builder().build().unwrap();
135+
136+
let builder = match query {
137+
Some(params) => builder.get(path).query(&ReqwestQuery::from(params)),
138+
None => builder.get(path),
139+
};
140+
141+
let builder = match headers {
142+
Some(headers) => builder.headers(headers),
143+
None => builder,
144+
};
145+
146+
builder.send().await.unwrap()
147+
}
148+
149+
/// Returns a `HeaderMap` with a request id header
150+
pub fn headers_with_request_id(request_id: Uuid) -> HeaderMap {
151+
let mut headers = HeaderMap::new();
152+
headers.insert("x-request-id", request_id.to_string().parse().unwrap());
153+
headers
131154
}
132155

133156
#[derive(Serialize, Debug)]

tests/servers/api/v1/contract/authentication.rs

+36-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
use torrust_tracker_test_helpers::configuration;
2+
use uuid::Uuid;
23

34
use crate::common::http::{Query, QueryParam};
4-
use crate::common::logging::{self};
5+
use crate::common::logging::{self, logs_contains_a_line_with};
56
use crate::servers::api::v1::asserts::{assert_token_not_valid, assert_unauthorized};
6-
use crate::servers::api::v1::client::Client;
7+
use crate::servers::api::v1::client::{headers_with_request_id, Client};
78
use crate::servers::api::Started;
89

910
#[tokio::test]
@@ -15,7 +16,7 @@ async fn should_authenticate_requests_by_using_a_token_query_param() {
1516
let token = env.get_connection_info().api_token.unwrap();
1617

1718
let response = Client::new(env.get_connection_info())
18-
.get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec()))
19+
.get_request_with_query("stats", Query::params([QueryParam::new("token", &token)].to_vec()), None)
1920
.await;
2021

2122
assert_eq!(response.status(), 200);
@@ -29,12 +30,19 @@ async fn should_not_authenticate_requests_when_the_token_is_missing() {
2930

3031
let env = Started::new(&configuration::ephemeral().into()).await;
3132

33+
let request_id = Uuid::new_v4();
34+
3235
let response = Client::new(env.get_connection_info())
33-
.get_request_with_query("stats", Query::default())
36+
.get_request_with_query("stats", Query::default(), Some(headers_with_request_id(request_id)))
3437
.await;
3538

3639
assert_unauthorized(response).await;
3740

41+
assert!(
42+
logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]),
43+
"Expected logs to contain: ERROR ... API ... request_id={request_id}"
44+
);
45+
3846
env.stop().await;
3947
}
4048

@@ -44,12 +52,23 @@ async fn should_not_authenticate_requests_when_the_token_is_empty() {
4452

4553
let env = Started::new(&configuration::ephemeral().into()).await;
4654

55+
let request_id = Uuid::new_v4();
56+
4757
let response = Client::new(env.get_connection_info())
48-
.get_request_with_query("stats", Query::params([QueryParam::new("token", "")].to_vec()))
58+
.get_request_with_query(
59+
"stats",
60+
Query::params([QueryParam::new("token", "")].to_vec()),
61+
Some(headers_with_request_id(request_id)),
62+
)
4963
.await;
5064

5165
assert_token_not_valid(response).await;
5266

67+
assert!(
68+
logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]),
69+
"Expected logs to contain: ERROR ... API ... request_id={request_id}"
70+
);
71+
5372
env.stop().await;
5473
}
5574

@@ -59,12 +78,23 @@ async fn should_not_authenticate_requests_when_the_token_is_invalid() {
5978

6079
let env = Started::new(&configuration::ephemeral().into()).await;
6180

81+
let request_id = Uuid::new_v4();
82+
6283
let response = Client::new(env.get_connection_info())
63-
.get_request_with_query("stats", Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec()))
84+
.get_request_with_query(
85+
"stats",
86+
Query::params([QueryParam::new("token", "INVALID TOKEN")].to_vec()),
87+
Some(headers_with_request_id(request_id)),
88+
)
6489
.await;
6590

6691
assert_token_not_valid(response).await;
6792

93+
assert!(
94+
logs_contains_a_line_with(&["ERROR", "API", &format!("{request_id}")]),
95+
"Expected logs to contain: ERROR ... API ... request_id={request_id}"
96+
);
97+
6898
env.stop().await;
6999
}
70100

0 commit comments

Comments
 (0)