Skip to main content

nautilus_testkit/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Local HTTP redirect assertions for client constructor tests.
17
18use std::{
19    future::Future,
20    sync::{
21        Arc,
22        atomic::{AtomicUsize, Ordering},
23    },
24    time::Duration,
25};
26
27use tokio::io::{AsyncReadExt, AsyncWriteExt};
28
29/// Asserts that a GET request returns the original 307 without contacting its redirect target.
30///
31/// `send` receives a loopback URL and returns the response status code. It must issue one
32/// GET request through the client under test without overriding the client's redirect policy.
33///
34/// # Panics
35///
36/// Panics if:
37/// - Binding a listener or reading its local address fails.
38/// - `send` panics or takes more than five seconds.
39/// - The response status is not 307 or the destination receives a request.
40pub async fn assert_http_redirect_rejected<F, Fut>(send: F)
41where
42    F: FnOnce(String) -> Fut,
43    Fut: Future<Output = u16>,
44{
45    let origin = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
46    let addr = origin.local_addr().unwrap();
47    let destination = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
48    let target = destination.local_addr().unwrap();
49    let destination_requests = Arc::new(AtomicUsize::new(0));
50    let requests = destination_requests.clone();
51
52    let destination_task = tokio::spawn(async move {
53        let (mut stream, _) = destination.accept().await.unwrap();
54        read_request_headers(&mut stream).await;
55        requests.fetch_add(1, Ordering::SeqCst);
56        stream
57            .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
58            .await
59            .unwrap();
60    });
61
62    let origin_task = tokio::spawn(async move {
63        let (mut stream, _) = origin.accept().await.unwrap();
64        read_request_headers(&mut stream).await;
65        stream.write_all(format!("HTTP/1.1 307 Temporary Redirect\r\nContent-Length: 0\r\nConnection: close\r\nLocation: http://{target}/destination\r\n\r\n").as_bytes()).await.unwrap();
66    });
67    let result = tokio::time::timeout(
68        Duration::from_secs(5),
69        send(format!("http://{addr}/origin")),
70    )
71    .await;
72    origin_task.abort();
73    destination_task.abort();
74
75    assert_eq!(result.unwrap(), 307);
76    assert_eq!(destination_requests.load(Ordering::SeqCst), 0);
77}
78
79async fn read_request_headers(stream: &mut tokio::net::TcpStream) {
80    let mut headers = Vec::new();
81    while !headers.ends_with(b"\r\n\r\n") {
82        let mut byte = [0];
83        stream.read_exact(&mut byte).await.unwrap();
84        headers.push(byte[0]);
85    }
86}