mod common;
use axum::http::StatusCode;
use common::*;
use serde_json::json;
use what_core::server::create_router;
fn future_exp() -> u64 {
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs()
+ 3600
}
fn auth_config_with_roles(secret: &str) -> what_core::Config {
let mut config = auth_config_with_secret(secret);
config.auth.jwt_claims.push("roles".to_string());
config
}
#[tokio::test]
async fn public_page_no_auth_directive() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Public</h1>");
let (_dir, state) = proj.build_state();
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Public");
}
#[tokio::test]
async fn public_page_auth_all() {
let proj = TestProject::new();
proj.add_page("index.html", "<what auth=\"all\" />\n<h1>Open</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Open");
}
#[tokio::test]
async fn protected_page_redirects_unauthenticated() {
let proj = TestProject::new();
proj.add_page("secret.html", "<what auth=\"user\" />\n<h1>Secret</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = get(&router, "/secret").await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let location = resp.location().expect("should have Location header");
assert!(
location.contains("/login"),
"redirect should point to login, got: {}",
location
);
}
#[tokio::test]
async fn protected_page_allows_jwt() {
let proj = TestProject::new();
proj.add_page("secret.html", "<what auth=\"user\" />\n<h1>Secret</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "user@test.com", "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/secret",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Secret");
}
#[tokio::test]
async fn role_protected_allows_matching_role() {
let proj = TestProject::new();
proj.add_page("admin.html", "<what auth=\"admin\" />\n<h1>Admin</h1>");
let config = auth_config_with_roles("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "admin@test.com", "roles": ["admin"], "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/admin",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Admin");
}
#[tokio::test]
async fn role_protected_denies_wrong_role() {
let proj = TestProject::new();
proj.add_page("admin.html", "<what auth=\"admin\" />\n<h1>Admin</h1>");
let config = auth_config_with_roles("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "editor@test.com", "roles": ["editor"], "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/admin",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::FORBIDDEN);
}
#[tokio::test]
async fn config_protected_paths_redirect() {
let proj = TestProject::new();
proj.add_page("dashboard/index.html", "<h1>Dashboard</h1>");
let mut config = auth_config_with_secret("test-secret");
config.auth.protected_paths = vec!["/dashboard/*".to_string()];
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = get(&router, "/dashboard/").await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let location = resp.location().expect("should have Location header");
assert!(
location.contains("/login"),
"should redirect to login, got: {}",
location
);
}
#[tokio::test]
async fn config_protected_paths_allows_jwt() {
let proj = TestProject::new();
proj.add_page("dashboard/index.html", "<h1>Dashboard</h1>");
let mut config = auth_config_with_secret("test-secret");
config.auth.protected_paths = vec!["/dashboard/*".to_string()];
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "user@test.com", "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/dashboard/",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("Dashboard");
}
#[tokio::test]
async fn user_authenticated_variable() {
let proj = TestProject::new();
proj.add_page("profile.html", "<p>#user.authenticated#</p>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "a@b.com", "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/profile",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("true");
}
#[tokio::test]
async fn user_email_variable() {
let proj = TestProject::new();
proj.add_page("profile.html", "<p>#user.email#</p>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let token = create_test_jwt(
&json!({ "email": "alice@example.com", "exp": future_exp() }),
"test-secret",
);
let resp = get_with_headers(
&router,
"/profile",
vec![("cookie", &format!("w_token={}", token))],
)
.await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("alice@example.com");
}
#[tokio::test]
async fn unauthenticated_user_variable() {
let proj = TestProject::new();
proj.add_page("index.html", "<p>#user.authenticated#</p>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = get(&router, "/").await;
assert_eq!(resp.status, StatusCode::OK);
resp.assert_contains("false");
}
#[tokio::test]
async fn logout_clears_cookie() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = post_form(&router, "/w-auth/logout", "").await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let cookie = resp.set_cookie().expect("should have Set-Cookie header");
assert!(
cookie.contains("Max-Age=0"),
"cookie should be cleared, got: {}",
cookie
);
assert!(
cookie.contains("w_token="),
"should clear the w_token cookie"
);
}
#[tokio::test]
async fn w_set_session_scope_allows_unauthenticated() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = post_form_with_headers(
&router,
"/w-set",
"expr=session.counter+%2B%3D+1",
vec![("X-Requested-With", "What")],
)
.await;
assert_ne!(
resp.status,
StatusCode::FORBIDDEN,
"session.* mutations should not require auth"
);
}
#[tokio::test]
async fn w_set_app_scope_rejects_unauthenticated() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = post_form_with_headers(
&router,
"/w-set",
"expr=app.counter+%2B%3D+1",
vec![("X-Requested-With", "What")],
)
.await;
assert_eq!(
resp.status,
StatusCode::FORBIDDEN,
"app.* mutations should require auth"
);
}
#[tokio::test]
async fn w_set_wired_scope_rejects_unauthenticated() {
let proj = TestProject::new();
proj.add_page("index.html", "<h1>Home</h1>");
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = post_form_with_headers(
&router,
"/w-set",
"expr=wired.total+%2B%3D+1",
vec![("X-Requested-With", "What")],
)
.await;
assert_eq!(
resp.status,
StatusCode::FORBIDDEN,
"wired.* mutations should require auth"
);
}
#[tokio::test]
async fn redirect_preserves_return_url() {
let proj = TestProject::new();
proj.add_page(
"admin/secret.html",
"<what auth=\"user\" />\n<h1>Secret</h1>",
);
let config = auth_config_with_secret("test-secret");
let (_dir, state) = proj.build_state_with_config(config);
let router = create_router(state);
let resp = get(&router, "/admin/secret").await;
assert_eq!(resp.status, StatusCode::SEE_OTHER);
let location = resp.location().expect("should have Location header");
assert!(
location.contains("redirect=%2Fadmin%2Fsecret"),
"redirect URL should be encoded in location, got: {}",
location
);
}