omni_dev/drive/slides/
client.rs1use anyhow::Result;
15
16use crate::drive::api_client::GoogleApiClient;
17use crate::drive::auth::DriveCredentials;
18use crate::drive::client::{DriveClient, SERVICE_TAG};
19use crate::utils::env::{EnvSource, SystemEnv};
20
21pub const SLIDES_API_URL: &str = "SLIDES_API_URL";
27
28const API_NAME: &str = "Slides";
33
34pub struct SlidesClient {
36 inner: GoogleApiClient,
37}
38
39impl std::fmt::Debug for SlidesClient {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 f.debug_struct("SlidesClient")
44 .field("base_url", &self.inner.base_url())
45 .finish_non_exhaustive()
46 }
47}
48
49impl SlidesClient {
50 const DEFAULT_BASE_URL: &'static str = "https://slides.googleapis.com";
56
57 pub fn new(base_url: &str, credentials: &DriveCredentials) -> Result<Self> {
63 Ok(Self {
64 inner: GoogleApiClient::new(base_url, credentials, SERVICE_TAG, API_NAME)?,
65 })
66 }
67
68 pub fn from_drive_client(drive: &DriveClient) -> Result<Self> {
80 Self::from_drive_client_with(&SystemEnv, drive)
81 }
82
83 pub(crate) fn from_drive_client_with(
87 env: &impl EnvSource,
88 drive: &DriveClient,
89 ) -> Result<Self> {
90 let base_url = Self::resolve_base_url(env);
91 Ok(Self {
92 inner: drive.transport().derived(&base_url, SERVICE_TAG, API_NAME),
93 })
94 }
95
96 fn resolve_base_url(env: &impl EnvSource) -> String {
97 env.var(SLIDES_API_URL)
98 .filter(|s| !s.is_empty())
99 .unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string())
100 }
101
102 #[must_use]
104 pub fn base_url(&self) -> &str {
105 self.inner.base_url()
106 }
107
108 pub(in crate::drive) fn transport(&self) -> &GoogleApiClient {
110 &self.inner
111 }
112}
113
114#[cfg(test)]
117pub(crate) mod test_support {
118 use super::{DriveClient, DriveCredentials, SlidesClient, SLIDES_API_URL};
119 use crate::drive::auth::DriveGrantedScopes;
120 use crate::test_support::env::MapEnv;
121 use crate::utils::secret::Secret;
122 use wiremock::matchers::{method, path};
123 use wiremock::{Mock, MockServer, ResponseTemplate};
124
125 #[allow(clippy::unwrap_used)]
128 pub(crate) async fn mock_clients(server: &MockServer) -> (DriveClient, SlidesClient) {
129 Mock::given(method("POST"))
130 .and(path("/token"))
131 .respond_with(
132 ResponseTemplate::new(200)
133 .set_body_json(serde_json::json!({"access_token":"test", "expires_in":3600})),
134 )
135 .mount(server)
136 .await;
137 let credentials = DriveCredentials {
138 client_id: "client".into(),
139 client_secret: Secret::new("secret"),
140 refresh_token: Secret::new("refresh"),
141 scope: DriveGrantedScopes::READONLY,
142 };
143 let mut drive = DriveClient::new(&server.uri(), &credentials).unwrap();
144 crate::drive::client::test_support::replace_session(
145 &mut drive,
146 &credentials,
147 &format!("{}/token", server.uri()),
148 );
149 let slides = SlidesClient::from_drive_client_with(
150 &MapEnv::new().with(SLIDES_API_URL, &server.uri()),
151 &drive,
152 )
153 .unwrap();
154 (drive, slides)
155 }
156}
157
158#[cfg(test)]
159#[allow(clippy::unwrap_used, clippy::expect_used)]
160mod tests {
161 use super::*;
162 use crate::drive::auth::DriveGrantedScopes;
163 use crate::test_support::env::MapEnv;
164 use crate::utils::secret::Secret;
165
166 fn test_credentials() -> DriveCredentials {
167 DriveCredentials {
168 client_id: "client-1".to_string(),
169 client_secret: Secret::new("secret-1"),
170 refresh_token: Secret::new("refresh-1"),
171 scope: DriveGrantedScopes::READONLY,
172 }
173 }
174
175 fn drive_client() -> DriveClient {
176 DriveClient::new("https://www.googleapis.com", &test_credentials()).unwrap()
177 }
178
179 #[test]
180 fn derives_the_real_slides_host_by_default() {
181 let slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive_client()).unwrap();
185 assert_eq!(slides.base_url(), "https://slides.googleapis.com");
186 }
187
188 #[test]
189 fn honours_the_slides_api_url_override() {
190 let env = MapEnv::new().with(SLIDES_API_URL, "http://127.0.0.1:9/slides");
191 let slides = SlidesClient::from_drive_client_with(&env, &drive_client()).unwrap();
192 assert_eq!(slides.base_url(), "http://127.0.0.1:9/slides");
193 }
194
195 #[test]
196 fn ignores_an_empty_slides_api_url_override() {
197 let env = MapEnv::new().with(SLIDES_API_URL, "");
198 let slides = SlidesClient::from_drive_client_with(&env, &drive_client()).unwrap();
199 assert_eq!(slides.base_url(), "https://slides.googleapis.com");
200 }
201
202 #[test]
203 fn deriving_does_not_disturb_the_drive_client_host() {
204 let drive = drive_client();
205 let _slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive).unwrap();
206 assert_eq!(drive.base_url(), "https://www.googleapis.com");
207 }
208
209 #[test]
214 fn slides_and_sheets_derive_to_different_hosts() {
215 use crate::drive::sheets::client::SheetsClient;
216 let drive = drive_client();
217 let slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive).unwrap();
218 let sheets = SheetsClient::from_drive_client_with(&MapEnv::new(), &drive).unwrap();
219 assert_eq!(slides.base_url(), "https://slides.googleapis.com");
220 assert_eq!(sheets.base_url(), "https://sheets.googleapis.com");
221 }
222
223 #[test]
224 fn new_strips_a_trailing_slash() {
225 let slides =
226 SlidesClient::new("https://slides.googleapis.com/", &test_credentials()).unwrap();
227 assert_eq!(slides.base_url(), "https://slides.googleapis.com");
228 }
229
230 #[tokio::test]
231 async fn a_slides_failure_names_slides_not_drive() {
232 let server = wiremock::MockServer::start().await;
236 wiremock::Mock::given(wiremock::matchers::method("GET"))
237 .and(wiremock::matchers::path("/boom"))
238 .respond_with(
239 wiremock::ResponseTemplate::new(403).set_body_json(serde_json::json!({
240 "error": {"code": 403, "message": "The caller does not have permission",
241 "status": "PERMISSION_DENIED"},
242 })),
243 )
244 .mount(&server)
245 .await;
246 let response = reqwest::get(format!("{}/boom", server.uri()))
247 .await
248 .unwrap();
249
250 let err = GoogleApiClient::response_to_error(API_NAME, response).await;
251 let text = err.to_string();
252 assert!(text.starts_with("Slides API request failed"), "{text}");
253 assert!(!text.contains("Drive API request failed"), "{text}");
254 assert!(!text.contains("Sheets API request failed"), "{text}");
255 }
256
257 #[test]
258 fn debug_never_mentions_the_session_or_a_secret() {
259 let slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive_client()).unwrap();
260 let debug = format!("{slides:?}");
261 assert!(debug.contains("SlidesClient"));
262 assert!(!debug.contains("session"));
263 assert!(!debug.contains("secret-1"));
264 assert!(!debug.contains("refresh-1"));
265 }
266}