mirror of
https://github.com/zyycn/codex-proxy-rs.git
synced 2026-11-06 08:06:19 +00:00
Consolidate overlapping scenarios and remove obsolete assertions and migration-specific SQL tests. Keep persistence fixtures on the current schema and document the test maintenance rules in the development skill. Validation: 3,997 tests passed across 12 crates, plus rustfmt, strict Clippy, and skill validation. All 33 migration SQL files passed temporary isolated execution, and the opt-in xAI official network test passed. Co-authored-by: Codex <noreply@openai.com>
737 lines
24 KiB
Rust
737 lines
24 KiB
Rust
//! 验证 HTTP 登录身份、会话轮换与管理员和 Key 会话的权限隔离
|
|
|
|
use std::sync::atomic::Ordering;
|
|
|
|
use axum::{
|
|
body::Body,
|
|
http::{Method, Request, StatusCode, header},
|
|
};
|
|
use serde_json::{Value, json};
|
|
use tower::ServiceExt as _;
|
|
|
|
use crate::support::{
|
|
RAW_KEY, auth_app, cookie_request, empty_request, json_request, response_json,
|
|
};
|
|
|
|
fn session_cookie(response: &axum::response::Response) -> String {
|
|
response.headers()[header::SET_COOKIE]
|
|
.to_str()
|
|
.expect("cookie")
|
|
.split(';')
|
|
.next()
|
|
.expect("cookie pair")
|
|
.to_owned()
|
|
}
|
|
|
|
fn refresh_cookie(response: &axum::response::Response) -> String {
|
|
response
|
|
.headers()
|
|
.get_all(header::SET_COOKIE)
|
|
.iter()
|
|
.filter_map(|value| value.to_str().ok())
|
|
.find(|value| value.starts_with("cpr_refresh="))
|
|
.expect("refresh cookie")
|
|
.split(';')
|
|
.next()
|
|
.unwrap()
|
|
.to_owned()
|
|
}
|
|
|
|
async fn refresh(app: &axum::Router, cookie: &str) -> axum::response::Response {
|
|
app.clone()
|
|
.oneshot(cookie_request(Method::POST, "/api/auth/refresh", cookie))
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
async fn login(
|
|
app: &axum::Router,
|
|
body: Value,
|
|
previous: Option<&str>,
|
|
) -> axum::response::Response {
|
|
let mut request = json_request(Method::POST, "/api/auth/login", body);
|
|
if let Some(cookie) = previous {
|
|
request
|
|
.headers_mut()
|
|
.insert(header::COOKIE, cookie.parse().expect("cookie"));
|
|
}
|
|
app.clone().oneshot(request).await.expect("login response")
|
|
}
|
|
|
|
async fn get(app: &axum::Router, path: &str, cookie: &str) -> axum::response::Response {
|
|
app.clone()
|
|
.oneshot(cookie_request(Method::GET, path, cookie))
|
|
.await
|
|
.expect("GET response")
|
|
}
|
|
|
|
async fn change_password(
|
|
app: &axum::Router,
|
|
cookie: Option<&str>,
|
|
current: &str,
|
|
new: &str,
|
|
) -> axum::response::Response {
|
|
let mut request = json_request(
|
|
Method::POST,
|
|
"/api/auth/password",
|
|
json!({"currentPassword": current, "newPassword": new}),
|
|
);
|
|
if let Some(cookie) = cookie {
|
|
request
|
|
.headers_mut()
|
|
.insert(header::COOKIE, cookie.parse().unwrap());
|
|
}
|
|
app.clone().oneshot(request).await.unwrap()
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn password_change_revokes_all_admin_sessions_but_preserves_key_sessions() {
|
|
let (app, fixture) = auth_app().await;
|
|
let admin_body = json!({"mode": "admin", "password": "strong-admin-password"});
|
|
let first = session_cookie(&login(&app, admin_body.clone(), None).await);
|
|
let second = session_cookie(&login(&app, admin_body.clone(), None).await);
|
|
let key = session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
fixture.set_api_key(&format!("admin-{}", "b".repeat(64)));
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &first)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
let response = change_password(
|
|
&app,
|
|
Some(&first),
|
|
"strong-admin-password",
|
|
"new-strong-admin-password",
|
|
)
|
|
.await;
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
|
|
assert!(
|
|
response.headers()[header::SET_COOKIE]
|
|
.to_str()
|
|
.unwrap()
|
|
.contains("Max-Age=0")
|
|
);
|
|
for cookie in [&first, &second] {
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", cookie)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
}
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &key).await).await["data"]["authenticated"],
|
|
true
|
|
);
|
|
assert_eq!(
|
|
login(&app, admin_body, None).await.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
assert_eq!(
|
|
login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "new-strong-admin-password"}),
|
|
None
|
|
)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn password_change_requires_admin_session_and_keeps_session_on_validation_failure() {
|
|
let (app, _) = auth_app().await;
|
|
assert_eq!(
|
|
change_password(&app, None, "strong-admin-password", "new-strong-password")
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
let key = session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
assert_eq!(
|
|
change_password(
|
|
&app,
|
|
Some(&key),
|
|
"strong-admin-password",
|
|
"new-strong-password"
|
|
)
|
|
.await
|
|
.status(),
|
|
StatusCode::FORBIDDEN
|
|
);
|
|
let admin = session_cookie(
|
|
&login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "strong-admin-password"}),
|
|
None,
|
|
)
|
|
.await,
|
|
);
|
|
for (current, new) in [
|
|
("wrong-current-password", "new-strong-password"),
|
|
("strong-admin-password", "short"),
|
|
("strong-admin-password", "strong-admin-password"),
|
|
] {
|
|
assert_eq!(
|
|
change_password(&app, Some(&admin), current, new)
|
|
.await
|
|
.status(),
|
|
StatusCode::BAD_REQUEST
|
|
);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &admin)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn password_change_audit_failure_preserves_password_and_session() {
|
|
let (app, fixture) = auth_app().await;
|
|
let admin = session_cookie(
|
|
&login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "strong-admin-password"}),
|
|
None,
|
|
)
|
|
.await,
|
|
);
|
|
fixture.fail_audit(true);
|
|
assert_eq!(
|
|
change_password(
|
|
&app,
|
|
Some(&admin),
|
|
"strong-admin-password",
|
|
"new-strong-password"
|
|
)
|
|
.await
|
|
.status(),
|
|
StatusCode::SERVICE_UNAVAILABLE
|
|
);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &admin)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
fixture.fail_audit(false);
|
|
assert_eq!(
|
|
login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "strong-admin-password"}),
|
|
None
|
|
)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unified_login_returns_server_identity_and_rotates_the_previous_session() {
|
|
let (app, _) = auth_app().await;
|
|
let key = login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await;
|
|
assert_eq!(key.status(), StatusCode::OK);
|
|
let key_cookie = session_cookie(&key);
|
|
let key_data = response_json(key).await["data"].clone();
|
|
assert_eq!(key_data["role"], "key");
|
|
assert!(key_data["expiresAt"].is_string());
|
|
assert_eq!(key_data.as_object().expect("session object").len(), 2);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &key_cookie).await).await["data"],
|
|
json!({"authenticated": true, "session": key_data})
|
|
);
|
|
|
|
let admin = login(
|
|
&app,
|
|
json!({"mode": "admin", "username": "admin_1", "password": "strong-admin-password"}),
|
|
Some(&key_cookie),
|
|
)
|
|
.await;
|
|
assert_eq!(admin.status(), StatusCode::OK);
|
|
let admin_cookie = session_cookie(&admin);
|
|
let admin_data = response_json(admin).await["data"].clone();
|
|
assert_eq!(admin_data["role"], "admin");
|
|
assert!(admin_data["expiresAt"].is_string());
|
|
assert_eq!(admin_data.as_object().expect("session object").len(), 2);
|
|
assert_ne!(key_cookie, admin_cookie);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &admin_cookie).await).await["data"],
|
|
json!({"authenticated": true, "session": admin_data})
|
|
);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &key_cookie).await).await["data"],
|
|
json!({"authenticated": false, "session": null})
|
|
);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &admin_cookie)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
|
|
let key = login(
|
|
&app,
|
|
json!({"mode": "key", "apiKey": RAW_KEY}),
|
|
Some(&admin_cookie),
|
|
)
|
|
.await;
|
|
assert_eq!(key.status(), StatusCode::OK);
|
|
let renewed = session_cookie(&key);
|
|
assert_ne!(renewed, key_cookie);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &admin_cookie)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &renewed).await).await["data"]["authenticated"],
|
|
true
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn login_rejects_missing_mode_and_unknown_fields_without_replacing_the_session() {
|
|
let (app, _) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
for body in [
|
|
json!({"apiKey": RAW_KEY}),
|
|
json!({"password": "strong-admin-password"}),
|
|
json!({"mode": "key", "unknownField": true, "apiKey": RAW_KEY}),
|
|
json!({"mode": "admin", "unknownField": true, "password": "strong-admin-password"}),
|
|
] {
|
|
let response = login(&app, body, Some(&cookie)).await;
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
|
|
assert!(!response.headers().contains_key(header::SET_COOKIE));
|
|
let error = response_json(response).await.to_string();
|
|
assert!(!error.contains(RAW_KEY));
|
|
assert!(!error.contains("strong-admin-password"));
|
|
}
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &cookie).await).await["data"]["session"]["role"],
|
|
"key"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn valid_key_session_cannot_read_or_mutate_admin_resources() {
|
|
let (app, _) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
for path in [
|
|
"/api/admin/system/version",
|
|
"/api/admin/accounts",
|
|
"/api/admin/client-keys",
|
|
"/api/admin/settings",
|
|
] {
|
|
let response = get(&app, path, &cookie).await;
|
|
assert_eq!(response.status(), StatusCode::FORBIDDEN, "{path}");
|
|
assert_eq!(response_json(response).await["code"], 40301);
|
|
}
|
|
let mut request = json_request(
|
|
Method::POST,
|
|
"/api/admin/settings/admin-api-key/regenerate",
|
|
json!({}),
|
|
);
|
|
request
|
|
.headers_mut()
|
|
.insert(header::COOKIE, cookie.parse().unwrap());
|
|
let response = app.clone().oneshot(request).await.unwrap();
|
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &cookie).await).await["data"]["authenticated"],
|
|
true
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn declared_roles_and_foreign_key_ids_cannot_override_server_authority() {
|
|
let (app, _) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
for body in [
|
|
json!({"mode": "admin", "apiKey": RAW_KEY}),
|
|
json!({"mode": "key", "apiKey": RAW_KEY, "role": "admin"}),
|
|
json!({"mode": "admin", "password": RAW_KEY, "role": "key"}),
|
|
json!({"role": "key", "apiKey": RAW_KEY}),
|
|
json!({"mode": "key", "apiKey": RAW_KEY, "adminUserId": "admin_1"}),
|
|
json!({"mode": "key", "apiKey": RAW_KEY, "clientKeyId": "other-key"}),
|
|
json!({"mode": "superadmin", "apiKey": RAW_KEY}),
|
|
json!({"apiKey": RAW_KEY}),
|
|
] {
|
|
let response = login(&app, body, Some(&cookie)).await;
|
|
assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
|
|
assert!(!response_json(response).await.to_string().contains(RAW_KEY));
|
|
}
|
|
let invalid = login(
|
|
&app,
|
|
json!({"mode": "admin", "password": RAW_KEY}),
|
|
Some(&cookie),
|
|
)
|
|
.await;
|
|
assert_eq!(invalid.status(), StatusCode::UNAUTHORIZED);
|
|
assert!(!invalid.headers().contains_key(header::SET_COOKIE));
|
|
assert_eq!(response_json(invalid).await["code"], 40102);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &cookie).await).await["data"]["session"]["role"],
|
|
"key"
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn data_plane_keys_and_browser_sessions_are_not_interchangeable() {
|
|
let (app, _) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
assert_eq!(
|
|
get(&app, "/v1/models", &cookie).await.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
for (name, value) in [
|
|
(header::AUTHORIZATION.as_str(), format!("Bearer {RAW_KEY}")),
|
|
("x-api-key", RAW_KEY.to_owned()),
|
|
] {
|
|
let request = Request::get("/api/auth/status")
|
|
.header(name, value)
|
|
.body(Body::empty())
|
|
.unwrap();
|
|
let response = app.clone().oneshot(request).await.unwrap();
|
|
assert_eq!(
|
|
response_json(response).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
}
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &format!("cpr_session={RAW_KEY}")).await).await
|
|
["data"]["authenticated"],
|
|
false
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn disabled_key_revokes_its_session_and_reenabling_does_not_restore_it() {
|
|
let (app, store) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
store.enabled.store(false, Ordering::SeqCst);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &cookie).await).await["data"],
|
|
json!({"authenticated": false, "session": null})
|
|
);
|
|
assert_eq!(
|
|
login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
store.enabled.store(true, Ordering::SeqCst);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &cookie).await).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn key_store_outage_is_unavailable_not_expired_or_authenticated() {
|
|
let (app, store) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
store.unavailable.store(true, Ordering::SeqCst);
|
|
for path in ["/api/auth/status", "/api/admin/system/version"] {
|
|
let response = get(&app, path, &cookie).await;
|
|
assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE, "{path}");
|
|
assert!(!response.headers().contains_key(header::SET_COOKIE));
|
|
}
|
|
store.unavailable.store(false, Ordering::SeqCst);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &cookie).await).await["data"]["authenticated"],
|
|
true
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn unknown_auth_routes_return_json_not_found() {
|
|
let (app, _) = auth_app().await;
|
|
let response = app
|
|
.oneshot(empty_request(Method::GET, "/api/auth/unknown"))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
|
assert_eq!(response_json(response).await["code"], 40401);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn session_authentication_requires_the_configured_cookie_name() {
|
|
let (app, _) = auth_app().await;
|
|
let cookie =
|
|
session_cookie(&login(&app, json!({"mode": "key", "apiKey": RAW_KEY}), None).await);
|
|
let unrelated_cookie = cookie.replace("cpr_session", "unrelated_session");
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &unrelated_cookie).await).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn login_wire_never_prints_either_credential_in_debug() {
|
|
for body in [
|
|
json!({"mode": "key", "apiKey": RAW_KEY}),
|
|
json!({"mode": "admin", "password": RAW_KEY}),
|
|
] {
|
|
let request: gateway_api::auth::LoginRequest = serde_json::from_value(body).unwrap();
|
|
let debug = format!("{request:?}");
|
|
assert!(debug.contains("[REDACTED]"));
|
|
assert!(!debug.contains(RAW_KEY));
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn key_auth_should_issue_restore_and_clear_a_unified_cookie() {
|
|
let (app, _) = auth_app().await;
|
|
let login = app
|
|
.clone()
|
|
.oneshot(json_request(
|
|
Method::POST,
|
|
"/api/auth/login",
|
|
json!({ "mode": "key", "apiKey": RAW_KEY }),
|
|
))
|
|
.await
|
|
.expect("client login response");
|
|
|
|
assert_eq!(login.status(), StatusCode::OK);
|
|
assert_eq!(login.headers()[header::CACHE_CONTROL], "no-store");
|
|
let set_cookie = login.headers()[header::SET_COOKIE]
|
|
.to_str()
|
|
.expect("client session cookie")
|
|
.to_owned();
|
|
assert!(set_cookie.starts_with("cpr_session=session_"));
|
|
assert!(set_cookie.contains("Path=/;"));
|
|
assert!(set_cookie.contains("; Secure; HttpOnly; SameSite=Lax"));
|
|
assert!(set_cookie.contains("; Max-Age="));
|
|
assert!(set_cookie.contains("; Expires="));
|
|
assert!(!set_cookie.contains(RAW_KEY));
|
|
let cookie = set_cookie
|
|
.split(';')
|
|
.next()
|
|
.expect("cookie pair")
|
|
.to_owned();
|
|
let login_body = response_json(login).await;
|
|
assert_eq!(login_body["data"]["role"], "key");
|
|
assert!(login_body["data"].get("key").is_none());
|
|
assert!(!login_body.to_string().contains(RAW_KEY));
|
|
|
|
let status = app
|
|
.clone()
|
|
.oneshot(cookie_request(Method::GET, "/api/auth/status", &cookie))
|
|
.await
|
|
.expect("client status response");
|
|
assert_eq!(status.status(), StatusCode::OK);
|
|
let status_body = response_json(status).await;
|
|
assert_eq!(status_body["data"]["authenticated"], true);
|
|
assert_eq!(status_body["data"]["session"]["role"], "key");
|
|
|
|
let logout = app
|
|
.clone()
|
|
.oneshot(cookie_request(Method::POST, "/api/auth/logout", &cookie))
|
|
.await
|
|
.expect("client logout response");
|
|
assert_eq!(logout.status(), StatusCode::OK);
|
|
let cleared = logout.headers()[header::SET_COOKIE]
|
|
.to_str()
|
|
.expect("cleared cookie");
|
|
assert!(cleared.starts_with("cpr_session=;"));
|
|
assert!(cleared.contains("Path=/;"));
|
|
assert!(cleared.contains("Max-Age=0"));
|
|
let status = app
|
|
.oneshot(cookie_request(Method::GET, "/api/auth/status", &cookie))
|
|
.await
|
|
.expect("logged out");
|
|
assert_eq!(response_json(status).await["data"]["authenticated"], false);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn refresh_restores_expired_access_with_only_the_refresh_cookie() {
|
|
let (app, fixture) = auth_app().await;
|
|
let response = login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "strong-admin-password"}),
|
|
None,
|
|
)
|
|
.await;
|
|
let access = session_cookie(&response);
|
|
let renewal = refresh_cookie(&response);
|
|
let cookies: Vec<_> = response
|
|
.headers()
|
|
.get_all(header::SET_COOKIE)
|
|
.iter()
|
|
.map(|value| value.to_str().unwrap())
|
|
.collect();
|
|
assert_eq!(cookies.len(), 2);
|
|
assert!(cookies[0].contains("Path=/;"));
|
|
assert!(cookies[1].contains("Path=/api/auth;"));
|
|
assert!(
|
|
cookies
|
|
.iter()
|
|
.all(|value| value.contains("HttpOnly") && value.contains("SameSite=Lax"))
|
|
);
|
|
assert_eq!(
|
|
response_json(refresh(&app, &access).await).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
fixture.set_session_expiry(
|
|
access.split_once('=').unwrap().1,
|
|
chrono::Utc::now() - chrono::Duration::seconds(1),
|
|
);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &access)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
assert_eq!(
|
|
response_json(get(&app, "/api/auth/status", &renewal).await).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
|
|
let response = refresh(&app, &renewal).await;
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(response.headers()[header::CACHE_CONTROL], "no-store");
|
|
let next_access = session_cookie(&response);
|
|
let next_renewal = refresh_cookie(&response);
|
|
assert_ne!(next_access, access);
|
|
assert_ne!(next_renewal, renewal);
|
|
assert_eq!(response_json(response).await["data"]["authenticated"], true);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &next_access)
|
|
.await
|
|
.status(),
|
|
StatusCode::OK
|
|
);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &access)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
// 模拟响应丢失后的重试,返回同一代 Cookie
|
|
let retry = refresh(&app, &renewal).await;
|
|
assert_eq!(session_cookie(&retry), next_access);
|
|
assert_eq!(refresh_cookie(&retry), next_renewal);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn key_refresh_preserves_fixed_expiry_and_clears_admin_refresh_cookie() {
|
|
let (app, _) = auth_app().await;
|
|
let admin = login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "strong-admin-password"}),
|
|
None,
|
|
)
|
|
.await;
|
|
let renewal = refresh_cookie(&admin);
|
|
let response = login(
|
|
&app,
|
|
json!({"mode": "key", "apiKey": RAW_KEY}),
|
|
Some(&renewal),
|
|
)
|
|
.await;
|
|
assert_eq!(refresh_cookie(&response), "cpr_refresh=");
|
|
assert!(
|
|
response
|
|
.headers()
|
|
.get_all(header::SET_COOKIE)
|
|
.iter()
|
|
.any(|value| {
|
|
let value = value.to_str().unwrap();
|
|
value.starts_with("cpr_refresh=")
|
|
&& value.contains("Max-Age=0")
|
|
&& value.contains("Path=/api/auth;")
|
|
})
|
|
);
|
|
let cookie = session_cookie(&response);
|
|
let original = response_json(response).await["data"]["expiresAt"].clone();
|
|
let response = refresh(&app, &cookie).await;
|
|
assert_eq!(session_cookie(&response), cookie);
|
|
let data = response_json(response).await;
|
|
assert_eq!(data["data"]["authenticated"], true);
|
|
assert_eq!(data["data"]["session"]["expiresAt"], original);
|
|
assert_eq!(
|
|
response_json(refresh(&app, &renewal).await).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn refresh_after_logout_or_password_change_cannot_recreate_a_session() {
|
|
let (app, fixture) = auth_app().await;
|
|
for password_change in [false, true] {
|
|
let login_response = login(
|
|
&app,
|
|
json!({"mode": "admin", "password": "strong-admin-password"}),
|
|
None,
|
|
)
|
|
.await;
|
|
let cookie = session_cookie(&login_response);
|
|
let renewal = refresh_cookie(&login_response);
|
|
let response = if password_change {
|
|
change_password(
|
|
&app,
|
|
Some(&cookie),
|
|
"strong-admin-password",
|
|
"new-strong-admin-password",
|
|
)
|
|
.await
|
|
} else {
|
|
fixture.set_session_expiry(
|
|
cookie.split_once('=').unwrap().1,
|
|
chrono::Utc::now() - chrono::Duration::seconds(1),
|
|
);
|
|
app.clone()
|
|
.oneshot(cookie_request(Method::POST, "/api/auth/logout", &renewal))
|
|
.await
|
|
.unwrap()
|
|
};
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert_eq!(
|
|
response
|
|
.headers()
|
|
.get_all(header::SET_COOKIE)
|
|
.iter()
|
|
.count(),
|
|
2
|
|
);
|
|
assert!(
|
|
response
|
|
.headers()
|
|
.get_all(header::SET_COOKIE)
|
|
.iter()
|
|
.all(|value| value.to_str().unwrap().contains("Max-Age=0"))
|
|
);
|
|
let response = app
|
|
.clone()
|
|
.oneshot(cookie_request(Method::POST, "/api/auth/refresh", &renewal))
|
|
.await
|
|
.unwrap();
|
|
assert_eq!(response.status(), StatusCode::OK);
|
|
assert!(!response.headers().contains_key(header::SET_COOKIE));
|
|
assert_eq!(
|
|
response_json(response).await["data"]["authenticated"],
|
|
false
|
|
);
|
|
assert_eq!(
|
|
get(&app, "/api/admin/system/version", &cookie)
|
|
.await
|
|
.status(),
|
|
StatusCode::UNAUTHORIZED
|
|
);
|
|
}
|
|
}
|