收口 Rust Web 入口与 AI 写入链
- 将 3000 主入口继续收口到 mnote-web,补齐 /favicon.ico、/api/auth、session alias、AI run 等 Rust Web 路由边界。 - 更新登录页与 Convex Auth 代理,支持测试账号快速登录写入真实 Convex Auth cookie。 - 推进页面设置、Wolai 对齐、Phase 7 AI kernel/CLI-first 设计文档与相关 smoke 脚本。 - 更新 leptos-tiptap 生成资产、mnote-cli/bridge-runtime、前端依赖和 dev/prod 启动脚本。
This commit is contained in:
@@ -19,6 +19,10 @@ use std::time::Duration;
|
||||
const HEADER_MNOTE_WEB_OWNER: &str = "x-mnote-web-owner";
|
||||
const HEADER_MNOTE_LEGACY_UPSTREAM: &str = "x-mnote-legacy-upstream";
|
||||
const COOKIE_RECENT_PAGE_ID: &str = "mnote_recent_page_id";
|
||||
const COOKIE_CONVEX_AUTH_JWT: &str = "__convexAuthJWT";
|
||||
const COOKIE_CONVEX_AUTH_REFRESH_TOKEN: &str = "__convexAuthRefreshToken";
|
||||
const COOKIE_MNOTE_WEB_CONVEX_TOKEN: &str = "mnote_web_convex_token";
|
||||
const COOKIE_MNOTE_WEB_DEV_SESSION: &str = "mnote_web_dev_session";
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -55,7 +59,16 @@ pub async fn gateway_health(State(state): State<AppState>) -> Response {
|
||||
response
|
||||
}
|
||||
|
||||
pub async fn auth_entry(
|
||||
pub async fn favicon() -> Response {
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::NO_CONTENT)
|
||||
.body(Body::empty())
|
||||
.unwrap_or_else(|_| StatusCode::NO_CONTENT.into_response());
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
response
|
||||
}
|
||||
|
||||
pub async fn auth_api(
|
||||
State(state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
request: Request<Body>,
|
||||
@@ -64,6 +77,49 @@ pub async fn auth_entry(
|
||||
return legacy_next_proxy(State(state), Extension(context), request).await;
|
||||
}
|
||||
|
||||
let body = axum::body::to_bytes(request.into_body(), 256 * 1024)
|
||||
.await
|
||||
.map_err(|error| WebError::bad_request(format!("读取登录请求失败: {error}")))?;
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"auth_bad_request",
|
||||
format!("登录请求不是合法 JSON: {error}"),
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web")
|
||||
})?;
|
||||
let action = payload
|
||||
.get("action")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
if action != "auth:signIn" && action != "auth:signOut" {
|
||||
return Err(WebError::bad_request_code(
|
||||
"auth_action_unsupported",
|
||||
"Rust gateway 当前仅支持 Convex Auth 登录与登出动作。",
|
||||
)
|
||||
.with_context(&context)
|
||||
.with_header(HEADER_MNOTE_WEB_OWNER, "mnote-web"));
|
||||
}
|
||||
|
||||
let convex_response = run_convex_auth_action(&state, &context, &payload).await?;
|
||||
Ok(build_auth_proxy_response(&convex_response, &context))
|
||||
}
|
||||
|
||||
pub async fn auth_entry(
|
||||
State(_state): State<AppState>,
|
||||
Extension(context): Extension<RequestContext>,
|
||||
_request: Request<Body>,
|
||||
) -> Result<Response, WebError> {
|
||||
if has_real_auth_context(&context) {
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header(header::LOCATION, "/")
|
||||
.body(Body::empty())
|
||||
.map_err(|error| WebError::internal(format!("认证跳转响应构造失败: {error}")))?;
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
let content = crate::ssr::render_view(crate::ssr::pages::auth::AuthPage());
|
||||
let mut response = Html(format!(
|
||||
r#"<!doctype html>
|
||||
@@ -90,6 +146,16 @@ pub async fn root_entry(
|
||||
Extension(context): Extension<RequestContext>,
|
||||
Query(query): Query<RootEntryQuery>,
|
||||
) -> Result<Response, WebError> {
|
||||
if !has_real_auth_context(&context) {
|
||||
let mut response = Response::builder()
|
||||
.status(StatusCode::SEE_OTHER)
|
||||
.header(header::LOCATION, "/auth")
|
||||
.body(Body::empty())
|
||||
.map_err(|error| WebError::internal(format!("认证入口跳转响应构造失败: {error}")))?;
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
return Ok(response);
|
||||
}
|
||||
|
||||
let workspace_id =
|
||||
resolve_root_workspace_id(&state, &context, query.workspace_id.as_deref()).await?;
|
||||
let requested_page_id = normalize_optional_id(query.page_id.as_deref());
|
||||
@@ -365,6 +431,195 @@ fn normalize_optional_id(value: Option<&str>) -> Option<&str> {
|
||||
value.map(str::trim).filter(|value| !value.is_empty())
|
||||
}
|
||||
|
||||
fn has_real_auth_context(context: &RequestContext) -> bool {
|
||||
let actor_id = context.auth.actor_id.trim();
|
||||
if !actor_id.is_empty() && actor_id != "anonymous" {
|
||||
return true;
|
||||
}
|
||||
|
||||
extract_cookie_value(context, COOKIE_CONVEX_AUTH_JWT).is_some()
|
||||
|| extract_cookie_value(context, COOKIE_MNOTE_WEB_CONVEX_TOKEN).is_some()
|
||||
}
|
||||
|
||||
async fn run_convex_auth_action(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
payload: &serde_json::Value,
|
||||
) -> Result<serde_json::Value, WebError> {
|
||||
let convex_url = state
|
||||
.config()
|
||||
.convex_url
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.ok_or_else(|| {
|
||||
WebError::service_unavailable_code(
|
||||
"convex_config_missing",
|
||||
"缺少 CONVEX_SELF_HOSTED_URL / NEXT_PUBLIC_CONVEX_URL,无法执行 Convex Auth。",
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "convex_auth_url")
|
||||
.with_header("x-upstream-service", "convex")
|
||||
})?;
|
||||
let action = payload
|
||||
.get("action")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
let mut args = payload.get("args").cloned().unwrap_or_else(|| json!({}));
|
||||
if action == "auth:signIn"
|
||||
&& args
|
||||
.get("refreshToken")
|
||||
.map(|value| !value.is_null())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if let Some(refresh_token) = extract_cookie_value(context, COOKIE_CONVEX_AUTH_REFRESH_TOKEN)
|
||||
{
|
||||
args["refreshToken"] = serde_json::Value::String(refresh_token);
|
||||
}
|
||||
}
|
||||
|
||||
let request_body = json!({
|
||||
"path": action,
|
||||
"format": "convex_encoded_json",
|
||||
"args": [args],
|
||||
});
|
||||
let client = reqwest::Client::builder()
|
||||
.timeout(Duration::from_secs(20))
|
||||
.build()
|
||||
.map_err(|error| {
|
||||
WebError::internal(format!("Convex Auth HTTP 客户端创建失败: {error}"))
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "convex_auth_client")
|
||||
.with_header("x-upstream-service", "convex")
|
||||
})?;
|
||||
let mut request = client
|
||||
.post(format!("{}/api/action", convex_url.trim_end_matches('/')))
|
||||
.header("Content-Type", "application/json")
|
||||
.header("Convex-Client", "mnote-web")
|
||||
.json(&request_body);
|
||||
if let Some(token) = extract_cookie_value(context, COOKIE_CONVEX_AUTH_JWT) {
|
||||
request = request.header(header::AUTHORIZATION, format!("Bearer {token}"));
|
||||
}
|
||||
|
||||
let response = request.send().await.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"convex_auth_proxy_error",
|
||||
format!("Convex Auth 请求失败: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "convex_auth_action")
|
||||
.with_header("x-upstream-service", "convex")
|
||||
})?;
|
||||
let status = response.status();
|
||||
let value: serde_json::Value = response.json().await.map_err(|error| {
|
||||
WebError::bad_gateway_code(
|
||||
"convex_auth_response_invalid",
|
||||
format!("Convex Auth 响应不是合法 JSON: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "convex_auth_decode")
|
||||
.with_header("x-upstream-service", "convex")
|
||||
})?;
|
||||
if !status.is_success() && status.as_u16() != 560 {
|
||||
return Err(WebError::bad_gateway_code(
|
||||
"convex_auth_upstream_error",
|
||||
format!("Convex Auth 返回 HTTP {}: {}", status.as_u16(), value),
|
||||
)
|
||||
.with_context(context)
|
||||
.with_header("x-error-phase", "convex_auth_status")
|
||||
.with_header("x-upstream-service", "convex"));
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn build_auth_proxy_response(
|
||||
convex_response: &serde_json::Value,
|
||||
context: &RequestContext,
|
||||
) -> Response {
|
||||
if convex_response
|
||||
.get("status")
|
||||
.and_then(|value| value.as_str())
|
||||
!= Some("success")
|
||||
{
|
||||
let message = convex_response
|
||||
.get("errorMessage")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or("Convex Auth 登录失败。");
|
||||
let mut response = axum::Json(json!({ "error": message })).into_response();
|
||||
*response.status_mut() = StatusCode::BAD_REQUEST;
|
||||
clear_auth_cookies(response.headers_mut());
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
context.apply_response_headers(response.headers_mut());
|
||||
return response;
|
||||
}
|
||||
|
||||
let value = convex_response
|
||||
.get("value")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| json!({}));
|
||||
let tokens = value.get("tokens");
|
||||
let mut response_body = value.clone();
|
||||
if let Some(tokens) = tokens {
|
||||
if tokens.is_null() {
|
||||
response_body["tokens"] = serde_json::Value::Null;
|
||||
} else if let Some(token) = tokens.get("token").and_then(|value| value.as_str()) {
|
||||
response_body["tokens"] = json!({
|
||||
"token": token,
|
||||
"refreshToken": "dummy",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let mut response = axum::Json(response_body).into_response();
|
||||
if let Some(tokens) = tokens {
|
||||
if tokens.is_null() {
|
||||
clear_auth_cookies(response.headers_mut());
|
||||
} else {
|
||||
set_auth_cookie_from_value(
|
||||
response.headers_mut(),
|
||||
COOKIE_CONVEX_AUTH_JWT,
|
||||
tokens.get("token"),
|
||||
);
|
||||
set_auth_cookie_from_value(
|
||||
response.headers_mut(),
|
||||
COOKIE_CONVEX_AUTH_REFRESH_TOKEN,
|
||||
tokens.get("refreshToken"),
|
||||
);
|
||||
expire_cookie(response.headers_mut(), COOKIE_MNOTE_WEB_DEV_SESSION);
|
||||
}
|
||||
}
|
||||
stamp_gateway_headers(response.headers_mut(), false);
|
||||
context.apply_response_headers(response.headers_mut());
|
||||
response
|
||||
}
|
||||
|
||||
fn set_auth_cookie_from_value(
|
||||
headers: &mut axum::http::HeaderMap,
|
||||
name: &'static str,
|
||||
value: Option<&serde_json::Value>,
|
||||
) {
|
||||
let Some(value) = value.and_then(|value| value.as_str()) else {
|
||||
return;
|
||||
};
|
||||
let cookie = format!("{name}={value}; Path=/; HttpOnly; SameSite=Lax");
|
||||
if let Ok(value) = HeaderValue::from_str(&cookie) {
|
||||
headers.append(header::SET_COOKIE, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_auth_cookies(headers: &mut axum::http::HeaderMap) {
|
||||
expire_cookie(headers, COOKIE_CONVEX_AUTH_JWT);
|
||||
expire_cookie(headers, COOKIE_CONVEX_AUTH_REFRESH_TOKEN);
|
||||
expire_cookie(headers, COOKIE_MNOTE_WEB_DEV_SESSION);
|
||||
}
|
||||
|
||||
fn expire_cookie(headers: &mut axum::http::HeaderMap, name: &'static str) {
|
||||
let cookie = format!("{name}=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0");
|
||||
if let Ok(value) = HeaderValue::from_str(&cookie) {
|
||||
headers.append(header::SET_COOKIE, value);
|
||||
}
|
||||
}
|
||||
|
||||
fn choose_root_entry_active_page_id(
|
||||
requested_page_id: Option<&str>,
|
||||
recent_page_id: Option<&str>,
|
||||
@@ -466,6 +721,14 @@ mod tests {
|
||||
fn app_with_config(
|
||||
legacy_next_base_url: String,
|
||||
enable_legacy_next_compat: bool,
|
||||
) -> axum::Router {
|
||||
app_with_config_and_convex_url(legacy_next_base_url, enable_legacy_next_compat, None)
|
||||
}
|
||||
|
||||
fn app_with_config_and_convex_url(
|
||||
legacy_next_base_url: String,
|
||||
enable_legacy_next_compat: bool,
|
||||
convex_url: Option<String>,
|
||||
) -> axum::Router {
|
||||
build_app(AppState::new(AppConfig {
|
||||
service_name: "mnote-web".into(),
|
||||
@@ -477,7 +740,7 @@ mod tests {
|
||||
enable_debug_shell_routes: false,
|
||||
hermes_base_path: "/api/hermes".into(),
|
||||
compat_next_base_path: "/api/compat/next".into(),
|
||||
convex_url: None,
|
||||
convex_url,
|
||||
convex_admin_key: None,
|
||||
allow_dev_fixtures: true,
|
||||
query_fixtures_json: None,
|
||||
@@ -488,6 +751,33 @@ mod tests {
|
||||
}))
|
||||
}
|
||||
|
||||
async fn spawn_convex_auth_upstream() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("convex auth listener");
|
||||
let addr = listener.local_addr().expect("convex auth addr");
|
||||
let app = axum::Router::new().route(
|
||||
"/api/action",
|
||||
post(|| async {
|
||||
axum::Json(serde_json::json!({
|
||||
"status": "success",
|
||||
"value": {
|
||||
"tokens": {
|
||||
"token": "jwt-demo",
|
||||
"refreshToken": "refresh-demo"
|
||||
}
|
||||
}
|
||||
}))
|
||||
}),
|
||||
);
|
||||
tokio::spawn(async move {
|
||||
axum::serve(listener, app)
|
||||
.await
|
||||
.expect("convex auth server");
|
||||
});
|
||||
format!("http://{addr}")
|
||||
}
|
||||
|
||||
async fn spawn_legacy_auth_upstream() -> String {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
@@ -598,6 +888,8 @@ mod tests {
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/")
|
||||
.header("x-mnote-actor-id", "user_real")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
@@ -674,6 +966,8 @@ mod tests {
|
||||
.uri("/")
|
||||
.header("cookie", "mnote_recent_page_id=page_child")
|
||||
.header("x-mnote-workspace-id", "ws_demo")
|
||||
.header("x-mnote-actor-id", "user_real")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
@@ -691,6 +985,57 @@ mod tests {
|
||||
assert!(html.contains(r#"data-root-active-page-id="page_child""#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_redirects_anonymous_viewer_to_auth() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get(header::LOCATION)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("/auth")
|
||||
);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-web-owner")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote-web")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn root_entry_allows_forwarded_actor_to_enter_workspace() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/")
|
||||
.header("x-mnote-actor-id", "user_real")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains(r#"data-mnote-shell="workspace""#));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_returns_gateway_fallback_shell_when_compat_disabled() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
@@ -720,7 +1065,77 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_uses_legacy_next_login_ui_when_compat_enabled() {
|
||||
async fn favicon_is_handled_by_gateway_when_compat_disabled() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/favicon.ico")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::NO_CONTENT);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-web-owner")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote-web")
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_api_sets_convex_auth_cookies_when_compat_disabled() {
|
||||
let convex_url = spawn_convex_auth_upstream().await;
|
||||
let response = app_with_config_and_convex_url(
|
||||
"http://127.0.0.1:3100".into(),
|
||||
false,
|
||||
Some(convex_url),
|
||||
)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/auth")
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(
|
||||
r#"{"action":"auth:signIn","args":{"provider":"password","params":{"email":"mnote.e2e@example.com","password":"MnoteE2E123!","flow":"signIn"}}}"#,
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-web-owner")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("mnote-web")
|
||||
);
|
||||
let cookies = response.headers().get_all(header::SET_COOKIE);
|
||||
let values = cookies
|
||||
.iter()
|
||||
.map(|value| value.to_str().unwrap_or_default())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(values
|
||||
.iter()
|
||||
.any(|value| value.contains("__convexAuthJWT=jwt-demo")));
|
||||
assert!(values
|
||||
.iter()
|
||||
.any(|value| value.contains("__convexAuthRefreshToken=refresh-demo")));
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: serde_json::Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["tokens"]["token"], "jwt-demo");
|
||||
assert_eq!(payload["tokens"]["refreshToken"], "dummy");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_uses_mnote_web_login_ui_when_compat_enabled() {
|
||||
let legacy_base_url = spawn_legacy_auth_upstream().await;
|
||||
let response = app_with_legacy_next_base_url(legacy_base_url)
|
||||
.oneshot(
|
||||
@@ -738,44 +1153,39 @@ mod tests {
|
||||
.headers()
|
||||
.get("x-mnote-legacy-upstream")
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("next-app-router")
|
||||
None
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let html = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert!(html.contains(r#"data-mnote-shell="auth""#));
|
||||
assert!(html.contains("邮箱登录"));
|
||||
assert!(html.contains("测试账号快速登录"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_entry_proxies_post_to_legacy_next_when_compat_enabled() {
|
||||
let legacy_base_url = spawn_legacy_auth_upstream().await;
|
||||
let response = app_with_legacy_next_base_url(legacy_base_url)
|
||||
async fn auth_entry_redirects_authenticated_viewer_to_root() {
|
||||
let response = app_with_config("http://127.0.0.1:3100".into(), false)
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/auth")
|
||||
.header("origin", "http://127.0.0.1:3000")
|
||||
.header("referer", "http://127.0.0.1:3000/auth")
|
||||
.header("x-mnote-actor-id", "user_real")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::empty())
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::SEE_OTHER);
|
||||
assert_eq!(
|
||||
response
|
||||
.headers()
|
||||
.get("x-mnote-legacy-upstream")
|
||||
.get(header::LOCATION)
|
||||
.and_then(|value| value.to_str().ok()),
|
||||
Some("next-app-router")
|
||||
Some("/")
|
||||
);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let text = String::from_utf8(body.to_vec()).expect("utf8");
|
||||
assert_eq!(text, "auth-post-ok");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Reference in New Issue
Block a user