test(presenter): 坏清单用例按错误分类区分「JSON 坏」与「签名被拒」

台账 §10 第 7 条 / M199 复审 P3-2:原来只断言「结果是 Failed」,而它喂的每一份清单都签对了
——验签一旦 fail-open 或签名错误被误分类,这条用例照样绿。

现在两组各钉自己的分类:
- 内容坏(5 个变体,签名都对):kind 必须是 invalid_manifest / invalid_version;
- 字节好、只有签名对不上(放行就会报「有更新 0.9.0」):kind 必须是 signature_invalid 且不可重试。

变异证明(只改实现侧,跑完已还原):
- A core::update::signature::verify_manifest fail-open → ② 分支红,实际 Available(0.9.0);
- B core::update::manifest::from_json 把解析失败误分类成 SignatureInvalid → ① 分支红,
  「not json:坏内容分类是 signature_invalid」。
两个变异各只打红对应分支,说明两类坏输入确实各走各的分类。

文件仍在 400 行红线内(394 行)。
This commit is contained in:
AskaEth committed 2026-11-09 09:32:58 +08:00
1 parent 06fcbb8a51
commit 133f14ba0b
1 file changed
+31 -10
+31 -10
View File
@@ -6,7 +6,7 @@
//! | 用例 | 守什么 |
//! |---|---|
//! | [`version_matrix_decides_who_gets_prompted`] | 版本更高才提示;同版本/更低一律不提示;`v0.2.3` 这类真实 tag 形态 |
//! | [`a_broken_manifest_degrades_to_failure_not_up_to_date`] | 清单坏 JSON → 「检查失败」,**不是**「已是最新」 |
//! | [`a_broken_manifest_degrades_to_failure_not_up_to_date`] | 坏 JSON → `invalid_*`、签名被拒 → `signature_*`;两种都是「检查失败」,**不是**「已是最新」 |
//! | [`a_higher_version_without_our_platform_is_not_up_to_date`] | 有新版但没本平台产物 → 单独的变体 |
//! | [`sha256_mismatch_refuses_the_apply_and_leaves_no_temp_file`] | 摘要不符拒绝应用、暂存文件不残留、目标没被换掉 |
//! | [`a_successful_apply_leaves_the_new_file_and_a_backup`] | 换上去的是新字节、旧字节留在备份里、回报是「就位」 |
@@ -191,11 +191,16 @@ fn version_matrix_decides_who_gets_prompted() {
// ------------------------------------------------------------- 清单契约降级
/// 坏 JSON / 缺字段 ⇒ **检查失败**,绝不是「已是最新」。
/// 坏 JSON / 缺字段 / 签名被拒 ⇒ **检查失败**,绝不是「已是最新」;而且两种坏法**分类不同**。
///
/// 两个变体的文案与后果完全不同:说成「已是最新」会让用户以为网络通、版本新,实际一个字节都没取到。
/// M199 复审 P3-2:旧版只断言「结果是 Failed」,而它喂的清单都**签对了**——验签一旦 fail-open,
/// 坏签名清单会一路走到解析,坏 JSON 照样解析失败,断言全绿。现在内容坏必须落 `invalid_*`,
/// 签名坏必须落 `signature_*` 且不可重试。
/// 抓得住:验签被跳过/放行(② 会拿到 `Available 0.9.0`)、签名与解析互相误分类、失败被折叠成「已是最新」;
/// 抓不住:签名算法细节(信封字段、`manifest_sha256` 冗余校验、钥表)——那在 core 的 `signature_cases.rs`。
#[test]
fn a_broken_manifest_degrades_to_failure_not_up_to_date() {
// ① 签名是对的(`write_manifest` 成对落盘),内容坏:分类只能是「清单结构 / 版本号不合法」。
for (label, manifest) in [
("not json", "{ this is not json".to_string()),
(
@@ -221,16 +226,32 @@ fn a_broken_manifest_degrades_to_failure_not_up_to_date() {
let tmp = TempDir::new("badmanifest");
write_manifest(&tmp, &manifest);
let outcome = outcome_of(job(&tmp).check(&version("0.2.2"), Channel::Stable));
let Some(failure) = outcome.failure() else {
panic!("{label}:坏清单必须降级成「检查失败」,实际 {outcome:?}");
};
let kind = failure.kind.as_str();
assert!(
matches!(outcome, UpdateOutcome::Failed(_)),
"{label}:坏清单必须降级成「检查失败」,实际 {outcome:?}"
);
let failure = outcome.failure().expect("失败带原因");
assert!(
!failure.message.trim().is_empty(),
"{label}:失败要有人话原因"
matches!(kind, "invalid_manifest" | "invalid_version"),
"{label}:坏内容分类是 {kind}"
);
assert!(!failure.message.trim().is_empty(), "{label}:失败没原因");
}
// ② 清单字节是好的(放行就会报「有更新 0.9.0」),只有签名对不上:必须落 `signature_invalid`。
let tmp = TempDir::new("badsignature");
tmp.write(
"dist/stable.json",
manifest_for("0.9.0", b"new", "").as_bytes(),
);
tmp.write(
"dist/stable.json.sig",
signature_envelope("another").as_bytes(),
);
let outcome = outcome_of(job(&tmp).check(&version("0.2.2"), Channel::Stable));
let Some(failure) = outcome.failure() else {
panic!("签名验不过必须拒装(放行就会报「有更新 0.9.0」),实际 {outcome:?}");
};
assert_eq!(failure.kind, "signature_invalid", "签名被拒的分类");
assert!(!failure.retryable, "签名问题不可重试");
}
/// 清单里**一条发布都没有**(`{"channel":"stable"}` / 空 `releases`)⇒ 已是最新,不是失败。