Skip to main content

azure_lite_rs/api/
functions.rs

1//! Azure Functions API client.
2//!
3//! Wraps the ARM management plane operations for Azure Functions (App Service
4//! Web Sites API). All URL construction is in `ops::functions::FunctionsOps`.
5//! `subscription_id` is auto-injected from the parent `AzureHttpClient`.
6
7use crate::{
8    AzureHttpClient, Result,
9    ops::functions::FunctionsOps,
10    types::functions::{
11        AppSettingsResult, AppSettingsUpdateRequest, Function, FunctionApp,
12        FunctionAppCreateRequest, FunctionAppListResult, FunctionListResult,
13    },
14};
15
16/// Client for the Azure Functions ARM management plane.
17///
18/// Wraps [`FunctionsOps`] with ergonomic signatures that auto-inject
19/// `subscription_id` from the parent [`AzureHttpClient`].
20pub struct FunctionsClient<'a> {
21    ops: FunctionsOps<'a>,
22    client: &'a AzureHttpClient,
23}
24
25impl<'a> FunctionsClient<'a> {
26    /// Create a new Azure Functions API client.
27    pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
28        Self {
29            ops: FunctionsOps::new(client),
30            client,
31        }
32    }
33
34    // --- Function App operations ---
35
36    /// Lists all Function Apps in the subscription.
37    pub async fn list_function_apps(&self) -> Result<FunctionAppListResult> {
38        self.ops
39            .list_function_apps(self.client.subscription_id())
40            .await
41    }
42
43    /// Lists all Function Apps in a resource group.
44    pub async fn list_function_apps_by_resource_group(
45        &self,
46        resource_group_name: &str,
47    ) -> Result<FunctionAppListResult> {
48        self.ops
49            .list_function_apps_by_resource_group(
50                self.client.subscription_id(),
51                resource_group_name,
52            )
53            .await
54    }
55
56    /// Gets a Function App.
57    pub async fn get_function_app(
58        &self,
59        resource_group_name: &str,
60        name: &str,
61    ) -> Result<FunctionApp> {
62        self.ops
63            .get_function_app(self.client.subscription_id(), resource_group_name, name)
64            .await
65    }
66
67    /// Creates or updates a Function App.
68    pub async fn create_function_app(
69        &self,
70        resource_group_name: &str,
71        name: &str,
72        body: &FunctionAppCreateRequest,
73    ) -> Result<FunctionApp> {
74        self.ops
75            .create_function_app(
76                self.client.subscription_id(),
77                resource_group_name,
78                name,
79                body,
80            )
81            .await
82    }
83
84    /// Deletes a Function App.
85    pub async fn delete_function_app(&self, resource_group_name: &str, name: &str) -> Result<()> {
86        self.ops
87            .delete_function_app(self.client.subscription_id(), resource_group_name, name)
88            .await
89    }
90
91    // --- Function operations ---
92
93    /// Lists all functions in a Function App.
94    pub async fn list_functions(
95        &self,
96        resource_group_name: &str,
97        name: &str,
98    ) -> Result<FunctionListResult> {
99        self.ops
100            .list_functions(self.client.subscription_id(), resource_group_name, name)
101            .await
102    }
103
104    /// Gets a specific function in a Function App.
105    pub async fn get_function(
106        &self,
107        resource_group_name: &str,
108        name: &str,
109        function_name: &str,
110    ) -> Result<Function> {
111        self.ops
112            .get_function(
113                self.client.subscription_id(),
114                resource_group_name,
115                name,
116                function_name,
117            )
118            .await
119    }
120
121    // --- App Settings operations ---
122
123    /// Gets the application settings of a Function App.
124    pub async fn list_app_settings(
125        &self,
126        resource_group_name: &str,
127        name: &str,
128    ) -> Result<AppSettingsResult> {
129        self.ops
130            .list_app_settings(self.client.subscription_id(), resource_group_name, name)
131            .await
132    }
133
134    /// Updates the application settings of a Function App.
135    pub async fn update_app_settings(
136        &self,
137        resource_group_name: &str,
138        name: &str,
139        body: &AppSettingsUpdateRequest,
140    ) -> Result<AppSettingsResult> {
141        self.ops
142            .update_app_settings(
143                self.client.subscription_id(),
144                resource_group_name,
145                name,
146                body,
147            )
148            .await
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155    use crate::MockClient;
156
157    const SUB_ID: &str = "test-subscription-id";
158    const RG: &str = "test-rg";
159    const APP: &str = "cloud-lite-test-func-app";
160    const FUNC: &str = "MyFunction";
161
162    fn make_client(mock: MockClient) -> AzureHttpClient {
163        AzureHttpClient::from_mock(mock)
164    }
165
166    fn app_json() -> serde_json::Value {
167        serde_json::json!({
168            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}"),
169            "name": APP,
170            "type": "Microsoft.Web/sites",
171            "kind": "functionapp,linux",
172            "location": "eastus",
173            "properties": {
174                "state": "Running",
175                "hostNames": ["cloud-lite-test-func-app.azurewebsites.net"],
176                "defaultHostName": "cloud-lite-test-func-app.azurewebsites.net",
177                "resourceGroup": RG,
178                "serverFarmId": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/serverfarms/EastUSLinuxDynamicPlan"),
179                "siteConfig": {
180                    "numberOfWorkers": 1,
181                    "linuxFxVersion": "Python|3.11"
182                }
183            }
184        })
185    }
186
187    fn function_json() -> serde_json::Value {
188        serde_json::json!({
189            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}/functions/{FUNC}"),
190            "name": format!("{APP}/{FUNC}"),
191            "type": "Microsoft.Web/sites/functions",
192            "properties": {
193                "name": FUNC,
194                "functionAppId": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}"),
195                "isDisabled": false
196            }
197        })
198    }
199
200    fn settings_json() -> serde_json::Value {
201        serde_json::json!({
202            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}/config/appsettings"),
203            "name": "appsettings",
204            "type": "Microsoft.Web/sites/config",
205            "properties": {
206                "AzureWebJobsStorage": "DefaultEndpointsProtocol=https;...",
207                "FUNCTIONS_WORKER_RUNTIME": "python"
208            }
209        })
210    }
211
212    #[tokio::test]
213    async fn list_function_apps_returns_list() {
214        let mut mock = MockClient::new();
215        mock.expect_get(&format!(
216            "/subscriptions/{SUB_ID}/providers/Microsoft.Web/sites"
217        ))
218        .returning_json(serde_json::json!({ "value": [app_json()] }));
219        let client = make_client(mock);
220        let result = client
221            .functions()
222            .list_function_apps()
223            .await
224            .expect("list_function_apps failed");
225        assert_eq!(result.value.len(), 1);
226        let app = &result.value[0];
227        assert_eq!(app.name.as_deref(), Some(APP));
228        let props = app.properties.as_ref().unwrap();
229        assert_eq!(props.state.as_deref(), Some("Running"));
230    }
231
232    #[tokio::test]
233    async fn list_function_apps_by_resource_group_returns_list() {
234        let mut mock = MockClient::new();
235        mock.expect_get(&format!(
236            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites"
237        ))
238        .returning_json(serde_json::json!({ "value": [app_json()] }));
239        let client = make_client(mock);
240        let result = client
241            .functions()
242            .list_function_apps_by_resource_group(RG)
243            .await
244            .expect("list_function_apps_by_resource_group failed");
245        assert_eq!(result.value.len(), 1);
246    }
247
248    #[tokio::test]
249    async fn get_function_app_deserializes_properties() {
250        let mut mock = MockClient::new();
251        mock.expect_get(&format!(
252            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}"
253        ))
254        .returning_json(app_json());
255        let client = make_client(mock);
256        let app = client
257            .functions()
258            .get_function_app(RG, APP)
259            .await
260            .expect("get_function_app failed");
261        assert_eq!(app.name.as_deref(), Some(APP));
262        let props = app.properties.as_ref().unwrap();
263        assert_eq!(
264            props.default_host_name.as_deref(),
265            Some("cloud-lite-test-func-app.azurewebsites.net")
266        );
267        let config = props.site_config.as_ref().unwrap();
268        assert_eq!(config.linux_fx_version.as_deref(), Some("Python|3.11"));
269    }
270
271    #[tokio::test]
272    async fn create_function_app_sends_body() {
273        let mut mock = MockClient::new();
274        mock.expect_put(&format!(
275            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}"
276        ))
277        .returning_json(app_json());
278        let client = make_client(mock);
279        let body = FunctionAppCreateRequest {
280            location: "eastus".into(),
281            kind: Some("functionapp,linux".into()),
282            ..Default::default()
283        };
284        let app = client
285            .functions()
286            .create_function_app(RG, APP, &body)
287            .await
288            .expect("create_function_app failed");
289        assert_eq!(app.name.as_deref(), Some(APP));
290    }
291
292    #[tokio::test]
293    async fn delete_function_app_succeeds() {
294        let mut mock = MockClient::new();
295        mock.expect_delete(&format!(
296            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}"
297        ))
298        .returning_json(serde_json::json!({}));
299        let client = make_client(mock);
300        client
301            .functions()
302            .delete_function_app(RG, APP)
303            .await
304            .expect("delete_function_app failed");
305    }
306
307    #[tokio::test]
308    async fn list_functions_returns_list() {
309        let mut mock = MockClient::new();
310        mock.expect_get(
311            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}/functions"),
312        )
313        .returning_json(serde_json::json!({ "value": [function_json()] }));
314        let client = make_client(mock);
315        let result = client
316            .functions()
317            .list_functions(RG, APP)
318            .await
319            .expect("list_functions failed");
320        assert_eq!(result.value.len(), 1);
321        let f = &result.value[0];
322        let props = f.properties.as_ref().unwrap();
323        assert_eq!(props.is_disabled, Some(false));
324    }
325
326    #[tokio::test]
327    async fn get_function_deserializes_properties() {
328        let mut mock = MockClient::new();
329        mock.expect_get(
330            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}/functions/{FUNC}"),
331        )
332        .returning_json(function_json());
333        let client = make_client(mock);
334        let f = client
335            .functions()
336            .get_function(RG, APP, FUNC)
337            .await
338            .expect("get_function failed");
339        let props = f.properties.as_ref().unwrap();
340        assert_eq!(props.name.as_deref(), Some(FUNC));
341    }
342
343    #[tokio::test]
344    async fn list_app_settings_returns_settings() {
345        let mut mock = MockClient::new();
346        mock.expect_post(
347            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}/config/appsettings/list"),
348        )
349        .returning_json(settings_json());
350        let client = make_client(mock);
351        let settings = client
352            .functions()
353            .list_app_settings(RG, APP)
354            .await
355            .expect("list_app_settings failed");
356        assert!(settings.properties.contains_key("AzureWebJobsStorage"));
357        assert!(settings.properties.contains_key("FUNCTIONS_WORKER_RUNTIME"));
358    }
359
360    #[tokio::test]
361    async fn update_app_settings_sends_body() {
362        let mut mock = MockClient::new();
363        mock.expect_put(
364            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Web/sites/{APP}/config/appsettings"),
365        )
366        .returning_json(settings_json());
367        let client = make_client(mock);
368        let mut props = std::collections::HashMap::new();
369        props.insert("CLOUD_LITE_TEST".to_string(), "hello".to_string());
370        let body = AppSettingsUpdateRequest { properties: props };
371        let result = client
372            .functions()
373            .update_app_settings(RG, APP, &body)
374            .await
375            .expect("update_app_settings failed");
376        assert_eq!(result.name.as_deref(), Some("appsettings"));
377    }
378}