//! Generated by `rustio-admin test-init` — first-line CI check for your
//! Rustio project. Boots the project binary via `cargo run`, polls
//! the documented port, sends a raw HTTP request to `/admin/`, and
//! asserts the response is a 302 / 303 redirect to `/admin/login`.
//!
//! Stdlib-only — no extra deps, drop-in. Replace with `reqwest` or
//! similar once your project warrants richer checks; this skeleton
//! exists to catch the "server doesn't start" / "auth middleware
//! doesn't redirect anonymous traffic" class of regression that
//! kills new deployments silently.
//!
//! Run with: `cargo test --test smoke`
//!
//! Prerequisites:
//! - `DATABASE_URL` exported in your shell (the binary reads it).
//! - The framework's auth tables present (run `rustio-admin migrate
//! apply` once first).
use std::io::{Read, Write};
use std::net::TcpStream;
use std::process::{Child, Command, Stdio};
use std::time::{Duration, Instant};
/// Port the project binary is expected to bind. Override via
/// `RUSTIO_PORT` so this test doesn't collide with a running
/// admin on the default 3000.
fn target_port() -> u16 {
std::env::var("RUSTIO_PORT")
.ok()
.and_then(|s| s.parse::<u16>().ok())
.unwrap_or(3000)
}
/// Cargo run cancellation guard. Sends SIGKILL on drop so a panic
/// in the test body doesn't leak a server process onto the host.
struct ServerGuard {
child: Child,
}
impl Drop for ServerGuard {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
/// Spawn `cargo run --quiet` in the background and wait for the
/// target port to start accepting TCP connections. Times out after
/// 60 s — first-build warm-ups can be slow, retries are not.
fn boot_server(port: u16) -> ServerGuard {
let child = Command::new("cargo")
.args(["run", "--quiet"])
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
.expect("cargo run failed to spawn");
let guard = ServerGuard { child };
let deadline = Instant::now() + Duration::from_secs(60);
let addr = format!("127.0.0.1:{port}");
while Instant::now() < deadline {
if TcpStream::connect_timeout(&addr.parse().unwrap(), Duration::from_millis(200)).is_ok() {
return guard;
}
std::thread::sleep(Duration::from_millis(250));
}
panic!("server didn't bind {addr} within 60s — check `cargo run` locally");
}
/// Send a raw GET to `/admin/` and read enough of the response to
/// inspect the status line + the `Location:` header. Doesn't follow
/// redirects (that's the whole point of the assertion).
fn fetch_admin_root(port: u16) -> String {
let addr = format!("127.0.0.1:{port}");
let mut stream = TcpStream::connect(&addr).expect("connect to running server");
stream
.set_read_timeout(Some(Duration::from_secs(5)))
.unwrap();
let req = "GET /admin/ HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n";
stream.write_all(req.as_bytes()).expect("write request");
let mut response = String::new();
let _ = stream.read_to_string(&mut response);
response
}
#[test]
fn admin_root_redirects_to_login() {
let port = target_port();
let _guard = boot_server(port);
let response = fetch_admin_root(port);
// First line is the status line: `HTTP/1.1 302 Found` or
// `HTTP/1.1 303 See Other`. Either redirect status is fine for
// the anonymous-traffic-redirected-to-login policy.
let status_line = response.lines().next().unwrap_or("");
assert!(
status_line.contains(" 302 ") || status_line.contains(" 303 "),
"expected redirect, got status line: {status_line:?}\n\nfull response:\n{response}"
);
// The Location header should point at the login page. Match
// case-insensitively so a future header-rewrite refactor
// doesn't trip the test.
let location: Option<&str> = response
.lines()
.find_map(|l| {
let lower = l.to_ascii_lowercase();
lower.strip_prefix("location:").map(|_| l)
});
let location = location.expect("missing Location header");
assert!(
location.contains("/admin/login"),
"Location should point at /admin/login, got: {location:?}"
);
}