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/// - Binding a listener or reading its local address fails.
37/// - `send` panics or takes more than five seconds.
38/// - The response status is not 307 or the destination receives a request.
39pub async fn assert_http_redirect_rejected<F, Fut>(send: F)
40where
41    F: FnOnce(String) -> Fut,
42    Fut: Future<Output = u16>,
43{
44    let origin = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
45    let addr = origin.local_addr().unwrap();
46    let destination = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
47    let target = destination.local_addr().unwrap();
48    let destination_requests = Arc::new(AtomicUsize::new(0));
49    let requests = destination_requests.clone();
50
51    let destination_task = tokio::spawn(async move {
52        let (mut stream, _) = destination.accept().await.unwrap();
53        read_request_headers(&mut stream).await;
54        requests.fetch_add(1, Ordering::SeqCst);
55        stream
56            .write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
57            .await
58            .unwrap();
59    });
60
61    let origin_task = tokio::spawn(async move {
62        let (mut stream, _) = origin.accept().await.unwrap();
63        read_request_headers(&mut stream).await;
64        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();
65    });
66    let result = tokio::time::timeout(
67        Duration::from_secs(5),
68        send(format!("http://{addr}/origin")),
69    )
70    .await;
71    origin_task.abort();
72    destination_task.abort();
73
74    assert_eq!(result.unwrap(), 307);
75    assert_eq!(destination_requests.load(Ordering::SeqCst), 0);
76}
77
78async fn read_request_headers(stream: &mut tokio::net::TcpStream) {
79    let mut headers = Vec::new();
80    while !headers.ends_with(b"\r\n\r\n") {
81        let mut byte = [0];
82        stream.read_exact(&mut byte).await.unwrap();
83        headers.push(byte[0]);
84    }
85}