Skip to main content

omni_dev/drive/
about_api.rs

1//! Drive About API wrapper.
2//!
3//! `about.get`, used solely by `drive auth status`'s live authentication
4//! check (mirrors `crate::gmail::profile_api::ProfileApi`, whose
5//! `users.getProfile` plays the same role for `gmail auth status`).
6
7use anyhow::Result;
8use serde::Deserialize;
9use url::Url;
10
11use crate::drive::client::DriveClient;
12
13/// The authenticated user's identity, as embedded in `about.get`'s `user`
14/// field.
15#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
16pub struct AboutUser {
17    /// The user's email address.
18    #[serde(default, rename = "emailAddress")]
19    pub email_address: Option<String>,
20    /// The user's display name.
21    #[serde(default, rename = "displayName")]
22    pub display_name: Option<String>,
23}
24
25/// Response for `GET /drive/v3/about`.
26#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
27pub struct About {
28    /// The authenticated user's identity.
29    pub user: AboutUser,
30}
31
32/// About API façade.
33#[derive(Debug)]
34pub struct AboutApi<'a> {
35    client: &'a DriveClient,
36}
37
38impl<'a> AboutApi<'a> {
39    /// Wraps an existing [`DriveClient`] for `about` operations.
40    #[must_use]
41    pub fn new(client: &'a DriveClient) -> Self {
42        Self { client }
43    }
44
45    /// Fetches the authenticated user's identity.
46    pub async fn get(&self) -> Result<About> {
47        let url = build_about_url(self.client.base_url())?;
48        self.client
49            .get_parsed(url.as_str(), "Failed to parse about.get response")
50            .await
51    }
52}
53
54fn build_about_url(base_url: &str) -> Result<Url> {
55    let mut url = DriveClient::api_url(base_url, "/drive/v3/about")?;
56    url.query_pairs_mut()
57        .append_pair("fields", "user(emailAddress,displayName)");
58    Ok(url)
59}
60
61#[cfg(test)]
62#[allow(clippy::unwrap_used, clippy::expect_used)]
63mod tests {
64    use super::*;
65    use crate::drive::auth::{DriveCredentials, DriveScope};
66    use crate::utils::secret::Secret;
67
68    fn test_credentials() -> DriveCredentials {
69        DriveCredentials {
70            client_id: "client-1".to_string(),
71            client_secret: Secret::new("secret-1"),
72            refresh_token: Secret::new("refresh-1"),
73            scope: DriveScope::ReadOnly,
74        }
75    }
76
77    async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> DriveClient {
78        wiremock::Mock::given(wiremock::matchers::method("POST"))
79            .and(wiremock::matchers::path("/token"))
80            .respond_with(
81                wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
82                    "access_token": "test-token",
83                    "expires_in": 3600,
84                })),
85            )
86            .mount(server)
87            .await;
88
89        let mut client = DriveClient::new(&server.uri(), &test_credentials()).unwrap();
90        crate::drive::client::test_support::replace_session(
91            &mut client,
92            &test_credentials(),
93            &format!("{}/token", server.uri()),
94        );
95        client
96    }
97
98    #[test]
99    fn build_about_url_includes_fields_query_param() {
100        let url = build_about_url("https://www.googleapis.com").unwrap();
101        assert!(url.as_str().contains("/drive/v3/about"));
102        assert!(url.as_str().contains("fields="));
103    }
104
105    #[tokio::test]
106    async fn get_parses_user_fields() {
107        let server = wiremock::MockServer::start().await;
108        let client = client_with_bootstrapped_token(&server).await;
109        wiremock::Mock::given(wiremock::matchers::method("GET"))
110            .and(wiremock::matchers::path("/drive/v3/about"))
111            .respond_with(
112                wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
113                    "user": {
114                        "emailAddress": "user@example.com",
115                        "displayName": "User Name",
116                    }
117                })),
118            )
119            .expect(1)
120            .mount(&server)
121            .await;
122
123        let about = AboutApi::new(&client).get().await.unwrap();
124        assert_eq!(
125            about.user.email_address.as_deref(),
126            Some("user@example.com")
127        );
128        assert_eq!(about.user.display_name.as_deref(), Some("User Name"));
129    }
130
131    #[tokio::test]
132    async fn get_propagates_api_errors() {
133        let server = wiremock::MockServer::start().await;
134        let client = client_with_bootstrapped_token(&server).await;
135        wiremock::Mock::given(wiremock::matchers::method("GET"))
136            .and(wiremock::matchers::path("/drive/v3/about"))
137            .respond_with(wiremock::ResponseTemplate::new(403).set_body_string("Forbidden"))
138            .mount(&server)
139            .await;
140
141        let err = AboutApi::new(&client).get().await.unwrap_err();
142        assert!(err.to_string().contains("403"));
143    }
144
145    #[tokio::test]
146    async fn get_rejects_invalid_base_url() {
147        let client = DriveClient::new("not a url", &test_credentials()).unwrap();
148        let err = AboutApi::new(&client).get().await.unwrap_err();
149        assert!(err.to_string().contains("Invalid Drive base URL"));
150    }
151}