merge: code-review fixes (d7720662 baseline + 9f8a 文案常量化、env_prefix 测试、补充用例); secrets-mcp 0.5.21
All checks were successful
Secrets MCP — Build & Release / 检查 / 构建 / 发版 (push) Successful in 5m59s
Secrets MCP — Build & Release / 部署 secrets-mcp (push) Successful in 1m35s

- default 已 rebase 到 d7720662;合并说明见 plans/merge-code-review-fixes-2026-04-11.md
- Web PATCH 长度错误用 validation 常量拼接;env_map 单测;import/api_key 单测
- rustfmt 收尾
This commit is contained in:
voson
2026-04-11 17:04:21 +08:00
parent d772066210
commit c3b1a0df1a
7 changed files with 289 additions and 15 deletions

View File

@@ -66,3 +66,30 @@ pub async fn validate_api_key(pool: &PgPool, raw_key: &str) -> Result<Option<Uui
.await?;
Ok(row.map(|(id,)| id))
}
#[cfg(test)]
mod tests {
use sqlx::PgPool;
use super::regenerate_api_key;
use crate::error::AppError;
#[tokio::test]
async fn regenerate_api_key_unknown_user_returns_not_found() {
let Ok(url) = std::env::var("SECRETS_DATABASE_URL") else {
return;
};
let Ok(pool) = PgPool::connect(&url).await else {
return;
};
let id = uuid::Uuid::new_v4();
let err = regenerate_api_key(&pool, id)
.await
.err()
.expect("expected error");
assert!(matches!(
err.downcast_ref::<AppError>(),
Some(AppError::NotFoundUser)
));
}
}

View File

@@ -87,11 +87,36 @@ fn json_to_env_string(v: &Value) -> String {
#[cfg(test)]
mod tests {
use super::secret_name_to_env_segment;
use serde_json::Value;
use super::{env_prefix, secret_name_to_env_segment};
use crate::models::Entry;
#[test]
fn secret_name_env_segment_disambiguates_dot_from_underscore() {
assert_eq!(secret_name_to_env_segment("db.password"), "DB__PASSWORD");
assert_eq!(secret_name_to_env_segment("db_password"), "DB_PASSWORD");
assert_eq!(secret_name_to_env_segment("api-key"), "API_KEY");
}
#[test]
fn env_prefix_with_and_without_prefix() {
let entry = Entry {
id: uuid::Uuid::new_v4(),
user_id: None,
folder: "test".into(),
entry_type: "server".into(),
name: "my-server".into(),
notes: String::new(),
tags: vec![],
metadata: Value::Null,
version: 1,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
deleted_at: None,
};
assert_eq!(env_prefix(&entry, ""), "MY_SERVER");
assert_eq!(env_prefix(&entry, "ALIYUN"), "ALIYUN_MY_SERVER");
assert_eq!(env_prefix(&entry, "aliyun_"), "ALIYUN_MY_SERVER");
}
}

View File

@@ -131,3 +131,50 @@ pub async fn run(
dry_run: params.dry_run,
})
}
#[cfg(test)]
mod tests {
use std::collections::{BTreeMap, HashMap};
use crate::models::ExportEntry;
/// Mirrors the map built in `run` before `AddParams` (legacy files omit `secret_types`).
fn secret_types_for_add(entry: &ExportEntry) -> HashMap<String, String> {
entry
.secret_types
.as_ref()
.map(|m| m.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
.unwrap_or_default()
}
#[test]
fn secret_types_three_kinds_round_trip_for_add_params() {
let mut types = BTreeMap::new();
types.insert("p".into(), "password".into());
types.insert("k".into(), "key".into());
types.insert("t".into(), "text".into());
let entry = ExportEntry {
name: "n".into(),
folder: "f".into(),
entry_type: "ty".into(),
notes: "".into(),
tags: vec![],
metadata: serde_json::json!({}),
secrets: Some(BTreeMap::new()),
secret_types: Some(types),
};
let m = secret_types_for_add(&entry);
assert_eq!(m.get("p").map(String::as_str), Some("password"));
assert_eq!(m.get("k").map(String::as_str), Some("key"));
assert_eq!(m.get("t").map(String::as_str), Some("text"));
}
#[test]
fn secret_types_absent_defaults_to_empty_map_like_legacy_export() {
let json =
r#"{"name":"a","folder":"","type":"","notes":"","tags":[],"metadata":{},"secrets":{}}"#;
let entry: ExportEntry = serde_json::from_str(json).unwrap();
assert!(entry.secret_types.is_none());
assert!(secret_types_for_add(&entry).is_empty());
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "secrets-mcp"
version = "0.5.20"
version = "0.5.21"
edition.workspace = true
[[bin]]

View File

@@ -636,33 +636,65 @@ pub(super) async fn api_entry_patch(
if folder.chars().count() > crate::validation::MAX_FOLDER_LENGTH {
return Err((
StatusCode::BAD_REQUEST,
Json(
json!({ "error": tr(lang, "folder 长度不能超过 128 个字符", "folder 長度不能超過 128 個字元", "folder must be at most 128 characters") }),
),
Json(json!({ "error": format!(
"{} {} {}",
tr(
lang,
"folder 长度不能超过",
"folder 長度不能超過",
"folder must be at most"
),
crate::validation::MAX_FOLDER_LENGTH,
tr(lang, " 个字符", " 個字元", " characters")
) })),
));
}
if entry_type.chars().count() > crate::validation::MAX_ENTRY_TYPE_LENGTH {
return Err((
StatusCode::BAD_REQUEST,
Json(
json!({ "error": tr(lang, "type 长度不能超过 64 个字符", "type 長度不能超過 64 個字元", "type must be at most 64 characters") }),
),
Json(json!({ "error": format!(
"{} {} {}",
tr(
lang,
"type 长度不能超过",
"type 長度不能超過",
"type must be at most"
),
crate::validation::MAX_ENTRY_TYPE_LENGTH,
tr(lang, " 个字符", " 個字元", " characters")
) })),
));
}
if name.chars().count() > crate::validation::MAX_NAME_LENGTH {
return Err((
StatusCode::BAD_REQUEST,
Json(
json!({ "error": tr(lang, "name 长度不能超过 256 个字符", "name 長度不能超過 256 個字元", "name must be at most 256 characters") }),
),
Json(json!({ "error": format!(
"{} {} {}",
tr(
lang,
"name 长度不能超过",
"name 長度不能超過",
"name must be at most"
),
crate::validation::MAX_NAME_LENGTH,
tr(lang, " 个字符", " 個字元", " characters")
) })),
));
}
if notes.chars().count() > crate::validation::MAX_NOTES_LENGTH {
return Err((
StatusCode::BAD_REQUEST,
Json(
json!({ "error": tr(lang, "notes 长度不能超过 10000 个字符", "notes 長度不能超過 10000 個字元", "notes must be at most 10000 characters") }),
),
Json(json!({ "error": format!(
"{} {} {}",
tr(
lang,
"notes 长度不能超过",
"notes 長度不能超過",
"notes must be at most"
),
crate::validation::MAX_NOTES_LENGTH,
tr(lang, " 个字符", " 個字元", " characters")
) })),
));
}