#![cfg(feature = "auth")]
use arcature::auth::{Flash, SessionConfig};
use arcature::http::response::{RedirectResponse, redirect};
use arcature::routing::{RedirectMapper, Route, Routes};
use axum::Router;
use axum::body::Body;
use axum::http::{Request, Response, StatusCode, header};
use tower::ServiceExt;
use tower_sessions_memory_store::MemoryStore;
const KEY: &[u8] = &[7u8; 64];
async fn save() -> RedirectResponse {
redirect()
.route("users.show", 7u64)
.with("status", "Profile updated")
}
async fn show(flash: Flash) -> String {
flash.get("status").unwrap_or("-").to_string()
}
fn app() -> Router {
let routes: Routes = Routes::new([
Route::post("/save", save).name("save"),
Route::get("/users/{id}", show).name("users.show"),
]);
let table = routes.table();
let session = SessionConfig::dev(KEY)
.expect("a 64-byte key is valid")
.into_layer(MemoryStore::default())
.expect("the dev session config is valid");
routes
.into_router()
.layer(RedirectMapper::new(table))
.layer(session)
}
fn cookie(response: &Response<Body>) -> String {
response
.headers()
.get(header::SET_COOKIE)
.expect("the session layer set a cookie")
.to_str()
.expect("cookies are ASCII")
.split(';')
.next()
.expect("split always yields one element")
.to_string()
}
async fn body(response: Response<Body>) -> String {
let bytes = axum::body::to_bytes(response.into_body(), 64 * 1024)
.await
.expect("the test bodies are tiny");
String::from_utf8(bytes.to_vec()).expect("the handler returns UTF-8")
}
#[tokio::test]
async fn a_flashed_redirect_resolves_its_name_and_survives_exactly_one_request() {
let app = app();
let posted = app
.clone()
.oneshot(
Request::post("/save")
.body(Body::empty())
.expect("a valid request"),
)
.await
.expect("the router is infallible");
assert_eq!(
posted.headers().get(header::LOCATION).unwrap(),
"/users/7",
"`route(\"users.show\", 7)` should resolve against the table"
);
let session = cookie(&posted);
let followed = app
.clone()
.oneshot(
Request::get("/users/7")
.header(header::COOKIE, &session)
.body(Body::empty())
.expect("a valid request"),
)
.await
.expect("the router is infallible");
assert_eq!(followed.status(), StatusCode::OK);
assert_eq!(body(followed).await, "Profile updated");
let reloaded = app
.oneshot(
Request::get("/users/7")
.header(header::COOKIE, &session)
.body(Body::empty())
.expect("a valid request"),
)
.await
.expect("the router is infallible");
assert_eq!(body(reloaded).await, "-", "flash data outlived its request");
}
#[tokio::test]
async fn a_second_session_cannot_read_the_first_session_flash() {
let app = app();
let posted = app
.clone()
.oneshot(
Request::post("/save")
.body(Body::empty())
.expect("a valid request"),
)
.await
.expect("the router is infallible");
let _ = cookie(&posted);
let stranger = app
.oneshot(
Request::get("/users/7")
.body(Body::empty())
.expect("a valid request"),
)
.await
.expect("the router is infallible");
assert_eq!(
body(stranger).await,
"-",
"flash data leaked across sessions"
);
}