fix(workout): make mobile verification fail closed

Make workout mobile-verify reject non-HTTPS origins and release kits that omit detected external signing/toolchain blockers, so verification checks objective release preconditions instead of pretending to predict store approval.

req: examples/006
This commit is contained in:
slhx agent
2026-06-12 12:31:19 +02:00
parent 32fa1a4952
commit 2b9979e339
2 changed files with 64 additions and 4 deletions
+5 -2
View File
@@ -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
+59 -2
View File
@@ -261,7 +261,7 @@ fn verify_workout_mobile_release(
config: &WorkoutMobileConfig,
require_server_binary: bool,
) -> Vec<String> {
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<String> {
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