omni_dev/gmail/
labels_api.rs1use anyhow::Result;
10use url::Url;
11
12use crate::gmail::client::GmailClient;
13use crate::gmail::types::{Label, LabelListResponse};
14
15#[derive(Debug)]
17pub struct LabelsApi<'a> {
18 client: &'a GmailClient,
19}
20
21impl<'a> LabelsApi<'a> {
22 #[must_use]
24 pub fn new(client: &'a GmailClient) -> Self {
25 Self { client }
26 }
27
28 pub async fn list(&self) -> Result<LabelListResponse> {
30 let url = build_labels_list_url(self.client.base_url())?;
31 self.client
32 .get_parsed(url.as_str(), "Failed to parse labels.list response")
33 .await
34 }
35
36 pub async fn get(&self, id: &str) -> Result<Label> {
38 let url = build_label_get_url(self.client.base_url(), id)?;
39 self.client
40 .get_parsed(url.as_str(), "Failed to parse labels.get response")
41 .await
42 }
43}
44
45fn build_labels_list_url(base_url: &str) -> Result<Url> {
46 GmailClient::api_url(base_url, "/gmail/v1/users/me/labels")
47}
48
49fn build_label_get_url(base_url: &str, id: &str) -> Result<Url> {
50 GmailClient::api_url(base_url, &format!("/gmail/v1/users/me/labels/{id}"))
51}
52
53#[cfg(test)]
54#[allow(clippy::unwrap_used, clippy::expect_used)]
55mod tests {
56 use super::*;
57 use crate::gmail::auth::{GmailCredentials, GmailScope};
58 use crate::utils::secret::Secret;
59
60 fn test_credentials() -> GmailCredentials {
61 GmailCredentials {
62 client_id: "client-1".to_string(),
63 client_secret: Secret::new("secret-1"),
64 refresh_token: Secret::new("refresh-1"),
65 scope: GmailScope::ReadOnly,
66 }
67 }
68
69 fn dead_client() -> GmailClient {
70 let mut client = GmailClient::new("http://127.0.0.1:1", &test_credentials()).unwrap();
74 crate::gmail::client::test_support::replace_session(
75 &mut client,
76 &test_credentials(),
77 "http://127.0.0.1:1",
78 );
79 client
80 }
81
82 async fn client_with_bootstrapped_token(server: &wiremock::MockServer) -> GmailClient {
83 wiremock::Mock::given(wiremock::matchers::method("POST"))
84 .and(wiremock::matchers::path("/token"))
85 .respond_with(
86 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
87 "access_token": "test-token",
88 "expires_in": 3600,
89 })),
90 )
91 .mount(server)
92 .await;
93
94 let mut client = GmailClient::new(&server.uri(), &test_credentials()).unwrap();
95 crate::gmail::client::test_support::replace_session(
96 &mut client,
97 &test_credentials(),
98 &format!("{}/token", server.uri()),
99 );
100 client
101 }
102
103 #[test]
106 fn build_labels_list_url_is_exact() {
107 let url = build_labels_list_url("https://gmail.googleapis.com").unwrap();
108 assert_eq!(
109 url.as_str(),
110 "https://gmail.googleapis.com/gmail/v1/users/me/labels"
111 );
112 }
113
114 #[test]
115 fn build_label_get_url_interpolates_id() {
116 let url = build_label_get_url("https://gmail.googleapis.com", "Label_1").unwrap();
117 assert_eq!(
118 url.as_str(),
119 "https://gmail.googleapis.com/gmail/v1/users/me/labels/Label_1"
120 );
121 }
122
123 #[test]
124 fn build_label_get_url_percent_encodes_special_characters_in_user_label_ids() {
125 let url = build_label_get_url("https://gmail.googleapis.com", "a b/c").unwrap();
126 assert!(!url.as_str().contains(' '));
127 }
128
129 #[test]
130 fn build_urls_reject_invalid_base_url() {
131 assert!(build_labels_list_url("not a url").is_err());
132 assert!(build_label_get_url("not a url", "id").is_err());
133 }
134
135 #[tokio::test]
138 async fn list_parses_labels_with_and_without_color() {
139 let server = wiremock::MockServer::start().await;
140 let client = client_with_bootstrapped_token(&server).await;
141 wiremock::Mock::given(wiremock::matchers::method("GET"))
142 .and(wiremock::matchers::path("/gmail/v1/users/me/labels"))
143 .respond_with(
144 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
145 "labels": [
146 {"id": "INBOX", "name": "INBOX", "type": "system"},
147 {
148 "id": "Label_1",
149 "name": "Finance",
150 "type": "user",
151 "color": {"textColor": "#000000", "backgroundColor": "#ffffff"},
152 },
153 ]
154 })),
155 )
156 .expect(1)
157 .mount(&server)
158 .await;
159
160 let result = LabelsApi::new(&client).list().await.unwrap();
161 assert_eq!(result.labels.len(), 2);
162 assert!(result.labels[0].is_system());
163 assert!(result.labels[1].color.is_some());
164 }
165
166 #[tokio::test]
167 async fn list_propagates_api_errors() {
168 let server = wiremock::MockServer::start().await;
169 let client = client_with_bootstrapped_token(&server).await;
170 wiremock::Mock::given(wiremock::matchers::method("GET"))
171 .and(wiremock::matchers::path("/gmail/v1/users/me/labels"))
172 .respond_with(wiremock::ResponseTemplate::new(403).set_body_string("nope"))
173 .mount(&server)
174 .await;
175
176 let err = LabelsApi::new(&client).list().await.unwrap_err();
177 assert!(err.to_string().contains("403"));
178 }
179
180 #[tokio::test]
181 async fn list_propagates_network_errors() {
182 let client = dead_client();
186 let err = LabelsApi::new(&client).list().await.unwrap_err();
187 assert!(err
188 .to_string()
189 .contains("Failed to obtain a Gmail access token"));
190 }
191
192 #[tokio::test]
195 async fn get_builds_correct_url_and_parses_a_single_label() {
196 let server = wiremock::MockServer::start().await;
197 let client = client_with_bootstrapped_token(&server).await;
198 wiremock::Mock::given(wiremock::matchers::method("GET"))
199 .and(wiremock::matchers::path(
200 "/gmail/v1/users/me/labels/Label_1",
201 ))
202 .respond_with(
203 wiremock::ResponseTemplate::new(200).set_body_json(serde_json::json!({
204 "id": "Label_1",
205 "name": "Finance",
206 "type": "user",
207 })),
208 )
209 .expect(1)
210 .mount(&server)
211 .await;
212
213 let label = LabelsApi::new(&client).get("Label_1").await.unwrap();
214 assert_eq!(label.name, "Finance");
215 }
216}