omni_dev/gmail/
profile_api.rs1use anyhow::Result;
11use serde::Deserialize;
12
13use crate::gmail::client::GmailClient;
14
15#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
17pub struct Profile {
18 #[serde(rename = "emailAddress")]
20 pub email_address: String,
21 #[serde(rename = "messagesTotal")]
23 pub messages_total: i64,
24 #[serde(rename = "threadsTotal")]
26 pub threads_total: i64,
27 #[serde(rename = "historyId")]
29 pub history_id: String,
30}
31
32#[derive(Debug)]
34pub struct ProfileApi<'a> {
35 client: &'a GmailClient,
36}
37
38impl<'a> ProfileApi<'a> {
39 #[must_use]
41 pub fn new(client: &'a GmailClient) -> Self {
42 Self { client }
43 }
44
45 pub async fn get(&self) -> Result<Profile> {
47 let url = GmailClient::api_url(self.client.base_url(), "/gmail/v1/users/me/profile")?;
48 self.client
49 .get_parsed(url.as_str(), "Failed to parse users.getProfile response")
50 .await
51 }
52}
53
54#[cfg(test)]
55#[allow(clippy::unwrap_used, clippy::expect_used)]
56mod tests {
57 use super::*;
58 use crate::gmail::auth::{GmailCredentials, GmailScope};
59 use crate::utils::secret::Secret;
60
61 fn test_credentials() -> GmailCredentials {
62 GmailCredentials {
63 client_id: "client-1".to_string(),
64 client_secret: Secret::new("secret-1"),
65 refresh_token: Secret::new("refresh-1"),
66 scope: GmailScope::ReadOnly,
67 }
68 }
69
70 async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> GmailClient {
71 wiremock::Mock::given(wiremock::matchers::method("POST"))
72 .and(wiremock::matchers::path("/token"))
73 .respond_with(
74 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
75 "access_token": "test-token",
76 "expires_in": 3600,
77 })),
78 )
79 .mount(server)
80 .await;
81
82 let mut client = GmailClient::new(&server.uri(), &test_credentials()).unwrap();
83 crate::gmail::client::test_support::replace_session(
84 &mut client,
85 &test_credentials(),
86 &format!("{}/token", server.uri()),
87 );
88 client
89 }
90
91 #[tokio::test]
92 async fn get_parses_profile_fields() {
93 let server = wiremock::MockServer::start().await;
94 let client = client_with_bootstrapped_token(&server).await;
95 wiremock::Mock::given(wiremock::matchers::method("GET"))
96 .and(wiremock::matchers::path("/gmail/v1/users/me/profile"))
97 .respond_with(
98 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
99 "emailAddress": "user@example.com",
100 "messagesTotal": 100,
101 "threadsTotal": 42,
102 "historyId": "123456",
103 })),
104 )
105 .expect(1)
106 .mount(&server)
107 .await;
108
109 let profile = ProfileApi::new(&client).get().await.unwrap();
110 assert_eq!(profile.email_address, "user@example.com");
111 assert_eq!(profile.messages_total, 100);
112 assert_eq!(profile.threads_total, 42);
113 assert_eq!(profile.history_id, "123456");
114 }
115
116 #[tokio::test]
117 async fn get_propagates_api_errors() {
118 let server = wiremock::MockServer::start().await;
119 let client = client_with_bootstrapped_token(&server).await;
120 wiremock::Mock::given(wiremock::matchers::method("GET"))
121 .and(wiremock::matchers::path("/gmail/v1/users/me/profile"))
122 .respond_with(wiremock::ResponseTemplate::new(403).set_body_string("Forbidden"))
123 .mount(&server)
124 .await;
125
126 let err = ProfileApi::new(&client).get().await.unwrap_err();
127 assert!(err.to_string().contains("403"));
128 }
129
130 #[tokio::test]
131 async fn get_rejects_invalid_base_url() {
132 let client = GmailClient::new("not a url", &test_credentials()).unwrap();
133 let err = ProfileApi::new(&client).get().await.unwrap_err();
134 assert!(err.to_string().contains("Invalid Gmail base URL"));
135 }
136}