diff --git a/crates/presenter/src/host/update_tests.rs b/crates/presenter/src/host/update_tests.rs index 58057d5..077c144 100644 --- a/crates/presenter/src/host/update_tests.rs +++ b/crates/presenter/src/host/update_tests.rs @@ -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`)⇒ 已是最新,不是失败。