use actus::prelude::*;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::oneshot;
const BODY_LEN: usize = 4096;
struct Text;
#[controller]
impl Text {
routes! {
GET "" => text(),
GET "stream" => streamed(),
}
pub async fn text(&self) -> Reply {
Ok(reply::bytes(
"text/plain; charset=utf-8",
vec![b'a'; BODY_LEN],
))
}
pub async fn streamed(&self) -> Reply {
let chunks = ["one", "two", "three"]
.map(|s| Ok::<_, std::io::Error>(Bytes::from_static(s.as_bytes())));
Ok(reply::stream(futures_util::stream::iter(chunks)))
}
}
app_routes! {
routes {
"text" => Text,
}
}
async fn spawn(server: Server) -> (SocketAddr, oneshot::Sender<()>) {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = oneshot::channel::<()>();
tokio::spawn(async move {
server
.run_with_shutdown_listener(listener, async move {
let _ = rx.await;
})
.await
.unwrap();
});
(addr, tx)
}
async fn get(addr: SocketAddr, path: &str, headers: &str) -> (String, Vec<u8>) {
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let raw = format!("GET {path} HTTP/1.1\r\nHost: x\r\n{headers}Connection: close\r\n\r\n");
stream.write_all(raw.as_bytes()).await.unwrap();
let mut received = Vec::new();
stream.read_to_end(&mut received).await.unwrap();
let split = received
.windows(4)
.position(|w| w == b"\r\n\r\n")
.expect("a header terminator");
let head = String::from_utf8_lossy(&received[..split]).to_ascii_lowercase();
(head, received)
}
#[tokio::test]
async fn every_byte_a_client_receives_is_counted() {
let sent = Arc::new(AtomicU64::new(0));
let (addr, _stop) =
spawn(Server::new(init().await.unwrap()).with_sent_bytes(sent.clone())).await;
let mut received = 0;
let (head, all) = get(addr, "/text", "").await;
assert!(head.starts_with("http/1.1 200"), "{head}");
assert!(all.len() > BODY_LEN);
received += all.len();
assert_eq!(sent.load(Ordering::Relaxed), received as u64);
let (head, all) = get(addr, "/text/stream", "").await;
assert!(head.contains("transfer-encoding: chunked"), "{head}");
received += all.len();
assert_eq!(sent.load(Ordering::Relaxed), received as u64);
let (head, all) = get(addr, "/no-such-route", "").await;
assert!(head.starts_with("http/1.1 404"), "{head}");
received += all.len();
assert_eq!(sent.load(Ordering::Relaxed), received as u64);
}
#[cfg(feature = "compression")]
#[tokio::test]
async fn a_compressed_reply_counts_the_compressed_bytes() {
let sent = Arc::new(AtomicU64::new(0));
let (addr, _stop) = spawn(
Server::new(init().await.unwrap())
.with_compression(actus_server::CompressionLayer::new())
.with_sent_bytes(sent.clone()),
)
.await;
let (head, all) = get(addr, "/text", "Accept-Encoding: gzip\r\n").await;
assert!(head.contains("content-encoding: gzip"), "{head}");
assert!(all.len() < BODY_LEN, "compression shrank what was sent");
assert_eq!(sent.load(Ordering::Relaxed), all.len() as u64);
}
#[cfg(feature = "websocket")]
mod upgraded {
use super::*;
use futures_util::{SinkExt, StreamExt};
struct Echo;
#[controller]
impl Echo {
routes! {
GET "" => echo(),
}
pub async fn echo(&self) -> Reply {
Ok(ws::upgrade(|mut socket| async move {
while let Some(Ok(msg)) = socket.next().await {
if msg.is_text() && socket.send(msg).await.is_err() {
break;
}
}
}))
}
}
app_routes! {
routes {
"ws" => Echo,
}
}
#[tokio::test]
async fn the_101_and_every_frame_after_it_are_counted() {
let sent = Arc::new(AtomicU64::new(0));
let (addr, _stop) =
spawn(Server::new(init().await.unwrap()).with_sent_bytes(sent.clone())).await;
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
stream
.write_all(
b"GET /ws HTTP/1.1\r\nHost: x\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n",
)
.await
.unwrap();
let mut head = Vec::new();
while !head.ends_with(b"\r\n\r\n") {
head.push(stream.read_u8().await.unwrap());
}
assert!(head.starts_with(b"HTTP/1.1 101"));
assert_eq!(sent.load(Ordering::Relaxed), head.len() as u64);
let mask = [0x11, 0x22, 0x33, 0x44];
let mut frame = vec![0x81, 0x80 | 2];
frame.extend(mask);
frame.extend(b"hi".iter().zip(mask).map(|(b, m)| b ^ m));
stream.write_all(&frame).await.unwrap();
let mut echo = [0u8; 4];
stream.read_exact(&mut echo).await.unwrap();
assert_eq!(&echo, b"\x81\x02hi");
assert_eq!(
sent.load(Ordering::Relaxed),
(head.len() + echo.len()) as u64
);
}
}