| 1 | //! Attaching a build to an App Store Version, and confirming it took. |
| 2 | //! |
| 3 | //! The PATCH response is the version resource, whose `build` relationship |
| 4 | //! holds only links, so it cannot say which build is attached. Reading the |
| 5 | //! build back is what catches a wrong attachment before App Review does. |
| 6 | |
| 7 | use serde::Serialize; |
| 8 | use smbcloud_ascapi_aso::build::Build; |
| 9 | use smbcloud_ascapi_aso::prelude::*; |
| 10 | use smbcloud_ascapi_core::Client; |
| 11 | |
| 12 | /// The outcome of [`attach_build`]. |
| 13 | #[derive(Debug, Clone, Serialize)] |
| 14 | #[serde(rename_all = "camelCase")] |
| 15 | pub struct AttachedBuild { |
| 16 | pub version_id: String, |
| 17 | /// The build read back from App Store Connect: its id, `version`, |
| 18 | /// `processingState` and `usesNonExemptEncryption`. `None` only when |
| 19 | /// verification was skipped. |
| 20 | pub build: Option<Build>, |
| 21 | } |
| 22 | |
| 23 | /// Point `version_id` at `build_id`, then (when `verify`) read the attached |
| 24 | /// build back and fail unless it is the one requested. |
| 25 | pub async fn attach_build( |
| 26 | client: &Client, |
| 27 | version_id: &str, |
| 28 | build_id: &str, |
| 29 | verify: bool, |
| 30 | ) -> Result<AttachedBuild, String> { |
| 31 | client |
| 32 | .set_app_store_version_build(version_id, build_id) |
| 33 | .await |
| 34 | .map_err(|error| error.to_string())?; |
| 35 | if !verify { |
| 36 | return Ok(AttachedBuild { |
| 37 | version_id: version_id.to_string(), |
| 38 | build: None, |
| 39 | }); |
| 40 | } |
| 41 | let attached = client |
| 42 | .get_app_store_version_build(version_id) |
| 43 | .await |
| 44 | .map_err(|error| error.to_string())?; |
| 45 | check_attached(build_id, attached.as_ref())?; |
| 46 | Ok(AttachedBuild { |
| 47 | version_id: version_id.to_string(), |
| 48 | build: attached, |
| 49 | }) |
| 50 | } |
| 51 | |
| 52 | /// The read-back check, split out so it can be tested without a network. |
| 53 | fn check_attached(expected: &str, attached: Option<&Build>) -> Result<(), String> { |
| 54 | match attached { |
| 55 | None => Err(format!( |
| 56 | "attached build {expected}, but the version reports no build" |
| 57 | )), |
| 58 | Some(build) if build.id != expected => Err(format!( |
| 59 | "asked to attach build {expected}, but the version reports build {}", |
| 60 | build.id |
| 61 | )), |
| 62 | Some(_) => Ok(()), |
| 63 | } |
| 64 | } |
| 65 | |
| 66 | #[cfg(test)] |
| 67 | mod tests { |
| 68 | use super::*; |
| 69 | use smbcloud_ascapi_aso::build::BuildAttributes; |
| 70 | |
| 71 | fn build(id: &str) -> Build { |
| 72 | Build { |
| 73 | id: id.to_string(), |
| 74 | resource_type: "builds".to_string(), |
| 75 | attributes: BuildAttributes { |
| 76 | version: Some("1".to_string()), |
| 77 | uploaded_date: None, |
| 78 | expired: None, |
| 79 | processing_state: None, |
| 80 | min_os_version: None, |
| 81 | uses_non_exempt_encryption: None, |
| 82 | }, |
| 83 | relationships: None, |
| 84 | } |
| 85 | } |
| 86 | |
| 87 | #[test] |
| 88 | fn a_matching_build_passes() { |
| 89 | assert!(check_attached("b1", Some(&build("b1"))).is_ok()); |
| 90 | } |
| 91 | |
| 92 | #[test] |
| 93 | fn no_build_is_an_error_naming_the_expected_id() { |
| 94 | let error = check_attached("b1", None).unwrap_err(); |
| 95 | assert!(error.contains("b1")); |
| 96 | } |
| 97 | |
| 98 | #[test] |
| 99 | fn a_different_build_names_both_ids() { |
| 100 | let error = check_attached("b1", Some(&build("b2"))).unwrap_err(); |
| 101 | assert!(error.contains("b1") && error.contains("b2")); |
| 102 | } |
| 103 | } |