//! `serve` as deployed: the router with `ConnectInfo` (so the rate limiter sees each client's address, //! SPEC §8.6), `/i/` with an empty code as the HTML 404 page, and `rotate-epoch` as the real binary (SPEC §10.4). use std::net::SocketAddr; use std::process::Command; use axum::body::Body; use axum::http::{Method, Request}; use kvitt_server::cli::make_service; use tokio::net::TcpListener; use crate::common::*; #[tokio::test] async fn two_client_addresses_get_separate_rate_limit_buckets_end_to_end() { let app = TestApp::empty().await; // One service, so one rate limiter, reached from two client addresses: 127.0.0.1 and ::1. let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let v4 = listener.local_addr().unwrap(); let listener6 = TcpListener::bind("[::1]:0").await.unwrap(); let v6 = listener6.local_addr().unwrap(); let service = make_service(app.state.clone()); let service6 = service.clone(); tokio::spawn(async move { axum::serve(listener, service).await.unwrap() }); tokio::spawn(async move { axum::serve(listener6, service6).await.unwrap() }); let client = reqwest::Client::new(); let get = |addr: SocketAddr| { let client = client.clone(); async move { client .get(format!("http://{addr}/health")) .send() .await .unwrap() .status() .as_u16() } }; for _ in 0..60 { assert_eq!(get(v4).await, 200); } assert_eq!( get(v4).await, 429, "the 61st request from 127.0.0.1 in the minute" ); assert_eq!(get(v6).await, 200, "::1 has its own bucket"); } #[tokio::test] async fn empty_invite_code_is_the_html_404_page() { let app = TestApp::empty().await; let reply = app .send( Request::builder() .method(Method::GET) .uri("/i/") .body(Body::empty()) .unwrap(), ) .await; assert_eq!(reply.status, 404); assert_eq!(reply.headers["content-type"], "text/html; charset=utf-8"); assert!(reply.text().contains("

404

")); } #[tokio::test] async fn rotate_epoch_sets_a_new_random_epoch_and_exits_0() { let app = TestApp::empty().await; let epoch = || async { sqlx::query_scalar::<_, String>("SELECT value FROM meta WHERE key = 'sync_epoch'") .fetch_one(&app.state.db) .await .unwrap() }; let before = epoch().await; let output = Command::new(env!("CARGO_BIN_EXE_kvitt-server")) .arg("rotate-epoch") .env("KVITT_DATA_DIR", app.data_dir()) .env("RUST_LOG", "warn") .output() .unwrap(); assert_eq!(output.status.code(), Some(0), "{output:?}"); let after = epoch().await; assert_ne!(before, after); assert_eq!(after.len(), 32); assert!(after.chars().all(|c| matches!(c, '0'..='9' | 'a'..='f'))); assert!(String::from_utf8_lossy(&output.stdout).contains(&after)); }