smbcloud_ascapi_aso/
beta_group.rs1use async_trait::async_trait;
12use reqwest::Method;
13use serde::{Deserialize, Serialize};
14use smbcloud_ascapi_core::jsonapi::{Resource, ResourceId};
15use smbcloud_ascapi_core::Client;
16use smbcloud_ascapi_core::Result;
17
18pub const RESOURCE_TYPE: &str = "betaGroups";
19
20#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct BetaGroupAttributes {
24 pub name: Option<String>,
26 pub is_internal_group: Option<bool>,
28 pub has_access_to_all_builds: Option<bool>,
30 pub created_date: Option<String>,
32 pub public_link_enabled: Option<bool>,
34 pub public_link: Option<String>,
36 pub public_link_limit: Option<i64>,
38}
39
40pub type BetaGroup = Resource<BetaGroupAttributes>;
42
43#[derive(Debug, Clone, Serialize)]
49struct AddBuildsBody {
50 data: Vec<ResourceId>,
51}
52
53#[async_trait]
60pub trait BetaGroupsApi {
61 async fn list_beta_groups(&self, app_id: &str) -> Result<Vec<BetaGroup>>;
65
66 async fn get_beta_group(&self, id: &str) -> Result<BetaGroup>;
68
69 async fn add_builds_to_beta_group(&self, group_id: &str, build_ids: &[&str]) -> Result<()>;
76}
77
78#[async_trait]
79impl BetaGroupsApi for Client {
80 async fn list_beta_groups(&self, app_id: &str) -> Result<Vec<BetaGroup>> {
81 let mut groups = self
82 .list_all::<BetaGroupAttributes>(
83 "/v1/betaGroups",
84 &[("filter[app]", app_id), ("limit", "200")],
85 )
86 .await?;
87 groups.sort_by(|a, b| a.attributes.name.cmp(&b.attributes.name));
88 Ok(groups)
89 }
90
91 async fn get_beta_group(&self, id: &str) -> Result<BetaGroup> {
92 use smbcloud_ascapi_core::jsonapi::Document;
93 let path = format!("/v1/betaGroups/{id}");
94 let doc: Document<BetaGroupAttributes> =
95 self.request(Method::GET, &path, &[], None::<&()>).await?;
96 Ok(doc.data)
97 }
98
99 async fn add_builds_to_beta_group(&self, group_id: &str, build_ids: &[&str]) -> Result<()> {
100 let path = format!("/v1/betaGroups/{group_id}/relationships/builds");
101 let body = AddBuildsBody {
102 data: build_ids
103 .iter()
104 .map(|id| ResourceId {
105 resource_type: crate::build::RESOURCE_TYPE,
106 id: id.to_string(),
107 })
108 .collect(),
109 };
110 self.request_no_content(Method::POST, &path, &[], Some(&body))
113 .await
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use smbcloud_ascapi_core::jsonapi::ListDocument;
121
122 #[test]
123 fn deserializes_a_beta_groups_list() {
124 let body = serde_json::json!({
125 "data": [
126 {
127 "id": "BG1",
128 "type": "betaGroups",
129 "attributes": {
130 "name": "Internal Testers",
131 "isInternalGroup": true,
132 "hasAccessToAllBuilds": true,
133 "createdDate": "2024-01-01T00:00:00.000+0000",
134 "publicLinkEnabled": false,
135 "publicLink": null,
136 "publicLinkLimit": null
137 }
138 },
139 {
140 "id": "BG2",
141 "type": "betaGroups",
142 "attributes": {
143 "name": "External Beta",
144 "isInternalGroup": false,
145 "hasAccessToAllBuilds": false,
146 "createdDate": "2024-02-01T00:00:00.000+0000",
147 "publicLinkEnabled": true,
148 "publicLink": "https://testflight.apple.com/join/abc123",
149 "publicLinkLimit": 1000
150 }
151 }
152 ]
153 });
154
155 let doc: ListDocument<BetaGroupAttributes> =
156 serde_json::from_value(body).expect("beta groups list must deserialize");
157 assert_eq!(doc.data.len(), 2);
158
159 let internal = &doc.data[0];
160 assert_eq!(internal.id, "BG1");
161 assert_eq!(
162 internal.attributes.name.as_deref(),
163 Some("Internal Testers")
164 );
165 assert_eq!(internal.attributes.is_internal_group, Some(true));
166 assert_eq!(internal.attributes.has_access_to_all_builds, Some(true));
167 assert_eq!(internal.attributes.public_link_enabled, Some(false));
168
169 let external = &doc.data[1];
170 assert_eq!(external.id, "BG2");
171 assert_eq!(external.attributes.name.as_deref(), Some("External Beta"));
172 assert_eq!(external.attributes.is_internal_group, Some(false));
173 assert_eq!(
174 external.attributes.public_link.as_deref(),
175 Some("https://testflight.apple.com/join/abc123")
176 );
177 assert_eq!(external.attributes.public_link_limit, Some(1000));
178 }
179
180 #[test]
181 fn add_builds_body_serializes_correctly() {
182 let body = AddBuildsBody {
183 data: vec![
184 ResourceId {
185 resource_type: "builds",
186 id: "BUILD1".into(),
187 },
188 ResourceId {
189 resource_type: "builds",
190 id: "BUILD2".into(),
191 },
192 ],
193 };
194 let v = serde_json::to_value(&body).unwrap();
195 let data = v["data"].as_array().unwrap();
196 assert_eq!(data.len(), 2);
197 assert_eq!(data[0]["type"], "builds");
198 assert_eq!(data[0]["id"], "BUILD1");
199 assert_eq!(data[1]["id"], "BUILD2");
200 }
201
202 #[test]
203 fn empty_build_ids_produces_empty_data_array() {
204 let body = AddBuildsBody { data: vec![] };
205 let v = serde_json::to_value(&body).unwrap();
206 assert!(v["data"].as_array().unwrap().is_empty());
207 }
208}