use async_trait::async_trait;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use smbcloud_ascapi_core::jsonapi::{Resource, ResourceId};
use smbcloud_ascapi_core::Client;
use smbcloud_ascapi_core::Result;
pub const RESOURCE_TYPE: &str = "betaGroups";
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BetaGroupAttributes {
pub name: Option<String>,
pub is_internal_group: Option<bool>,
pub has_access_to_all_builds: Option<bool>,
pub created_date: Option<String>,
pub public_link_enabled: Option<bool>,
pub public_link: Option<String>,
pub public_link_limit: Option<i64>,
}
pub type BetaGroup = Resource<BetaGroupAttributes>;
#[derive(Debug, Clone, Serialize)]
struct AddBuildsBody {
data: Vec<ResourceId>,
}
#[async_trait]
pub trait BetaGroupsApi {
async fn list_beta_groups(&self, app_id: &str) -> Result<Vec<BetaGroup>>;
async fn get_beta_group(&self, id: &str) -> Result<BetaGroup>;
async fn add_builds_to_beta_group(&self, group_id: &str, build_ids: &[&str]) -> Result<()>;
}
#[async_trait]
impl BetaGroupsApi for Client {
async fn list_beta_groups(&self, app_id: &str) -> Result<Vec<BetaGroup>> {
let mut groups = self
.list_all::<BetaGroupAttributes>(
"/v1/betaGroups",
&[("filter[app]", app_id), ("limit", "200")],
)
.await?;
groups.sort_by(|a, b| a.attributes.name.cmp(&b.attributes.name));
Ok(groups)
}
async fn get_beta_group(&self, id: &str) -> Result<BetaGroup> {
use smbcloud_ascapi_core::jsonapi::Document;
let path = format!("/v1/betaGroups/{id}");
let doc: Document<BetaGroupAttributes> =
self.request(Method::GET, &path, &[], None::<&()>).await?;
Ok(doc.data)
}
async fn add_builds_to_beta_group(&self, group_id: &str, build_ids: &[&str]) -> Result<()> {
let path = format!("/v1/betaGroups/{group_id}/relationships/builds");
let body = AddBuildsBody {
data: build_ids
.iter()
.map(|id| ResourceId {
resource_type: crate::build::RESOURCE_TYPE,
id: id.to_string(),
})
.collect(),
};
self.request_no_content(Method::POST, &path, &[], Some(&body))
.await
}
}
#[cfg(test)]
mod tests {
use super::*;
use smbcloud_ascapi_core::jsonapi::ListDocument;
#[test]
fn deserializes_a_beta_groups_list() {
let body = serde_json::json!({
"data": [
{
"id": "BG1",
"type": "betaGroups",
"attributes": {
"name": "Internal Testers",
"isInternalGroup": true,
"hasAccessToAllBuilds": true,
"createdDate": "2024-01-01T00:00:00.000+0000",
"publicLinkEnabled": false,
"publicLink": null,
"publicLinkLimit": null
}
},
{
"id": "BG2",
"type": "betaGroups",
"attributes": {
"name": "External Beta",
"isInternalGroup": false,
"hasAccessToAllBuilds": false,
"createdDate": "2024-02-01T00:00:00.000+0000",
"publicLinkEnabled": true,
"publicLink": "https://testflight.apple.com/join/abc123",
"publicLinkLimit": 1000
}
}
]
});
let doc: ListDocument<BetaGroupAttributes> =
serde_json::from_value(body).expect("beta groups list must deserialize");
assert_eq!(doc.data.len(), 2);
let internal = &doc.data[0];
assert_eq!(internal.id, "BG1");
assert_eq!(
internal.attributes.name.as_deref(),
Some("Internal Testers")
);
assert_eq!(internal.attributes.is_internal_group, Some(true));
assert_eq!(internal.attributes.has_access_to_all_builds, Some(true));
assert_eq!(internal.attributes.public_link_enabled, Some(false));
let external = &doc.data[1];
assert_eq!(external.id, "BG2");
assert_eq!(external.attributes.name.as_deref(), Some("External Beta"));
assert_eq!(external.attributes.is_internal_group, Some(false));
assert_eq!(
external.attributes.public_link.as_deref(),
Some("https://testflight.apple.com/join/abc123")
);
assert_eq!(external.attributes.public_link_limit, Some(1000));
}
#[test]
fn add_builds_body_serializes_correctly() {
let body = AddBuildsBody {
data: vec![
ResourceId {
resource_type: "builds",
id: "BUILD1".into(),
},
ResourceId {
resource_type: "builds",
id: "BUILD2".into(),
},
],
};
let v = serde_json::to_value(&body).unwrap();
let data = v["data"].as_array().unwrap();
assert_eq!(data.len(), 2);
assert_eq!(data[0]["type"], "builds");
assert_eq!(data[0]["id"], "BUILD1");
assert_eq!(data[1]["id"], "BUILD2");
}
#[test]
fn empty_build_ids_produces_empty_data_array() {
let body = AddBuildsBody { data: vec![] };
let v = serde_json::to_value(&body).unwrap();
assert!(v["data"].as_array().unwrap().is_empty());
}
}