test: 补齐安全关键路径的单元测试盲区
针对前期测试评估发现的高性价比纯函数与安全不变量盲区补测, 共新增 35 个回归测试(全部通过),分布如下: - infra/docker.rs (+5): 扩展 build_host_config 的隔离断言——cap_drop=ALL、 security_opt=no-new-privileges、pids_limit=64、memory_swap==memory(禁 swap)、 tmpfs 的 exec 仅落在 /tmp、ulimits 只保留 nofile(无 nproc)。这些配置若被 误删会让容器隔离悄悄失效,此前 test_host_config_generation 仅断言 cpu/memory。 - api/code_runner/execute.rs (+8): validate_exec_request 此前零测试。覆盖 语言白名单(大小写/空白容忍、多 token 拒绝、未注册拒绝)、源码大小边界 (恰好 max 放行 / max+1 拒绝)、校验顺序(语言优先于大小)。 - api/database/export.rs (+12): is_simple_ident / parse_source 此前零测试。 覆盖表名白名单(SQL 注入 / 路径穿越 / Unicode 拒绝)、query: 来源的只读 AST 校验(INSERT/UPDATE/DELETE/DROP/TRUNCATE/CREATE/ALTER/GRANT 全拦截)。 - api/sanitizer.rs (+10): 补 XSS 攻击向量回归——事件处理器属性(onerror/onload/ onclick + 大小写混淆)、script/style 连内容移除、iframe/object/svg/form 等 危险标签整体移除、javascript:/vbscript: scheme(含 HTML 实体编码绕过)、 data URI 在文章正文路径(allow_data_uri=false)的拒绝、svg data URI 风险点锁定。 顺带记录一个既有可维护性问题:docker.rs 的 4 个 test_run_in_container_* 依赖真实 Docker daemon,无 daemon 环境会失败而非跳过,建议后续加 #[ignore] 或 daemon 探测守卫。本次未改动它们。
This commit is contained in:
parent
fdf2243299
commit
fa8b1d9968
@ -346,3 +346,91 @@ pub async fn get_exec_result(task_id: String) -> Result<ExecTask, ServerFnError>
|
||||
Err(ServerFnError::new("找不到指定的任务".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "server"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn req(language: &str, source: &str) -> ExecRequest {
|
||||
ExecRequest {
|
||||
language: language.to_string(),
|
||||
source: source.to_string(),
|
||||
overrides: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_accepts_registered_language() {
|
||||
// python/node/go/rust 默认注册,应放行。
|
||||
for lang in ["python", "node", "go", "rust"] {
|
||||
assert!(
|
||||
validate_exec_request(&req(lang, "x")).is_ok(),
|
||||
"{lang} 应被支持"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_language_is_case_and_whitespace_insensitive() {
|
||||
// is_supported_lang 内部 trim+lowercase,校验链应透传该容忍。
|
||||
assert!(validate_exec_request(&req("Python", "x")).is_ok());
|
||||
assert!(validate_exec_request(&req(" RUST ", "x")).is_ok());
|
||||
assert!(validate_exec_request(&req("Go", "x")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_unregistered_language() {
|
||||
// 未注册语言 / 命令注入尝试都应在白名单阶段被拒。
|
||||
assert!(validate_exec_request(&req("c", "x")).is_err());
|
||||
assert!(validate_exec_request(&req("bash", "x")).is_err());
|
||||
assert!(validate_exec_request(&req("python2", "x")).is_err());
|
||||
assert!(validate_exec_request(&req("", "x")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_multi_token_language() {
|
||||
// 语言字段不会到达容器层:只有命中 LANGUAGES 的 key 才放行,
|
||||
// 否则被拒。任何多 token / 含 shell 元字符的值都因找不到注册项而拒绝。
|
||||
assert!(validate_exec_request(&req("python; rm -rf /", "x")).is_err());
|
||||
assert!(validate_exec_request(&req("python node", "x")).is_err());
|
||||
assert!(validate_exec_request(&req("python$(whoami)", "x")).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_language_tolerates_surrounding_whitespace() {
|
||||
// is_supported_lang 内部 trim+lowercase:首尾空白(含换行)被吃掉后等于 "python"。
|
||||
// 锁定该契约——语言字段只用于 HashMap 查 key,空白无害。
|
||||
assert!(validate_exec_request(&req("python\n", "x")).is_ok());
|
||||
assert!(validate_exec_request(&req("\tpython\t", "x")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_rejects_source_exceeding_max_bytes() {
|
||||
let max = RUNNER_CONFIG.max_source_bytes as usize;
|
||||
// 恰好 max 放行,max+1 拒绝——边界精确,防止 off-by-one 让攻击者多塞 1 字节。
|
||||
let exactly = "a".repeat(max);
|
||||
assert!(
|
||||
validate_exec_request(&req("python", &exactly)).is_ok(),
|
||||
"源码恰好等于上限应放行"
|
||||
);
|
||||
let over = "a".repeat(max + 1);
|
||||
let err =
|
||||
validate_exec_request(&req("python", &over)).expect_err("超出上限应拒绝");
|
||||
assert!(err.to_string().contains("过大"), "错误信息应提及大小: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_empty_source_accepted_for_supported_lang() {
|
||||
// 空源码不是校验层的职责(容器层处理),这里只验语言/大小。
|
||||
// 锁定该契约:空源码 + 合法语言 → Ok,避免未来误把空当拒绝。
|
||||
assert!(validate_exec_request(&req("python", "")).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_checks_language_before_size() {
|
||||
// 不支持语言 + 巨大源码:应先因语言被拒(而非先报大小)。
|
||||
let huge = "a".repeat((RUNNER_CONFIG.max_source_bytes as usize) + 100);
|
||||
let err = validate_exec_request(&req("brainfuck", &huge)).unwrap_err();
|
||||
assert!(err.to_string().contains("语言"), "应先报语言错误: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -231,3 +231,148 @@ fn sql_quote_cell(row: &tokio_postgres::Row, idx: usize) -> String {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, feature = "server"))]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---- is_simple_ident:表名白名单(防 SQL 注入与路径穿越)----
|
||||
|
||||
#[test]
|
||||
fn simple_ident_accepts_legal_names() {
|
||||
assert!(is_simple_ident("posts"));
|
||||
assert!(is_simple_ident("user_posts"));
|
||||
assert!(is_simple_ident("table1"));
|
||||
assert!(is_simple_ident("a"));
|
||||
assert!(is_simple_ident("_private"));
|
||||
assert!(is_simple_ident("ABC123xyz"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_ident_rejects_empty_and_whitespace() {
|
||||
assert!(!is_simple_ident(""));
|
||||
assert!(!is_simple_ident(" "));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_ident_rejects_injection_vectors() {
|
||||
// SQL 注入:引号、分号、注释、空格分隔
|
||||
assert!(!is_simple_ident("posts; DROP TABLE users"));
|
||||
assert!(!is_simple_ident("posts' OR '1'='1"));
|
||||
assert!(!is_simple_ident("posts--"));
|
||||
assert!(!is_simple_ident("public.posts"));
|
||||
assert!(!is_simple_ident("posts where 1=1"));
|
||||
// 路径穿越
|
||||
assert!(!is_simple_ident("../etc/passwd"));
|
||||
assert!(!is_simple_ident("..\\windows"));
|
||||
assert!(!is_simple_ident("/etc/passwd"));
|
||||
// 反引号 / 双引号标识符语法
|
||||
assert!(!is_simple_ident("`posts`"));
|
||||
assert!(!is_simple_ident("\"posts\""));
|
||||
// 连字符(常见合法表名片段,但本白名单禁止,防 `a-b` 被 SQL 解析为减法)
|
||||
assert!(!is_simple_ident("my-posts"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple_ident_rejects_unicode_and_non_ascii() {
|
||||
assert!(!is_simple_ident("文章"));
|
||||
assert!(!is_simple_ident("posts²"));
|
||||
assert!(!is_simple_ident("café"));
|
||||
}
|
||||
|
||||
// ---- parse_source:来源解析 + 只读校验(安全入口)----
|
||||
|
||||
#[test]
|
||||
fn parse_table_source_builds_select_star() {
|
||||
let (sql, name) = parse_source("table:posts").unwrap();
|
||||
assert_eq!(sql, "SELECT * FROM \"posts\"");
|
||||
assert_eq!(name, "posts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_table_source_trims_whitespace() {
|
||||
let (sql, name) = parse_source("table: posts ").unwrap();
|
||||
assert_eq!(sql, "SELECT * FROM \"posts\"");
|
||||
assert_eq!(name, "posts");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_table_source_rejects_bad_name() {
|
||||
// 非法表名(注入/穿越/空)必须在拼进 SQL 前被拒,防 SQL 注入。
|
||||
for bad in [
|
||||
"table:",
|
||||
"table: ",
|
||||
"table:posts; DROP TABLE users",
|
||||
"table:../secret",
|
||||
"table:a b",
|
||||
"table:\"x\"",
|
||||
] {
|
||||
let (code, msg) = parse_source(bad).expect_err(bad);
|
||||
assert_eq!(code, StatusCode::BAD_REQUEST);
|
||||
assert!(msg.contains("表名"), "{bad}: {msg}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_query_source_accepts_select_and_explain() {
|
||||
assert!(parse_source("query:SELECT * FROM posts").is_ok());
|
||||
assert!(parse_source("query:SELECT id, title FROM posts WHERE id > 0").is_ok());
|
||||
assert!(parse_source("query:EXPLAIN SELECT * FROM posts").is_ok());
|
||||
assert!(parse_source("query:SELECT 1").is_ok());
|
||||
// 复杂但只读的查询
|
||||
assert!(parse_source("query:WITH t AS (SELECT 1) SELECT * FROM t").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_query_source_rejects_all_write_operations() {
|
||||
// 每一种写/破坏操作都必须被 AST 校验拦截——这是数据导出的核心安全不变量。
|
||||
for evil in [
|
||||
"query:INSERT INTO posts VALUES (1)",
|
||||
"query:UPDATE posts SET title='x'",
|
||||
"query:DELETE FROM posts",
|
||||
"query:DROP TABLE posts",
|
||||
"query:TRUNCATE posts",
|
||||
"query:CREATE TABLE evil (id int)",
|
||||
"query:ALTER TABLE posts ADD COLUMN x int",
|
||||
"query:GRANT SELECT ON posts TO public",
|
||||
// 即便嵌在 SELECT 里,子查询的写操作也要拒
|
||||
"query:INSERT INTO logs VALUES (1) RETURNING 1",
|
||||
] {
|
||||
let (code, msg) = parse_source(evil).expect_err(evil);
|
||||
assert_eq!(code, StatusCode::BAD_REQUEST, "{evil}");
|
||||
// 写操作要么被 AST 校验拒("只读"),要么 sqlparser 解析阶段就失败
|
||||
assert!(
|
||||
msg.contains("只读") || msg.contains("解析失败"),
|
||||
"{evil}: {msg}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_query_source_rejects_unparseable_sql() {
|
||||
for bad in ["query:这不是SQL", "query:SELECT FROM", "query:@#$%", "query:1+1"] {
|
||||
assert!(
|
||||
parse_source(bad).is_err(),
|
||||
"{bad} 应解析失败或被拒"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_query_source_allows_empty_result_set() {
|
||||
// sqlparser 容忍纯分号(解析为零语句),`any()` 在空集上为 false → 放行。
|
||||
// 锁定该契约:这只产生空导出,无数据泄露,属可接受行为;
|
||||
// 若未来想收紧为"空查询拒绝",此测试会提醒你更新。
|
||||
assert!(parse_source("query:;;;").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_source_rejects_unknown_prefix() {
|
||||
let (code, msg) = parse_source("unknown:posts").expect_err("未知前缀");
|
||||
assert_eq!(code, StatusCode::BAD_REQUEST);
|
||||
assert!(msg.contains("source"));
|
||||
// 完全无前缀的裸字符串也应被拒
|
||||
assert!(parse_source("posts").is_err());
|
||||
assert!(parse_source("").is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@ -713,4 +713,149 @@ mod tests {
|
||||
"未知 data-* 应被剥离, got: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
// ---- XSS 攻击向量回归:属性白名单是这里的核心防线 ----
|
||||
// 这些测试锁定"非白名单属性被剥离"这一不变量——若有人放宽属性过滤,
|
||||
// 经典 XSS 向量就会重新可用,测试会在那一步失败。
|
||||
|
||||
#[test]
|
||||
fn clean_html_strips_event_handler_attributes() {
|
||||
// onerror/onload/onclick 等事件处理器属性全不在白名单,必须被移除。
|
||||
// 即便 <img> 本身合法,onerror 也不能留下。
|
||||
let cases = [
|
||||
r#"<img src="x" onerror="alert(1)">"#,
|
||||
r#"<img src=x onerror=alert(1)>"#,
|
||||
r#"<body onload="alert(1)">"#,
|
||||
r#"<div onclick="alert(1)">x</div>"#,
|
||||
r#"<a href="/x" onmouseover="alert(1)">x</a>"#,
|
||||
r#"<svg onload="alert(1)"></svg>"#,
|
||||
];
|
||||
for input in cases {
|
||||
let result = clean_html(input);
|
||||
assert!(
|
||||
!result.contains("onerror")
|
||||
&& !result.contains("onload")
|
||||
&& !result.contains("onclick")
|
||||
&& !result.contains("onmouseover"),
|
||||
"事件处理器属性应被剥离, input: {input}, got: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_strips_event_handler_attribute_with_mixed_case() {
|
||||
// 大小写混淆绕过尝试:EvEr、大写、混合都应被拦(属性名匹配应大小写不敏感地拒绝)。
|
||||
for attr in ["OnErRoR", "ONERROR", "On_Error".replace('_', "or").as_str()] {
|
||||
let input = format!(r#"<img src="x" {attr}="alert(1)">"#);
|
||||
let result = clean_html(&input);
|
||||
assert!(
|
||||
!result.to_lowercase().contains("onerror"),
|
||||
"大小写混淆的事件处理器应被剥离: {input} -> {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_removes_script_tag_and_content() {
|
||||
// script 在 CLEAN_CONTENT_TAGS:标签连同内容一起移除(而非转义后保留)。
|
||||
let result = clean_html("<p>hi</p><script>alert(1)</script><p>bye</p>");
|
||||
assert!(!result.contains("script"), "script 标签应被完全移除: {result}");
|
||||
assert!(!result.contains("alert"), "script 内容应被清除: {result}");
|
||||
assert!(result.contains("hi") && result.contains("bye"), "周围内容应保留: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_removes_style_tag_and_content() {
|
||||
// style 也走 CLEAN_CONTENT_TAGS:CSS 注入(expression()、@import)随内容一起移除。
|
||||
let result = clean_html("<style>body{background:url(javascript:alert(1))}</style><p>x</p>");
|
||||
assert!(!result.contains("style"), "style 标签应被移除: {result}");
|
||||
assert!(!result.contains("javascript"), "style 内危险内容应被清除: {result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_drops_dangerous_tags_entirely() {
|
||||
// 这些标签不在白名单:整体移除(含子树),无法用于 XSS / 数据外泄。
|
||||
for tag in ["iframe", "object", "embed", "form", "svg", "math", "base", "meta"] {
|
||||
let input = format!("<{tag} src=\"javascript:alert(1)\"></{tag}>");
|
||||
let result = clean_html(&input);
|
||||
assert!(
|
||||
!result.to_lowercase().contains(&format!("<{tag}")),
|
||||
"<{tag}> 不在白名单应被移除: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_drops_javascript_scheme_in_href_and_src() {
|
||||
// 即便标签合法,javascript:/vbscript: scheme 必须被拒(is_safe_url 防线)。
|
||||
let cases = [
|
||||
r#"<a href="javascript:alert(1)">x</a>"#,
|
||||
r#"<a href="vbscript:msgbox(1)">x</a>"#,
|
||||
r#"<img src="javascript:alert(1)">"#,
|
||||
// 编码绕过尝试:HTML 实体编码的 javascript:
|
||||
r#"<a href="javascript:alert(1)">x</a>"#,
|
||||
r#"<a href="javascript:alert(1)">x</a>"#,
|
||||
];
|
||||
for input in cases {
|
||||
let result = clean_html(input);
|
||||
// href/src 值里不应残留可执行的 javascript scheme(属性可能被整段移除或值被清空)。
|
||||
// 至少裸的 "javascript:" 字面量不应在 href/src 属性值中出现。
|
||||
let lower = result.to_lowercase();
|
||||
assert!(
|
||||
!lower.contains("javascript:") && !lower.contains("vbscript:"),
|
||||
"危险 scheme 应被移除, input: {input}, got: {result}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_data_uri_blocked_in_article_body() {
|
||||
// clean_html 配置 allow_data_uri: false——所有 data URI(含 image/png)都应被拒。
|
||||
// 这是文章正文路径的契约;评论路径同样。
|
||||
let result = clean_html(r#"<img src="data:image/png;base64,iVBORw0KGgo=처리">"#);
|
||||
assert!(
|
||||
!result.contains("data:image/png"),
|
||||
"文章正文应禁用 data URI: {result}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_data_uri_text_html_blocked_even_if_flag_true() {
|
||||
// 即便 allow_data_uri=true,is_safe_data_uri 也只放行图片类型;
|
||||
// data:text/html(可执行脚本)永远拒绝。直接测内部函数锁定该不变量。
|
||||
let schemes = DEFAULT_ALLOWED_SCHEMES.clone();
|
||||
assert!(!is_safe_url("data:text/html,<script>alert(1)</script>", &schemes, true));
|
||||
assert!(!is_safe_url(
|
||||
"data:application/javascript,alert(1)",
|
||||
&schemes,
|
||||
true
|
||||
));
|
||||
// 安全图片类型在 flag=true 时放行
|
||||
assert!(is_safe_url("data:image/png;base64,iVBOR=", &schemes, true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_html_svg_data_uri_carries_risk_even_when_allowed() {
|
||||
// is_safe_data_uri 允许 image/svg+xml——SVG 内可嵌 <script>。
|
||||
// 这里锁定当前行为:flag=true 时 svg data URI 被放行(调用方需自行评估风险),
|
||||
// 并用注释标记这是一个潜在风险点(文章正文 allow_data_uri=false 已堵住)。
|
||||
let schemes = DEFAULT_ALLOWED_SCHEMES.clone();
|
||||
assert!(
|
||||
is_safe_url("data:image/svg+xml,<svg></svg>", &schemes, true),
|
||||
"svg data URI 在 flag=true 时当前被放行(已知风险点)"
|
||||
);
|
||||
// 但文章正文路径 flag=false,svg data URI 同样被拒
|
||||
assert!(!is_safe_url(
|
||||
"data:image/svg+xml,<svg><script>alert(1)</script></svg>",
|
||||
&schemes,
|
||||
false
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clean_comment_html_strips_event_handlers() {
|
||||
// 评论路径(更严格)同样不能漏掉事件处理器。
|
||||
let result = clean_comment_html(r#"<p onclick="alert(1)">x</p>"#);
|
||||
assert!(!result.contains("onclick"), "评论 XSS 向量: {result}");
|
||||
}
|
||||
}
|
||||
|
||||
@ -611,6 +611,89 @@ mod tests {
|
||||
assert_eq!(host_config.memory, Some(256 * 1024 * 1024));
|
||||
assert_eq!(host_config.readonly_rootfs, Some(true));
|
||||
assert_eq!(host_config.network_mode.as_deref(), Some("none"));
|
||||
|
||||
// 隔离不变量:这些配置若被误删,容器隔离会悄悄失效。
|
||||
// 单独断言而非隐含在端到端测试里,确保回归可见。
|
||||
assert_eq!(host_config.cap_drop.as_deref(), Some(&["ALL".to_string()][..]));
|
||||
assert_eq!(
|
||||
host_config.security_opt.as_deref(),
|
||||
Some(&["no-new-privileges".to_string()][..])
|
||||
);
|
||||
assert_eq!(host_config.pids_limit, Some(64));
|
||||
// memory_swap == memory 才是真正禁用 swap;二者不等价于可换出到磁盘
|
||||
assert_eq!(host_config.memory_swap, host_config.memory);
|
||||
assert_eq!(host_config.memory_swap, Some(256 * 1024 * 1024));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_ulimits_drops_only_nofile() {
|
||||
// nproc 在容器内对 non-root 用户有害(初始 exec /bin/sh 直接 EAGAIN),
|
||||
// 只保留 nofile=64。若有人加回 nproc,这里会失败。
|
||||
let limits = ResourceLimits {
|
||||
cpu_cores: 1.0,
|
||||
memory_mb: 128,
|
||||
timeout_secs: 5,
|
||||
output_bytes: 1024,
|
||||
allow_network: false,
|
||||
};
|
||||
let host_config = build_host_config(&limits);
|
||||
let ulimits = host_config.ulimits.expect("ulimits must be set");
|
||||
assert_eq!(ulimits.len(), 1, "only nofile, no nproc");
|
||||
let nf = &ulimits[0];
|
||||
assert_eq!(nf.name.as_deref(), Some("nofile"));
|
||||
assert_eq!(nf.soft, Some(64));
|
||||
assert_eq!(nf.hard, Some(64));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_tmpfs_has_exec_on_tmp_only() {
|
||||
// /tmp 必须 exec(编译型语言把产物落在 /tmp 后再 exec);
|
||||
// /code、/run 不带 exec(默认 noexec)。若误给 /code 也加 exec,测试失败。
|
||||
let limits = ResourceLimits {
|
||||
cpu_cores: 1.0,
|
||||
memory_mb: 128,
|
||||
timeout_secs: 5,
|
||||
output_bytes: 1024,
|
||||
allow_network: false,
|
||||
};
|
||||
let host_config = build_host_config(&limits);
|
||||
let tmpfs = host_config.tmpfs.expect("tmpfs must be set");
|
||||
assert_eq!(tmpfs.len(), 3);
|
||||
assert!(tmpfs["/tmp"].contains("exec"));
|
||||
assert!(!tmpfs["/code"].contains("exec"));
|
||||
assert!(!tmpfs["/run"].contains("exec"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_network_mode_follows_allow_network() {
|
||||
let base = ResourceLimits {
|
||||
cpu_cores: 1.0,
|
||||
memory_mb: 128,
|
||||
timeout_secs: 5,
|
||||
output_bytes: 1024,
|
||||
allow_network: false,
|
||||
};
|
||||
assert_eq!(build_host_config(&base).network_mode.as_deref(), Some("none"));
|
||||
let mut net = base;
|
||||
net.allow_network = true;
|
||||
assert_eq!(build_host_config(&net).network_mode.as_deref(), Some("bridge"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_config_cpu_and_memory_scale_with_limits() {
|
||||
// 资源钳制是安全边界,断言换算公式不漂移(cpu_cores * 100_000,memory_mb * 1MiB)。
|
||||
let limits = ResourceLimits {
|
||||
cpu_cores: 2.0,
|
||||
memory_mb: 512,
|
||||
timeout_secs: 5,
|
||||
output_bytes: 1024,
|
||||
allow_network: false,
|
||||
};
|
||||
let host_config = build_host_config(&limits);
|
||||
assert_eq!(host_config.cpu_quota, Some(200_000));
|
||||
assert_eq!(host_config.cpu_period, Some(100_000));
|
||||
assert_eq!(host_config.memory, Some(512 * 1024 * 1024));
|
||||
assert_eq!(host_config.memory_swap, Some(512 * 1024 * 1024));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user