Skip to main content

max / makenotwork

server: pin Tauri OTA manifest JSON contract Two unit tests in routes::ota::tests lock the contract the Tauri updater plugin reads: the five top-level fields version/url/signature/notes/pub_date must appear in that order; the signature must ride inline in the manifest JSON, not as a separate .sig sidecar in S3. A future rename of any field would break every installed Tauri app silently (Tauri logs a deserialize error and stays on the old version); the test catches that at build time.
Author: Max Johnson <me@maxj.phd> · 2026-06-04 02:56 UTC
Commit: f677625d7ce2ccb47833253d5dff04f8311b52e2
Parent: e53c566
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 + }