diff --git a/docs/recipes/mobile-release.md b/docs/recipes/mobile-release.md index de0824a..d646236 100644 --- a/docs/recipes/mobile-release.md +++ b/docs/recipes/mobile-release.md @@ -29,8 +29,11 @@ target/hemx-mobile/workout/ ios/README.md ``` -Use `workout mobile-verify` to check the generated kit and release binary. Use -`workout doctor` when you only want to see missing external inputs. +Use `workout mobile-verify` to check the generated kit and release binary. It +fails on a non-HTTPS production origin, missing/inconsistent Android or iOS +metadata, or external toolchain/signing blockers that were not written into the +manifest and `BLOCKERS.md`. Use `workout doctor` when you only want to see +missing external inputs. ## Production configuration diff --git a/hemx-xtask/src/main.rs b/hemx-xtask/src/main.rs index cfe28dc..ba33a17 100644 --- a/hemx-xtask/src/main.rs +++ b/hemx-xtask/src/main.rs @@ -261,7 +261,7 @@ fn verify_workout_mobile_release( config: &WorkoutMobileConfig, require_server_binary: bool, ) -> Vec { - let mut failures = Vec::new(); + let mut failures = mobile_policy_failures(config); if require_server_binary && !Path::new("target/release/hemx-workout-example").exists() { failures.push( "target/release/hemx-workout-example is missing; run workout-mobile release first" @@ -329,6 +329,23 @@ fn verify_workout_mobile_release( ], &mut failures, ); + + for blocker in mobile_external_blockers(config) { + check_file_contains(&manifest_path, &[&blocker], &mut failures); + check_file_contains(&blockers_path, &[&blocker], &mut failures); + } + + failures +} + +fn mobile_policy_failures(config: &WorkoutMobileConfig) -> Vec { + let mut failures = Vec::new(); + if !config.origin.starts_with("https://") { + failures.push( + "HEMX_WORKOUT_ORIGIN must be a production HTTPS origin before mobile verification can pass" + .into(), + ); + } failures } @@ -797,6 +814,25 @@ mod tests { .any(|blocker| blocker.contains("production HTTPS origin"))); } + #[test] + fn workout_mobile_verify_fails_closed_on_non_https_origin() { + // req: examples/006 + let config = workout_mobile_config_at( + "http://workout.example.com", + "target/test-workout-mobile-non-https", + ); + let _ = fs::remove_dir_all(&config.out_dir); + write_workout_mobile_release(&config, &mobile_external_blockers(&config)) + .expect("write release kit"); + + let failures = verify_workout_mobile_release(&config, false); + + assert!(failures + .iter() + .any(|failure| failure.contains("production HTTPS origin"))); + let _ = fs::remove_dir_all(&config.out_dir); + } + #[test] fn workout_mobile_verify_accepts_generated_release_kit() { // req: examples/006 @@ -805,7 +841,7 @@ mod tests { "target/test-workout-mobile-accepts", ); let _ = fs::remove_dir_all(&config.out_dir); - write_workout_mobile_release(&config, &["Android signing key not configured".into()]) + write_workout_mobile_release(&config, &mobile_external_blockers(&config)) .expect("write release kit"); assert!(verify_workout_mobile_release(&config, false).is_empty()); @@ -813,6 +849,27 @@ mod tests { let _ = fs::remove_dir_all(&config.out_dir); } + #[test] + fn workout_mobile_verify_rejects_unacknowledged_external_blockers() { + // req: examples/006 + let config = workout_mobile_config_at( + "https://workout.example.com", + "target/test-workout-mobile-unacknowledged-blockers", + ); + let _ = fs::remove_dir_all(&config.out_dir); + write_workout_mobile_release(&config, &[]).expect("write release kit"); + + let failures = verify_workout_mobile_release(&config, false); + + assert!(failures.iter().any(|failure| { + failure.contains("release-manifest.json") && failure.contains("Android") + })); + assert!(failures + .iter() + .any(|failure| failure.contains("BLOCKERS.md") && failure.contains("Android"))); + let _ = fs::remove_dir_all(&config.out_dir); + } + #[test] fn workout_mobile_verify_rejects_missing_release_kit() { // req: examples/006