use anyhow::Result;
use crate::drive::api_client::GoogleApiClient;
use crate::drive::auth::DriveCredentials;
use crate::drive::client::{DriveClient, SERVICE_TAG};
use crate::utils::env::{EnvSource, SystemEnv};
pub const SLIDES_API_URL: &str = "SLIDES_API_URL";
const API_NAME: &str = "Slides";
pub struct SlidesClient {
inner: GoogleApiClient,
}
impl std::fmt::Debug for SlidesClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SlidesClient")
.field("base_url", &self.inner.base_url())
.finish_non_exhaustive()
}
}
impl SlidesClient {
const DEFAULT_BASE_URL: &'static str = "https://slides.googleapis.com";
pub fn new(base_url: &str, credentials: &DriveCredentials) -> Result<Self> {
Ok(Self {
inner: GoogleApiClient::new(base_url, credentials, SERVICE_TAG, API_NAME)?,
})
}
pub fn from_drive_client(drive: &DriveClient) -> Result<Self> {
Self::from_drive_client_with(&SystemEnv, drive)
}
pub(crate) fn from_drive_client_with(
env: &impl EnvSource,
drive: &DriveClient,
) -> Result<Self> {
let base_url = Self::resolve_base_url(env);
Ok(Self {
inner: drive.transport().derived(&base_url, SERVICE_TAG, API_NAME),
})
}
fn resolve_base_url(env: &impl EnvSource) -> String {
env.var(SLIDES_API_URL)
.filter(|s| !s.is_empty())
.unwrap_or_else(|| Self::DEFAULT_BASE_URL.to_string())
}
#[must_use]
pub fn base_url(&self) -> &str {
self.inner.base_url()
}
pub(in crate::drive) fn transport(&self) -> &GoogleApiClient {
&self.inner
}
}
#[cfg(test)]
pub(crate) mod test_support {
use super::{DriveClient, DriveCredentials, SlidesClient, SLIDES_API_URL};
use crate::drive::auth::DriveGrantedScopes;
use crate::test_support::env::MapEnv;
use crate::utils::secret::Secret;
use wiremock::matchers::{method, path};
use wiremock::{Mock, MockServer, ResponseTemplate};
#[allow(clippy::unwrap_used)]
pub(crate) async fn mock_clients(server: &MockServer) -> (DriveClient, SlidesClient) {
Mock::given(method("POST"))
.and(path("/token"))
.respond_with(
ResponseTemplate::new(200)
.set_body_json(serde_json::json!({"access_token":"test", "expires_in":3600})),
)
.mount(server)
.await;
let credentials = DriveCredentials {
client_id: "client".into(),
client_secret: Secret::new("secret"),
refresh_token: Secret::new("refresh"),
scope: DriveGrantedScopes::READONLY,
};
let mut drive = DriveClient::new(&server.uri(), &credentials).unwrap();
crate::drive::client::test_support::replace_session(
&mut drive,
&credentials,
&format!("{}/token", server.uri()),
);
let slides = SlidesClient::from_drive_client_with(
&MapEnv::new().with(SLIDES_API_URL, &server.uri()),
&drive,
)
.unwrap();
(drive, slides)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::drive::auth::DriveGrantedScopes;
use crate::test_support::env::MapEnv;
use crate::utils::secret::Secret;
fn test_credentials() -> DriveCredentials {
DriveCredentials {
client_id: "client-1".to_string(),
client_secret: Secret::new("secret-1"),
refresh_token: Secret::new("refresh-1"),
scope: DriveGrantedScopes::READONLY,
}
}
fn drive_client() -> DriveClient {
DriveClient::new("https://www.googleapis.com", &test_credentials()).unwrap()
}
#[test]
fn derives_the_real_slides_host_by_default() {
let slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive_client()).unwrap();
assert_eq!(slides.base_url(), "https://slides.googleapis.com");
}
#[test]
fn honours_the_slides_api_url_override() {
let env = MapEnv::new().with(SLIDES_API_URL, "http://127.0.0.1:9/slides");
let slides = SlidesClient::from_drive_client_with(&env, &drive_client()).unwrap();
assert_eq!(slides.base_url(), "http://127.0.0.1:9/slides");
}
#[test]
fn ignores_an_empty_slides_api_url_override() {
let env = MapEnv::new().with(SLIDES_API_URL, "");
let slides = SlidesClient::from_drive_client_with(&env, &drive_client()).unwrap();
assert_eq!(slides.base_url(), "https://slides.googleapis.com");
}
#[test]
fn deriving_does_not_disturb_the_drive_client_host() {
let drive = drive_client();
let _slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive).unwrap();
assert_eq!(drive.base_url(), "https://www.googleapis.com");
}
#[test]
fn slides_and_sheets_derive_to_different_hosts() {
use crate::drive::sheets::client::SheetsClient;
let drive = drive_client();
let slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive).unwrap();
let sheets = SheetsClient::from_drive_client_with(&MapEnv::new(), &drive).unwrap();
assert_eq!(slides.base_url(), "https://slides.googleapis.com");
assert_eq!(sheets.base_url(), "https://sheets.googleapis.com");
}
#[test]
fn new_strips_a_trailing_slash() {
let slides =
SlidesClient::new("https://slides.googleapis.com/", &test_credentials()).unwrap();
assert_eq!(slides.base_url(), "https://slides.googleapis.com");
}
#[tokio::test]
async fn a_slides_failure_names_slides_not_drive() {
let server = wiremock::MockServer::start().await;
wiremock::Mock::given(wiremock::matchers::method("GET"))
.and(wiremock::matchers::path("/boom"))
.respond_with(
wiremock::ResponseTemplate::new(403).set_body_json(serde_json::json!({
"error": {"code": 403, "message": "The caller does not have permission",
"status": "PERMISSION_DENIED"},
})),
)
.mount(&server)
.await;
let response = reqwest::get(format!("{}/boom", server.uri()))
.await
.unwrap();
let err = GoogleApiClient::response_to_error(API_NAME, response).await;
let text = err.to_string();
assert!(text.starts_with("Slides API request failed"), "{text}");
assert!(!text.contains("Drive API request failed"), "{text}");
assert!(!text.contains("Sheets API request failed"), "{text}");
}
#[test]
fn debug_never_mentions_the_session_or_a_secret() {
let slides = SlidesClient::from_drive_client_with(&MapEnv::new(), &drive_client()).unwrap();
let debug = format!("{slides:?}");
assert!(debug.contains("SlidesClient"));
assert!(!debug.contains("session"));
assert!(!debug.contains("secret-1"));
assert!(!debug.contains("refresh-1"));
}
}