cloud_terrastodon_azure 0.36.0

Helpers for interacting with Azure for the Cloud Terrastodon project
use cloud_terrastodon_azure_types::TenantLicense;
use cloud_terrastodon_azure_types::TenantLicenseCollection;
use cloud_terrastodon_command::CacheKey;
use cloud_terrastodon_command::CacheableCommand;
use cloud_terrastodon_command::async_trait;
use cloud_terrastodon_rest::RestRequest;
use eyre::Result;
use serde::Deserialize;
use std::path::PathBuf;

#[must_use = "This is a future request, you must .await it"]
pub struct TenantLicenseListRequest;

pub fn fetch_all_tenant_licenses() -> TenantLicenseListRequest {
    TenantLicenseListRequest
}

#[async_trait]
impl CacheableCommand for TenantLicenseListRequest {
    type Output = TenantLicenseCollection;

    fn cache_key(&self) -> CacheKey {
        CacheKey::new(PathBuf::from_iter(["az", "rest", "GET", "subscribedSkus"]))
    }

    async fn run(self) -> Result<Self::Output> {
        let url = "https://graph.microsoft.com/v1.0/subscribedSkus";
        #[derive(Deserialize)]
        #[serde(deny_unknown_fields)]
        struct Response {
            #[expect(dead_code)]
            #[serde(rename = "@odata.context")]
            context: String,
            value: Vec<TenantLicense>,
        }
        let resp = RestRequest::new(http::Method::GET, url)?
            .cache(self.cache_key())
            .receive::<Response>()
            .await?;
        Ok(TenantLicenseCollection(resp.value))
    }
}

cloud_terrastodon_command::impl_cacheable_into_future!(TenantLicenseListRequest);

#[cfg(test)]
pub mod test_helpers {
    use crate::fetch_all_tenant_licenses;
    use cloud_terrastodon_command::CommandOutput;
    use cloud_terrastodon_command::bstr::ByteSlice;

    /// If the given result is a failure, it MUST contain an AAD Premium P2 license error.
    /// If it is an error ,we also validate that the tenant licenses do not contain the AAD Premium P2 license.
    /// If it is a success, we validate that the tenant licenses DO contain the AAD Premium P2 license.
    pub async fn expect_aad_premium_p2_license<T>(
        result: eyre::Result<T>,
    ) -> eyre::Result<Option<T>> {
        let tenant_licenses = fetch_all_tenant_licenses().await?;
        let has_aad_premium_p2_license = tenant_licenses.has_aad_premium_p2();
        match result {
            Ok(x) => {
                eyre::ensure!(
                    has_aad_premium_p2_license,
                    "Expected AAD Premium P2 license, but it was not found in tenant licenses: {tenant_licenses:#?}"
                );
                Ok(Some(x))
            }
            Err(e) => {
                if has_aad_premium_p2_license {
                    return Err(e.wrap_err(
                        "A result failed while wrapped in a guard that expected AAD Premium P2 license to be missing, but it was found in tenant licenses, this means the inner command truly failed?"
                    ));
                }
                let expected = "The tenant needs to have Microsoft Entra ID P2 or Microsoft Entra ID Governance license.";
                let has_expected_message = e
                    .downcast_ref::<CommandOutput>()
                    .map(|command_output| command_output.stderr.contains_str(expected))
                    .unwrap_or(false)
                    || format!("{e:?}").contains(expected);
                if !has_expected_message {
                    return Err(e.wrap_err("A result failed while wrapped in a guard that expected AAD Premium P2 license to be missing, but the error did not contain expected AAD Premium P2 license error text"));
                }
                eprintln!(
                    "Result failed with expected error due to missing AAD_PREMIUM_P2 licenses."
                );
                Ok(None)
            }
        }
    }
}

#[cfg(test)]
mod test {
    use crate::fetch_all_tenant_licenses;

    #[tokio::test]
    pub async fn it_works() -> eyre::Result<()> {
        let tenant_licenses = fetch_all_tenant_licenses().await?;
        assert!(!tenant_licenses.0.is_empty());
        Ok(())
    }
}