Skip to main content

azure_lite_rs/api/
subscriptions.rs

1//! Azure Subscriptions API client.
2//!
3//! Lists all Azure subscriptions accessible to the authenticated principal.
4//! This is a tenant-level call — no subscription ID appears in the path.
5//! The bearer token issued by `AzureCredential::get_token()` is tenant-scoped
6//! and covers all subscriptions.
7
8use crate::{
9    AzureError, AzureHttpClient, Result,
10    ops::subscriptions::SubscriptionsOps,
11    types::subscriptions::{SubscriptionInfo, SubscriptionListResponse},
12};
13
14/// Client for the Azure Subscriptions API.
15///
16/// Wraps [`SubscriptionsOps`] with an ergonomic, auto-paginating method.
17pub struct SubscriptionsClient<'a> {
18    ops: SubscriptionsOps<'a>,
19}
20
21impl<'a> SubscriptionsClient<'a> {
22    /// Create a new Azure Subscriptions API client.
23    pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
24        Self {
25            ops: SubscriptionsOps::new(client),
26        }
27    }
28
29    /// List all subscriptions accessible to the authenticated principal.
30    ///
31    /// Handles pagination automatically — returns **all** subscriptions.
32    ///
33    /// This is a tenant-level call; no subscription ID is required in the path.
34    /// The bearer token used by the underlying client is tenant-scoped and
35    /// grants access to every subscription under the tenant.
36    pub async fn list(&self) -> Result<Vec<SubscriptionInfo>> {
37        let mut all = Vec::new();
38        let first_page = self.ops.list_subscriptions().await?;
39        all.extend(first_page.value);
40
41        let mut next_link = first_page.next_link;
42        while let Some(url) = next_link {
43            let page = self.fetch_page(&url).await?;
44            all.extend(page.value);
45            next_link = page.next_link;
46        }
47
48        Ok(all)
49    }
50
51    async fn fetch_page(&self, url: &str) -> Result<SubscriptionListResponse> {
52        let response = self.ops.client.get(url).await?;
53        let response = response.error_for_status().await?;
54        let bytes = response
55            .bytes()
56            .await
57            .map_err(|e| AzureError::InvalidResponse {
58                message: format!("Failed to read subscriptions page: {e}"),
59                body: None,
60            })?;
61        serde_json::from_slice(&bytes).map_err(|e| AzureError::InvalidResponse {
62            message: format!("Failed to parse subscriptions page: {e}"),
63            body: Some(String::from_utf8_lossy(&bytes).to_string()),
64        })
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use crate::MockClient;
72
73    fn make_client(mock: MockClient) -> AzureHttpClient {
74        AzureHttpClient::from_mock(mock)
75    }
76
77    #[tokio::test]
78    async fn list_returns_all_subscriptions_single_page() {
79        let mut mock = MockClient::new();
80        mock.expect_get("/subscriptions")
81            .returning_json(serde_json::json!({
82                "value": [
83                    {
84                        "subscriptionId": "35010625-993d-403a-a18c-9f32945c327e",
85                        "displayName": "Fractal Production",
86                        "tenantId": "1dff187b-fb27-4db3-a7b9-286a24aaa818",
87                        "state": "Enabled"
88                    }
89                ]
90            }));
91        let client = make_client(mock);
92        let subs = client.subscriptions().list().await.expect("list failed");
93        assert_eq!(subs.len(), 1);
94        assert_eq!(
95            subs[0].subscription_id.as_deref(),
96            Some("35010625-993d-403a-a18c-9f32945c327e")
97        );
98        assert_eq!(subs[0].display_name.as_deref(), Some("Fractal Production"));
99        assert_eq!(subs[0].state.as_deref(), Some("Enabled"));
100    }
101
102    #[tokio::test]
103    async fn list_returns_empty_when_no_subscriptions() {
104        let mut mock = MockClient::new();
105        mock.expect_get("/subscriptions")
106            .returning_json(serde_json::json!({ "value": [] }));
107        let client = make_client(mock);
108        let subs = client.subscriptions().list().await.expect("list failed");
109        assert_eq!(subs.len(), 0);
110    }
111}