65
/// check whether a bundle ID is already registered before trying to
66
/// create it, or before creating an App Store Version under an app
67
/// that uses it.
68
+ ///
69
+ /// App Store Connect's `filter[identifier]` is a case-insensitive
70
+ /// substring match: `ai.splitfire.SplitfireAI` also returns
71
+ /// `….topshelf`, `-Debug` and `ai.splitfire.SplitFireAIWatch`. The rows
72
+ /// are cut to the exact identifier here, so an empty result really
73
+ /// means "not registered".
74
async fn list_bundle_ids(&self, filter_identifier: Option<&str>) -> Result<Vec<BundleId>>;
75
76
/// `POST /v1/bundleIds` — registers a new bundle ID with the developer
90
// truncate the result.
91
query.push(("limit", "200"));
92
87
- self.list_all::<BundleIdAttributes>("/v1/bundleIds", &query)
88
- .await
93
+ let bundle_ids = self
94
+ .list_all::<BundleIdAttributes>("/v1/bundleIds", &query)
95
+ .await?;
96
+ Ok(match filter_identifier {
97
+ Some(identifier) => exact_identifier(bundle_ids, identifier),
98
+ None => bundle_ids,
99
+ })
100
}
101
102
async fn create_bundle_id(&self, attributes: BundleIdCreateAttributes) -> Result<BundleId> {
114
}
115
}
116
117
+/// Keep the bundle IDs whose identifier is exactly `identifier`.
118
+fn exact_identifier(bundle_ids: Vec<BundleId>, identifier: &str) -> Vec<BundleId> {
119
+ bundle_ids
120
+ .into_iter()
121
+ .filter(|bundle_id| bundle_id.attributes.identifier.as_deref() == Some(identifier))
122
+ .collect()
123
+}
124
+
125
#[cfg(test)]
126
mod tests {
127
use super::*;
128
use smbcloud_ascapi_core::jsonapi::ListDocument;
129
130
+ /// Trimmed from what `filter[identifier]=ai.splitfire.SplitfireAI`
131
+ /// actually returned: neighbours, a Services ID, and a different app
132
+ /// matched through case folding.
133
+ #[test]
134
+ fn an_identifier_filter_keeps_only_the_exact_match() {
135
+ let doc: ListDocument<BundleIdAttributes> = serde_json::from_value(serde_json::json!({
136
+ "data": [
137
+ { "id": "A", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitfireAIAuth", "platform": "SERVICES" } },
138
+ { "id": "B", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitfireAI", "platform": "UNIVERSAL" } },
139
+ { "id": "C", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitfireAI.topshelf", "platform": "UNIVERSAL" } },
140
+ { "id": "D", "type": "bundleIds", "attributes": { "identifier": "ai.splitfire.SplitFireAIWatch", "platform": "UNIVERSAL" } }
141
+ ]
142
+ }))
143
+ .unwrap();
144
+ let exact = exact_identifier(doc.data.clone(), "ai.splitfire.SplitfireAI");
145
+ assert_eq!(
146
+ exact.iter().map(|b| b.id.as_str()).collect::<Vec<_>>(),
147
+ ["B"]
148
+ );
149
+ assert!(exact_identifier(doc.data, "ai.splitfire.Splitfire").is_empty());
150
+ }
151
+
152
#[test]
153
fn a_services_identifier_does_not_break_the_list() {
154
// Regression: `GET /v1/bundleIds` returns every identifier a team