Files
freedak f7a720204a Update: 将子项目从 submodule 转为完整内容
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用
- 添加所有子项目的完整源代码
- 保留原始 .git 为 .git.bak 备份
2026-07-04 19:20:46 +08:00

692 lines
23 KiB
Rust

mod common;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use serde_json::json;
use tower::ServiceExt;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
use common::{body_json, build_app_with_noop_opener, json_with_token, setup_and_login};
// ---------------------------------------------------------------------------
// Helper: build multipart/form-data body
// ---------------------------------------------------------------------------
struct MultipartBuilder {
boundary: String,
parts: Vec<u8>,
}
impl MultipartBuilder {
fn new() -> Self {
Self {
boundary: "----TestBoundary7MA4YWxkTrZu0gW".to_owned(),
parts: Vec::new(),
}
}
fn add_text(mut self, name: &str, value: &str) -> Self {
self.parts
.extend_from_slice(format!("--{}\r\n", self.boundary).as_bytes());
self.parts
.extend_from_slice(format!("Content-Disposition: form-data; name=\"{name}\"\r\n\r\n").as_bytes());
self.parts.extend_from_slice(value.as_bytes());
self.parts.extend_from_slice(b"\r\n");
self
}
fn add_file(mut self, name: &str, filename: &str, mime: &str, data: &[u8]) -> Self {
self.parts
.extend_from_slice(format!("--{}\r\n", self.boundary).as_bytes());
self.parts.extend_from_slice(
format!("Content-Disposition: form-data; name=\"{name}\"; filename=\"{filename}\"\r\n").as_bytes(),
);
self.parts
.extend_from_slice(format!("Content-Type: {mime}\r\n\r\n").as_bytes());
self.parts.extend_from_slice(data);
self.parts.extend_from_slice(b"\r\n");
self
}
fn build(mut self) -> (String, Vec<u8>) {
self.parts
.extend_from_slice(format!("--{}--\r\n", self.boundary).as_bytes());
let content_type = format!("multipart/form-data; boundary={}", self.boundary);
(content_type, self.parts)
}
}
fn multipart_request(uri: &str, content_type: &str, body: Vec<u8>, token: &str, csrf: &str) -> Request<Body> {
Request::builder()
.method("POST")
.uri(uri)
.header("content-type", content_type)
.header("authorization", format!("Bearer {token}"))
.header("x-csrf-token", csrf)
.header("cookie", format!("nomifun-csrf-token={csrf}"))
.body(Body::from(body))
.unwrap()
}
async fn set_stt_config(app: &mut axum::Router, token: &str, csrf: &str, config: serde_json::Value) {
let req = json_with_token(
"PUT",
"/api/settings/client",
json!({ "speechToText": config }),
token,
csrf,
);
let resp = app.clone().oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
// ===========================================================================
// A. Shell Operations
// ===========================================================================
// SH-2: open-file — file not found
#[tokio::test]
async fn sh2_open_file_not_found() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/open-file",
json!({ "file_path": "/nonexistent/file.txt" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["success"], false);
}
// SH-4: show-item-in-folder — path not found
#[tokio::test]
async fn sh4_show_item_in_folder_not_found() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/show-item-in-folder",
json!({ "file_path": "/nonexistent/path" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
// SH-6: open-external — command injection attempt
#[tokio::test]
async fn sh6_open_external_command_injection() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/open-external",
json!({ "url": "; rm -rf /" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["success"], false);
}
// SH-7: open-external — disallowed scheme
#[tokio::test]
async fn sh7_open_external_file_scheme() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/open-external",
json!({ "url": "file:///etc/passwd" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
// SH-8: check-tool-installed — terminal always true
#[tokio::test]
async fn sh8_check_tool_terminal() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/check-tool-installed",
json!({ "tool": "terminal" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert_eq!(json["data"]["installed"], true);
}
// SH-9: check-tool-installed — explorer always true
#[tokio::test]
async fn sh9_check_tool_explorer() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/check-tool-installed",
json!({ "tool": "explorer" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert_eq!(json["data"]["installed"], true);
}
// SH-10: check-tool-installed — vscode (result depends on environment)
#[tokio::test]
async fn sh10_check_tool_vscode() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/check-tool-installed",
json!({ "tool": "vscode" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert!(json["data"]["installed"].is_boolean());
}
// SH-12: open-folder-with — directory not found
#[tokio::test]
async fn sh12_open_folder_with_nonexistent() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/open-folder-with",
json!({ "folder_path": "/nonexistent/dir", "tool": "explorer" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
// SH-13: open-file — missing filePath
#[tokio::test]
async fn sh13_open_file_missing_field() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token("POST", "/api/shell/open-file", json!({}), &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
// SH-14: open-external — empty URL
#[tokio::test]
async fn sh14_open_external_empty_url() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token("POST", "/api/shell/open-external", json!({ "url": "" }), &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
// ===========================================================================
// B. Speech-to-Text (STT)
// ===========================================================================
// ST-3: STT not enabled
#[tokio::test]
async fn st3_stt_disabled() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({ "enabled": false, "provider": "openai" }),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["code"], "STT_DISABLED");
}
// ST-4: STT config not set (treated as disabled)
#[tokio::test]
async fn st4_stt_config_not_set() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["code"], "STT_DISABLED");
}
// ST-5: OpenAI not configured (missing API key)
#[tokio::test]
async fn st5_openai_not_configured() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({
"enabled": true,
"provider": "openai",
"openai": { "api_key": "", "model": "whisper-1" }
}),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["code"], "STT_OPENAI_NOT_CONFIGURED");
}
// ST-6: Deepgram not configured (missing API key)
#[tokio::test]
async fn st6_deepgram_not_configured() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({
"enabled": true,
"provider": "deepgram",
"deepgram": { "api_key": "", "model": "nova-2" }
}),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["code"], "STT_DEEPGRAM_NOT_CONFIGURED");
}
// ST-7: STT third-party API failure (fake API key → 401)
#[tokio::test]
async fn st7_stt_api_failure() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/audio/transcriptions"))
.respond_with(ResponseTemplate::new(401).set_body_string("Unauthorized"))
.mount(&mock_server)
.await;
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({
"enabled": true,
"provider": "openai",
"openai": {
"api_key": "sk-fake-key",
"base_url": mock_server.uri(),
"model": "whisper-1"
}
}),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_GATEWAY);
let json = body_json(resp).await;
assert_eq!(json["code"], "STT_REQUEST_FAILED");
}
// ST-8: multipart missing fileName
#[tokio::test]
async fn st8_multipart_missing_filename() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({ "enabled": true, "provider": "openai", "openai": { "api_key": "sk-test", "model": "whisper-1" } }),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["success"], false);
}
// ST-9: multipart missing file
#[tokio::test]
async fn st9_multipart_missing_file() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({ "enabled": true, "provider": "openai", "openai": { "api_key": "sk-test", "model": "whisper-1" } }),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["success"], false);
}
// ST-1: OpenAI transcription success (mocked)
#[tokio::test]
async fn st1_openai_transcription_success() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/audio/transcriptions"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "text": "hello world" })))
.mount(&mock_server)
.await;
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({
"enabled": true,
"provider": "openai",
"openai": {
"api_key": "sk-test-key",
"base_url": mock_server.uri(),
"model": "whisper-1"
}
}),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert_eq!(json["data"]["text"], "hello world");
assert_eq!(json["data"]["model"], "whisper-1");
assert_eq!(json["data"]["provider"], "openai");
}
// ST-2: Deepgram transcription success (mocked)
#[tokio::test]
async fn st2_deepgram_transcription_success() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"metadata": {
"model_info": {
"key": { "name": "nova-2-general" }
}
},
"results": {
"channels": [{
"alternatives": [{
"transcript": "hello from deepgram"
}]
}]
}
})))
.mount(&mock_server)
.await;
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({
"enabled": true,
"provider": "deepgram",
"deepgram": {
"api_key": "dg-test-key",
"base_url": mock_server.uri(),
"model": "nova-2"
}
}),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert_eq!(json["data"]["text"], "hello from deepgram");
assert_eq!(json["data"]["provider"], "deepgram");
}
// ST-10: languageHint passed through
#[tokio::test]
async fn st10_language_hint_passed() {
let mock_server = MockServer::start().await;
Mock::given(method("POST"))
.and(path("/v1/audio/transcriptions"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "text": "你好世界" })))
.mount(&mock_server)
.await;
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({
"enabled": true,
"provider": "openai",
"openai": {
"api_key": "sk-test-key",
"base_url": mock_server.uri(),
"model": "whisper-1"
}
}),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.add_text("languageHint", "zh")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let json = body_json(resp).await;
assert_eq!(json["success"], true);
assert_eq!(json["data"]["text"], "你好世界");
}
// ===========================================================================
// C. Authentication
// ===========================================================================
// AU-1: unauthenticated shell request rejected
#[tokio::test]
async fn au1_shell_unauthenticated() {
let (app, _services) = build_app_with_noop_opener().await;
let req = Request::builder()
.method("POST")
.uri("/api/shell/open-file")
.header("content-type", "application/json")
.body(Body::from(r#"{"file_path":"/tmp/test.txt"}"#))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
// AU-2: unauthenticated STT request rejected
#[tokio::test]
async fn au2_stt_unauthenticated() {
let (app, _services) = build_app_with_noop_opener().await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio")
.add_text("fileName", "test.wav")
.add_text("mimeType", "audio/wav")
.build();
let req = Request::builder()
.method("POST")
.uri("/api/stt")
.header("content-type", content_type)
.body(Body::from(body))
.unwrap();
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::FORBIDDEN);
}
// M-145: mailto scheme URL positive test
#[tokio::test]
async fn sh_open_external_mailto_scheme() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
let req = json_with_token(
"POST",
"/api/shell/open-external",
json!({ "url": "mailto:user@example.com" }),
&token,
&csrf,
);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
// M-147: multipart missing mimeType field
#[tokio::test]
async fn st_multipart_missing_mimetype() {
let (mut app, services) = build_app_with_noop_opener().await;
let (token, csrf) = setup_and_login(&mut app, &services, "admin", "StrongP@ss1").await;
set_stt_config(
&mut app,
&token,
&csrf,
json!({ "enabled": true, "provider": "openai", "openai": { "api_key": "sk-test", "model": "whisper-1" } }),
)
.await;
let (content_type, body) = MultipartBuilder::new()
.add_file("file", "test.wav", "audio/wav", b"fake audio data")
.add_text("fileName", "test.wav")
.build();
let req = multipart_request("/api/stt", &content_type, body, &token, &csrf);
let resp = app.oneshot(req).await.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let json = body_json(resp).await;
assert_eq!(json["success"], false);
}