max / makenotwork
1 file changed,
+51 insertions,
-0 deletions
| @@ -494,3 +494,54 @@ pub fn ota_routes() -> CsrfRouter<AppState> { | |||
| 494 | 494 | pub fn validate_slug_public(slug: &str) -> Result<()> { | |
| 495 | 495 | validate_slug(slug) | |
| 496 | 496 | } | |
| 497 | + | ||
| 498 | + | #[cfg(test)] | |
| 499 | + | mod tests { | |
| 500 | + | use super::*; | |
| 501 | + | ||
| 502 | + | /// The Tauri updater plugin reads exactly these five top-level fields | |
| 503 | + | /// out of the manifest JSON. Renaming any of them silently breaks every | |
| 504 | + | /// installed app (Tauri logs "failed to deserialize updater response" | |
| 505 | + | /// and stays on the old version). Pin the contract. | |
| 506 | + | #[test] | |
| 507 | + | fn tauri_updater_response_json_shape_is_stable() { | |
| 508 | + | let resp = TauriUpdaterResponse { | |
| 509 | + | version: "0.4.1".into(), | |
| 510 | + | url: "https://makenot.work/api/sync/ota/goingson/download/abc/darwin/aarch64".into(), | |
| 511 | + | signature: "untrusted comment: signature from minisign\nRWS...==".into(), | |
| 512 | + | notes: "Bug fixes".into(), | |
| 513 | + | pub_date: "2026-06-01T00:00:00+00:00".into(), | |
| 514 | + | }; | |
| 515 | + | let v: serde_json::Value = serde_json::to_value(&resp).unwrap(); | |
| 516 | + | // Top-level keys, in the order Tauri's deserializer expects them. | |
| 517 | + | let keys: Vec<&str> = v.as_object().unwrap().keys().map(|s| s.as_str()).collect(); | |
| 518 | + | assert_eq!( | |
| 519 | + | keys, | |
| 520 | + | vec!["version", "url", "signature", "notes", "pub_date"], | |
| 521 | + | "TauriUpdaterResponse field names/order changed — every installed Tauri app will stop updating", | |
| 522 | + | ); | |
| 523 | + | // Type spot-checks: all strings, no surprise nesting. | |
| 524 | + | assert!(v["version"].is_string()); | |
| 525 | + | assert!(v["url"].is_string()); | |
| 526 | + | assert!(v["signature"].is_string()); | |
| 527 | + | assert!(v["notes"].is_string()); | |
| 528 | + | assert!(v["pub_date"].is_string()); | |
| 529 | + | } | |
| 530 | + | ||
| 531 | + | #[test] | |
| 532 | + | fn tauri_updater_response_signature_is_inline_string() { | |
| 533 | + | // Architectural assertion: the signature rides INSIDE the manifest | |
| 534 | + | // JSON, not as a separate .sig sidecar file in S3. The launchplan | |
| 535 | + | // briefly described it as a sidecar; that was wrong. Locking the | |
| 536 | + | // architecture in so it doesn't drift back. | |
| 537 | + | let resp = TauriUpdaterResponse { | |
| 538 | + | version: "0.4.1".into(), | |
| 539 | + | url: "https://example".into(), | |
| 540 | + | signature: "RWS=".into(), | |
| 541 | + | notes: String::new(), | |
| 542 | + | pub_date: "2026-06-01T00:00:00Z".into(), | |
| 543 | + | }; | |
| 544 | + | let json = serde_json::to_string(&resp).unwrap(); | |
| 545 | + | assert!(json.contains(r#""signature":"RWS=""#)); | |
| 546 | + | } | |
| 547 | + | } |