test(app-server): add unit tests for auth, CORS, and JSON-RPC helpers (#2448)
* test(app-server): add unit tests for auth, CORS, and JSON-RPC helpers Add 16 new unit tests (total 20) covering: - resolve_auth_token: empty token rejection, auto-generation, explicit token, insecure loopback - cors_layer: default origins, extra origins, empty origin skipping - JSON-RPC helpers: params_or_object, jsonrpc_result, jsonrpc_error, error codes - Default CORS origins verification * test(app-server): redact auth token debug output --------- Co-authored-by: Hu Qiantao <huqiantao@HudeMacBook-Air.local> Co-authored-by: Hunter B <hmbown@gmail.com>
This commit is contained in:
@@ -38,7 +38,7 @@ const DEFAULT_CORS_ORIGINS: &[&str] = &[
|
||||
"tauri://localhost",
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Clone)]
|
||||
pub struct AppServerOptions {
|
||||
pub listen: SocketAddr,
|
||||
pub config_path: Option<PathBuf>,
|
||||
@@ -47,6 +47,21 @@ pub struct AppServerOptions {
|
||||
pub cors_origins: Vec<String>,
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AppServerOptions {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("AppServerOptions")
|
||||
.field("listen", &self.listen)
|
||||
.field("config_path", &self.config_path)
|
||||
.field(
|
||||
"auth_token",
|
||||
&self.auth_token.as_ref().map(|_| "<redacted>"),
|
||||
)
|
||||
.field("insecure_no_auth", &self.insecure_no_auth)
|
||||
.field("cors_origins", &self.cors_origins)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct AppState {
|
||||
config_path: Option<PathBuf>,
|
||||
@@ -1069,4 +1084,155 @@ mod tests {
|
||||
|
||||
assert_eq!(response.data["value"], "sk-deepseek-secret");
|
||||
}
|
||||
|
||||
// ── resolve_auth_token ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn auth_token_empty_string_fails() {
|
||||
let options = AppServerOptions {
|
||||
listen: "127.0.0.1:0".parse().expect("addr"),
|
||||
config_path: None,
|
||||
auth_token: Some(" ".to_string()),
|
||||
insecure_no_auth: false,
|
||||
cors_origins: Vec::new(),
|
||||
};
|
||||
let err = resolve_auth_token(&options).expect_err("empty token should fail");
|
||||
assert!(err.to_string().contains("cannot be empty"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_token_generated_when_none_provided() {
|
||||
let options = AppServerOptions {
|
||||
listen: "127.0.0.1:0".parse().expect("addr"),
|
||||
config_path: None,
|
||||
auth_token: None,
|
||||
insecure_no_auth: false,
|
||||
cors_origins: Vec::new(),
|
||||
};
|
||||
let token = resolve_auth_token(&options).unwrap();
|
||||
assert!(token.is_some());
|
||||
assert!(token.unwrap().starts_with("cwapp_"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auth_token_explicit_is_preserved() {
|
||||
let options = AppServerOptions {
|
||||
listen: "127.0.0.1:0".parse().expect("addr"),
|
||||
config_path: None,
|
||||
auth_token: Some("my-secret".to_string()),
|
||||
insecure_no_auth: false,
|
||||
cors_origins: Vec::new(),
|
||||
};
|
||||
let token = resolve_auth_token(&options).unwrap();
|
||||
assert_eq!(token.as_deref(), Some("my-secret"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insecure_no_auth_on_loopback_returns_none() {
|
||||
let options = AppServerOptions {
|
||||
listen: "127.0.0.1:0".parse().expect("addr"),
|
||||
config_path: None,
|
||||
auth_token: None,
|
||||
insecure_no_auth: true,
|
||||
cors_origins: Vec::new(),
|
||||
};
|
||||
let token = resolve_auth_token(&options).unwrap();
|
||||
assert!(token.is_none());
|
||||
}
|
||||
|
||||
// ── cors_layer ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cors_layer_includes_default_origins() {
|
||||
let layer = cors_layer(&[]);
|
||||
// Just verify it doesn't panic and creates successfully
|
||||
let _ = layer;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_layer_adds_extra_origins() {
|
||||
let extras = vec!["https://example.com".to_string()];
|
||||
let layer = cors_layer(&extras);
|
||||
let _ = layer;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cors_layer_skips_empty_origins() {
|
||||
let extras = vec!["".to_string(), " ".to_string()];
|
||||
let layer = cors_layer(&extras);
|
||||
let _ = layer;
|
||||
}
|
||||
|
||||
// ── JsonRpc helpers ────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn params_or_object_returns_object_for_null() {
|
||||
let result = params_or_object(Value::Null);
|
||||
assert_eq!(result, json!({}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn params_or_object_passthrough_for_non_null() {
|
||||
let input = json!({"key": "value"});
|
||||
let result = params_or_object(input.clone());
|
||||
assert_eq!(result, input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_result_format() {
|
||||
let result = jsonrpc_result(Some(json!(1)), json!({"ok": true}));
|
||||
assert_eq!(result["jsonrpc"], "2.0");
|
||||
assert_eq!(result["id"], 1);
|
||||
assert_eq!(result["result"]["ok"], true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_result_null_id() {
|
||||
let result = jsonrpc_result(None, json!(null));
|
||||
assert_eq!(result["id"], Value::Null);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_error_format() {
|
||||
let err = jsonrpc_error(Some(json!(2)), JsonRpcError::internal("oops"));
|
||||
assert_eq!(err["jsonrpc"], "2.0");
|
||||
assert_eq!(err["id"], 2);
|
||||
assert_eq!(err["error"]["code"], -32603);
|
||||
assert_eq!(err["error"]["message"], "oops");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jsonrpc_error_codes() {
|
||||
assert_eq!(JsonRpcError::parse_error("").code, -32700);
|
||||
assert_eq!(JsonRpcError::invalid_request("").code, -32600);
|
||||
assert_eq!(JsonRpcError::method_not_found("x").code, -32601);
|
||||
assert_eq!(JsonRpcError::invalid_params("").code, -32602);
|
||||
assert_eq!(JsonRpcError::internal("").code, -32603);
|
||||
}
|
||||
|
||||
// ── AppServerOptions ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn app_server_options_debug_does_not_leak_token() {
|
||||
let options = AppServerOptions {
|
||||
listen: "127.0.0.1:8080".parse().expect("addr"),
|
||||
config_path: None,
|
||||
auth_token: Some("secret-token".to_string()),
|
||||
insecure_no_auth: false,
|
||||
cors_origins: vec!["https://example.com".to_string()],
|
||||
};
|
||||
let debug = format!("{options:?}");
|
||||
assert!(!debug.contains("secret-token"));
|
||||
assert!(debug.contains("<redacted>"));
|
||||
assert!(debug.contains("8080"));
|
||||
}
|
||||
|
||||
// ── Default CORS origins ──────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn default_cors_origins_include_common_dev_ports() {
|
||||
assert!(DEFAULT_CORS_ORIGINS.contains(&"http://localhost:3000"));
|
||||
assert!(DEFAULT_CORS_ORIGINS.contains(&"http://localhost:5173"));
|
||||
assert!(DEFAULT_CORS_ORIGINS.contains(&"tauri://localhost"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user