qcs_api_client_common/configuration/
mod.rs

1//!
2//! By default, all settings are loaded from files located under your home directory in the
3//! `.qcs` folder. Within that folder:
4//!
5//! * `settings.toml` will be used to load general settings (e.g. which URLs to connect to).
6//! * `secrets.toml` will be used to load tokens for authentication.
7//!
8//! Both files should contain profiles. Your settings should contain a `default_profile_name`
9//! that determines which profile is loaded when no other profile is explicitly provided.
10//!
11//! If you don't have either of these files, see [the QCS credentials guide](https://docs.rigetti.com/qcs/guides/qcs-credentials) for details on how to obtain them.
12//!
13//! You can use environment variables to override values in your configuration:
14//!
15//! * [`SETTINGS_PATH_VAR`]: Set the path of the `settings.toml` file to load.
16//! * [`SECRETS_PATH_VAR`]: Set the path of the `secrets.toml` file to load.
17//! * [`SECRETS_READ_ONLY_VAR`]: Flag indicating whether to treat the `secrets.toml` file as read-only. Disabled by default.
18//!     * Access token updates will _not_ be persisted to the secrets file, regardless of file permissions, for any of the following values (case insensitive): "true", "yes", "1".  
19//!     * Access token updates will be persisted to the secrets file if it is writeable for any other value or if unset.
20//! * [`PROFILE_NAME_VAR`]: Override the profile that is loaded by default
21//! * [`QUILC_URL_VAR`]: Override the URL used for requests to the quilc server.
22//! * [`QVM_URL_VAR`]: Override the URL used for requests to the QVM server.
23//! * [`API_URL_VAR`]: Override the URL used for requests to the QCS REST API server.
24//! * [`GRPC_API_URL_VAR`]: Override the URL used for requests to the QCS gRPC API.
25//!
26//! The [`ClientConfiguration`] exposes an API for loading and accessing your
27//! configuration.
28
29#[cfg(feature = "tracing-config")]
30use crate::tracing_configuration::TracingConfiguration;
31use derive_builder::Builder;
32use std::{env, path::PathBuf};
33
34#[cfg(feature = "python")]
35use pyo3::prelude::*;
36
37use self::{
38    secrets::{Credential, Secrets, TokenPayload},
39    settings::Settings,
40};
41
42mod error;
43#[cfg(feature = "python")]
44mod py;
45mod secrets;
46mod settings;
47mod tokens;
48
49pub use error::{LoadError, TokenError};
50#[cfg(feature = "python")]
51pub(crate) use py::*;
52pub use secrets::{DEFAULT_SECRETS_PATH, SECRETS_PATH_VAR, SECRETS_READ_ONLY_VAR};
53pub use settings::{AuthServer, DEFAULT_SETTINGS_PATH, SETTINGS_PATH_VAR};
54pub use tokens::{
55    ClientCredentials, ExternallyManaged, OAuthGrant, OAuthSession, RefreshFunction, RefreshToken,
56    TokenDispatcher, TokenRefresher,
57};
58
59const QCS_AUDIENCE: &str = "api://qcs";
60
61/// Default profile name.
62pub const DEFAULT_PROFILE_NAME: &str = "default";
63/// Setting this environment variable will change which profile is used from the loaded config files
64pub const PROFILE_NAME_VAR: &str = "QCS_PROFILE_NAME";
65fn env_or_default_profile_name() -> String {
66    env::var(PROFILE_NAME_VAR).unwrap_or_else(|_| DEFAULT_PROFILE_NAME.to_string())
67}
68
69/// Default URL to access the QCS API.
70pub const DEFAULT_API_URL: &str = "https://api.qcs.rigetti.com";
71/// Setting this environment variable will override the URL used to connect to the QCS REST API.
72pub const API_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_API_URL";
73fn env_or_default_api_url() -> String {
74    env::var(API_URL_VAR).unwrap_or_else(|_| DEFAULT_API_URL.to_string())
75}
76
77/// Default URL to access the gRPC API.
78pub const DEFAULT_GRPC_API_URL: &str = "https://grpc.qcs.rigetti.com";
79/// Setting this environment variable will override the URL used to connect to the GRPC server.
80pub const GRPC_API_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_GRPC_URL";
81fn env_or_default_grpc_url() -> String {
82    env::var(GRPC_API_URL_VAR).unwrap_or_else(|_| DEFAULT_GRPC_API_URL.to_string())
83}
84
85/// Default URL to access QVM.
86pub const DEFAULT_QVM_URL: &str = "http://127.0.0.1:5000";
87/// Setting this environment variable will override the URL used to access the QVM.
88pub const QVM_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_QVM_URL";
89fn env_or_default_qvm_url() -> String {
90    env::var(QVM_URL_VAR).unwrap_or_else(|_| DEFAULT_QVM_URL.to_string())
91}
92
93/// Default URL to access `quilc`.
94pub const DEFAULT_QUILC_URL: &str = "tcp://127.0.0.1:5555";
95/// Setting this environment variable will override the URL used to access quilc.
96pub const QUILC_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_QUILC_URL";
97fn env_or_default_quilc_url() -> String {
98    env::var(QUILC_URL_VAR).unwrap_or_else(|_| DEFAULT_QUILC_URL.to_string())
99}
100
101/// A configuration suitable for use as a QCS API Client.
102///
103/// This configuration can be constructed in a few ways.
104///
105/// The most common way is to use [`ClientConfiguration::load_default`]. This will load the
106/// configuration associated with your default QCS profile.
107///
108/// When loading your config, any values set by environment variables will override the values in
109/// your configuration files.
110///
111/// You can also build a configuration from scratch using [`ClientConfigurationBuilder`]. Using a
112/// builder bypasses configuration files and environment overrides.
113#[derive(Clone, Debug, Builder)]
114#[cfg_attr(feature = "python", pyclass)]
115pub struct ClientConfiguration {
116    #[builder(private, default = "env_or_default_profile_name()")]
117    profile: String,
118
119    #[doc = "The URL for the QCS REST API."]
120    #[builder(default = "env_or_default_api_url()")]
121    api_url: String,
122
123    #[doc = "The URL for the QCS gRPC API."]
124    #[builder(default = "env_or_default_grpc_url()")]
125    grpc_api_url: String,
126
127    #[doc = "The URL of the quilc server."]
128    #[builder(default = "env_or_default_quilc_url()")]
129    quilc_url: String,
130
131    #[doc = "The URL of the QVM server."]
132    #[builder(default = "env_or_default_qvm_url()")]
133    qvm_url: String,
134
135    /// Provides a single, semi-shared access to user credential tokens.
136    ///
137    /// Note that the tokens are *not* shared when the `ClientConfiguration` is created multiple
138    /// times, e.g. through [`ClientConfiguration::load_default`].
139    #[builder(default, setter(custom))]
140    pub(crate) oauth_session: Option<TokenDispatcher>,
141
142    #[builder(private, default = "ConfigSource::Builder")]
143    source: ConfigSource,
144
145    /// Configuration for tracing of network API calls. If `None`, tracing is disabled.
146    #[cfg(feature = "tracing-config")]
147    #[builder(default)]
148    tracing_configuration: Option<TracingConfiguration>,
149}
150
151impl ClientConfigurationBuilder {
152    /// The [`OAuthSession`] to use to authenticate with the QCS API.
153    ///
154    /// When set to [`None`], the configuration will not manage an OAuth Session, and access to the
155    /// QCS API will be limited to unauthenticated routes.
156    pub fn oauth_session(&mut self, oauth_session: Option<OAuthSession>) -> &mut Self {
157        self.oauth_session = Some(oauth_session.map(Into::into));
158        self
159    }
160}
161
162impl ClientConfiguration {
163    fn new(
164        settings: Settings,
165        mut secrets: Secrets,
166        profile_name: Option<String>,
167    ) -> Result<Self, LoadError> {
168        let Settings {
169            default_profile_name,
170            mut profiles,
171            mut auth_servers,
172            file_path: settings_path,
173        } = settings;
174        let profile_name = profile_name
175            .or_else(|| env::var(PROFILE_NAME_VAR).ok())
176            .unwrap_or(default_profile_name);
177        let profile = profiles
178            .remove(&profile_name)
179            .ok_or(LoadError::ProfileNotFound(profile_name.clone()))?;
180        let auth_server = auth_servers
181            .remove(&profile.auth_server_name)
182            .ok_or_else(|| LoadError::AuthServerNotFound(profile.auth_server_name.clone()))?;
183
184        let secrets_path = secrets.file_path;
185        let credential = secrets.credentials.remove(&profile.credentials_name);
186        let oauth_session = match credential {
187            Some(Credential {
188                token_payload:
189                    Some(TokenPayload {
190                        access_token,
191                        refresh_token,
192                        ..
193                    }),
194            }) => {
195                Some(OAuthSession::new(
196                    OAuthGrant::RefreshToken(RefreshToken::new(
197                        // Some configurations do not populate or may use an
198                        // empty string for the `refresh_token`, but are still
199                        // valid sessions with a valid `access_token`.
200                        //
201                        // Because we found a `token_payload`, we must assume
202                        // the user wants to construct an `OAuthSession`.
203                        // Note that this is no guarantee of session validity.
204                        refresh_token.unwrap_or_default(),
205                    )),
206                    auth_server,
207                    access_token,
208                ))
209            }
210            _ => None,
211        };
212
213        let api_url = env::var(API_URL_VAR).unwrap_or(profile.api_url);
214        let quilc_url = env::var(QUILC_URL_VAR).unwrap_or(profile.applications.pyquil.quilc_url);
215        let qvm_url = env::var(QVM_URL_VAR).unwrap_or(profile.applications.pyquil.qvm_url);
216        let grpc_api_url = env::var(GRPC_API_URL_VAR).unwrap_or(profile.grpc_api_url);
217
218        #[cfg(feature = "tracing-config")]
219        let tracing_configuration =
220            TracingConfiguration::from_env().map_err(LoadError::TracingFilterParseError)?;
221
222        let source = match (settings_path, secrets_path) {
223            (Some(settings_path), Some(secrets_path)) => ConfigSource::File {
224                settings_path,
225                secrets_path,
226            },
227            _ => ConfigSource::Default,
228        };
229
230        let mut builder = Self::builder();
231        builder
232            .oauth_session(oauth_session)
233            .profile(profile_name)
234            .source(source)
235            .api_url(api_url)
236            .quilc_url(quilc_url)
237            .qvm_url(qvm_url)
238            .grpc_api_url(grpc_api_url);
239
240        #[cfg(feature = "tracing-config")]
241        {
242            builder.tracing_configuration(tracing_configuration);
243        };
244
245        Ok({
246            builder
247                .build()
248                .expect("curated build process should not fail")
249        })
250    }
251
252    /// Attempts to load config files
253    ///
254    /// # Errors
255    ///
256    /// See [`LoadError`]
257    pub fn load_default() -> Result<Self, LoadError> {
258        let base_config = Self::load(None)?;
259        Ok(base_config)
260    }
261
262    /// Attempts to load a QCS configuration and creates a [`ClientConfiguration`] using the
263    /// specified profile.
264    ///
265    /// # Errors
266    ///
267    /// See [`LoadError`]
268    pub fn load_profile(profile_name: String) -> Result<Self, LoadError> {
269        Self::load(Some(profile_name))
270    }
271
272    /// Attempts to load a QCS configuration and creates a [`ClientConfiguration`] using the
273    /// specified profile. If no `profile_name` is provided, then a default configuration is
274    /// loaded.
275    ///
276    /// # Errors
277    ///
278    /// See [`LoadError`]
279    fn load(profile_name: Option<String>) -> Result<Self, LoadError> {
280        #[cfg(feature = "tracing-config")]
281        match profile_name.as_ref() {
282            None => tracing::debug!("loading default QCS profile"),
283            Some(profile) => tracing::debug!("loading QCS profile {profile}"),
284        }
285        let settings = Settings::load()?;
286        let secrets = Secrets::load()?;
287
288        Self::new(settings, secrets, profile_name)
289    }
290
291    /// Get a [`ClientConfigurationBuilder`]
292    #[must_use]
293    pub fn builder() -> ClientConfigurationBuilder {
294        ClientConfigurationBuilder::default()
295    }
296
297    /// Get the name of the profile that was loaded, if any.
298    #[must_use]
299    pub fn profile(&self) -> &str {
300        &self.profile
301    }
302
303    /// Get the URL of the QCS REST API.
304    #[must_use]
305    pub fn api_url(&self) -> &str {
306        &self.api_url
307    }
308
309    /// Get the URL of the QCS gRPC API.
310    #[must_use]
311    pub fn grpc_api_url(&self) -> &str {
312        &self.grpc_api_url
313    }
314
315    /// Get the URL of the quilc server.
316    #[must_use]
317    pub fn quilc_url(&self) -> &str {
318        &self.quilc_url
319    }
320
321    /// Get the URL of the QVM server.
322    #[must_use]
323    pub fn qvm_url(&self) -> &str {
324        &self.qvm_url
325    }
326
327    /// Get the [`TracingConfiguration`].
328    #[cfg(feature = "tracing-config")]
329    #[must_use]
330    pub const fn tracing_configuration(&self) -> Option<&TracingConfiguration> {
331        self.tracing_configuration.as_ref()
332    }
333
334    /// Get the source of the configuration.
335    #[must_use]
336    pub const fn source(&self) -> &ConfigSource {
337        &self.source
338    }
339
340    /// Get a copy of the current [`OAuthSession`].
341    ///
342    /// Note: This is a _copy_, the contained tokens will become stale once they expire.
343    ///
344    /// # Errors
345    ///
346    /// See [`TokenError`]
347    pub async fn oauth_session(&self) -> Result<OAuthSession, TokenError> {
348        Ok(self
349            .oauth_session
350            .as_ref()
351            .ok_or(TokenError::NoRefreshToken)?
352            .tokens()
353            .await)
354    }
355
356    /// Gets the `Bearer` access token, refreshing it if it is expired.
357    ///
358    /// # Errors
359    ///
360    /// See [`TokenError`].
361    pub async fn get_bearer_access_token(&self) -> Result<String, TokenError> {
362        let dispatcher = self
363            .oauth_session
364            .as_ref()
365            .ok_or_else(|| TokenError::NoCredentials)?;
366        match dispatcher.validate().await {
367            Ok(tokens) => Ok(tokens),
368            #[allow(unused_variables)]
369            Err(e) => {
370                #[cfg(feature = "tracing-config")]
371                tracing::debug!("Refreshing access token because current one is invalid: {e}");
372                dispatcher
373                    .refresh(self.source(), self.profile())
374                    .await
375                    .map(|e| e.access_token().map(ToString::to_string))?
376            }
377        }
378    }
379
380    /// Refreshes the [`Tokens`] in use and returns the new bearer access token.
381    ///
382    /// # Errors
383    ///
384    /// See [`TokenError`]
385    pub async fn refresh(&self) -> Result<OAuthSession, TokenError> {
386        self.oauth_session
387            .as_ref()
388            .ok_or(TokenError::NoRefreshToken)?
389            .refresh(self.source(), self.profile())
390            .await
391    }
392}
393
394/// Describes how a [`ClientConfiguration`] was initialized.
395#[derive(Clone, Debug)]
396pub enum ConfigSource {
397    /// A [`ClientConfiguration`] derived from a [`ClientConfigurationBuilder`]
398    Builder,
399    /// A [`ClientConfiguration`] derived from at least one file.
400    File {
401        /// The path to the QCS `settings.toml` file used to initialize the [`ClientConfiguration`].
402        settings_path: PathBuf,
403        /// The path to a QCS `secrets.toml` file used to initialize the [`ClientConfiguration`].
404        secrets_path: PathBuf,
405    },
406    /// A [`ClientConfiguration`] derived from default values.
407    Default,
408}
409
410fn expand_path_from_env_or_default(
411    env_var_name: &str,
412    default: &str,
413) -> Result<PathBuf, LoadError> {
414    match env::var(env_var_name) {
415        Ok(path) => {
416            let expanded_path = shellexpand::env(&path).map_err(LoadError::from)?;
417            let path_buf: PathBuf = expanded_path.as_ref().into();
418            if !path_buf.exists() {
419                return Err(LoadError::Path {
420                    path: path_buf,
421                    message: format!("The given path does not exist: {path}"),
422                });
423            }
424            Ok(path_buf)
425        }
426        Err(env::VarError::NotPresent) => {
427            let expanded_path = shellexpand::tilde(default);
428            let path_buf: PathBuf = expanded_path.as_ref().into();
429            if !path_buf.exists() {
430                return Err(LoadError::Path {
431                    path: path_buf,
432                    message: format!(
433                        "Could not find a QCS configuration at the default path: {default}"
434                    ),
435                });
436            }
437            Ok(path_buf)
438        }
439        Err(other_error) => Err(LoadError::EnvVar {
440            variable_name: env_var_name.to_string(),
441            message: other_error.to_string(),
442        }),
443    }
444}
445
446#[cfg(test)]
447mod test {
448
449    use jsonwebtoken::{encode, EncodingKey, Header};
450    use serde::Serialize;
451    use time::{Duration, OffsetDateTime};
452
453    use crate::configuration::{
454        expand_path_from_env_or_default, secrets::Secrets, settings::Settings, AuthServer,
455        ClientConfiguration, OAuthSession, RefreshToken, API_URL_VAR, DEFAULT_QUILC_URL,
456        GRPC_API_URL_VAR, QUILC_URL_VAR, QVM_URL_VAR,
457    };
458
459    use super::{
460        settings::QCS_DEFAULT_AUTH_ISSUER_PRODUCTION, tokens::ClientCredentials, TokenRefresher,
461        QCS_AUDIENCE,
462    };
463
464    #[test]
465    fn expands_env_var() {
466        figment::Jail::expect_with(|jail| {
467            let dir = jail.create_dir("~/blah/blah/")?;
468            jail.create_file(dir.join("file.toml"), "")?;
469            jail.set_env("SOME_PATH", "blah/blah");
470            jail.set_env("SOME_VAR", "~/$SOME_PATH/file.toml");
471            let secrets_path = expand_path_from_env_or_default("SOME_VAR", "default").unwrap();
472            assert_eq!(secrets_path.to_str().unwrap(), "~/blah/blah/file.toml");
473
474            Ok(())
475        });
476    }
477
478    #[test]
479    fn uses_env_var_overrides() {
480        figment::Jail::expect_with(|jail| {
481            let quilc_url = "tcp://quilc:5555";
482            let qvm_url = "http://qvm:5000";
483            let grpc_url = "http://grpc:80";
484            let api_url = "http://api:80";
485
486            jail.set_env(QUILC_URL_VAR, quilc_url);
487            jail.set_env(QVM_URL_VAR, qvm_url);
488            jail.set_env(API_URL_VAR, api_url);
489            jail.set_env(GRPC_API_URL_VAR, grpc_url);
490
491            let config = ClientConfiguration::new(
492                Settings::default(),
493                Secrets::default(),
494                Some("default".to_string()),
495            )
496            .expect("Should be able to build default config.");
497
498            assert_eq!(config.quilc_url, quilc_url);
499            assert_eq!(config.qvm_url, qvm_url);
500            assert_eq!(config.grpc_api_url, grpc_url);
501
502            Ok(())
503        });
504    }
505
506    #[tokio::test]
507    async fn test_default_uses_env_var_overrides() {
508        figment::Jail::expect_with(|jail| {
509            let quilc_url = "quilc_url";
510            let qvm_url = "qvm_url";
511            let grpc_url = "grpc_url";
512            let api_url = "api_url";
513
514            jail.set_env(QUILC_URL_VAR, quilc_url);
515            jail.set_env(QVM_URL_VAR, qvm_url);
516            jail.set_env(GRPC_API_URL_VAR, grpc_url);
517            jail.set_env(API_URL_VAR, api_url);
518
519            let config = ClientConfiguration::load_default().unwrap();
520            assert_eq!(config.quilc_url, quilc_url);
521            assert_eq!(config.qvm_url, qvm_url);
522            assert_eq!(config.grpc_api_url, grpc_url);
523            assert_eq!(config.api_url, api_url);
524
525            Ok(())
526        });
527    }
528
529    #[test]
530    fn test_default_loads_settings_with_partial_profile_applications() {
531        figment::Jail::expect_with(|jail| {
532            let directory = jail.directory();
533            let settings_file_name = "settings.toml";
534            let settings_file_path = directory.join(settings_file_name);
535
536            let quilc_url_env_var = "env-var://quilc.url/after";
537
538            let settings_file_contents = r#"
539default_profile_name = "default"
540
541[profiles]
542[profiles.default]
543api_url = ""
544auth_server_name = "default"
545credentials_name = "default"
546applications = {}
547
548[auth_servers]
549[auth_servers.default]
550client_id = ""
551issuer = ""
552"#;
553            jail.create_file(settings_file_name, settings_file_contents)
554                .expect("should create test settings.toml");
555
556            jail.set_env(
557                "QCS_SETTINGS_FILE_PATH",
558                settings_file_path
559                    .to_str()
560                    .expect("settings file path should be a string"),
561            );
562
563            // before setting env var
564            let config = ClientConfiguration::load_default().unwrap();
565            assert_eq!(config.quilc_url, DEFAULT_QUILC_URL);
566
567            jail.set_env("QCS_SETTINGS_APPLICATIONS_QUILC_URL", quilc_url_env_var);
568
569            // after setting env var
570            let config = ClientConfiguration::load_default().unwrap();
571            assert_eq!(config.quilc_url, quilc_url_env_var);
572
573            Ok(())
574        });
575    }
576
577    #[test]
578    fn test_default_loads_settings_with_partial_profile_applications_pyquil() {
579        figment::Jail::expect_with(|jail| {
580            let directory = jail.directory();
581            let settings_file_name = "settings.toml";
582            let settings_file_path = directory.join(settings_file_name);
583
584            let quilc_url_settings_toml = "settings-toml://quilc.url";
585            let quilc_url_env_var = "env-var://quilc.url/after";
586
587            let settings_file_contents = format!(
588                r#"
589default_profile_name = "default"
590
591[profiles]
592[profiles.default]
593api_url = ""
594auth_server_name = "default"
595credentials_name = "default"
596applications.pyquil.quilc_url = "{quilc_url_settings_toml}"
597
598[auth_servers]
599[auth_servers.default]
600client_id = ""
601issuer = ""
602"#
603            );
604
605            jail.create_file(settings_file_name, &settings_file_contents)
606                .expect("should create test settings.toml");
607
608            jail.set_env(
609                "QCS_SETTINGS_FILE_PATH",
610                settings_file_path
611                    .to_str()
612                    .expect("settings file path should be a string"),
613            );
614
615            // before setting env var
616            let config = ClientConfiguration::load_default().unwrap();
617            assert_eq!(config.quilc_url, quilc_url_settings_toml);
618
619            jail.set_env("QCS_SETTINGS_APPLICATIONS_QUILC_URL", quilc_url_env_var);
620
621            // after setting env var
622            let config = ClientConfiguration::load_default().unwrap();
623            assert_eq!(config.quilc_url, quilc_url_env_var);
624
625            Ok(())
626        });
627    }
628
629    #[tokio::test]
630    async fn test_hydrate_access_token_on_load() {
631        let mut config = ClientConfiguration::builder().build().unwrap();
632        let access_token = "test_access_token";
633        figment::Jail::expect_with(|jail| {
634            let directory = jail.directory();
635            let settings_file_name = "settings.toml";
636            let settings_file_path = directory.join(settings_file_name);
637            let secrets_file_name = "secrets.toml";
638            let secrets_file_path = directory.join(secrets_file_name);
639
640            let settings_file_contents = r#"
641default_profile_name = "default"
642
643[profiles]
644[profiles.default]
645api_url = ""
646auth_server_name = "default"
647credentials_name = "default"
648
649[auth_servers]
650[auth_servers.default]
651client_id = ""
652issuer = ""
653"#;
654
655            let secrets_file_contents = format!(
656                r#"
657[credentials]
658[credentials.default]
659[credentials.default.token_payload]
660access_token = "{access_token}"
661expires_in = 3600
662id_token = "id_token"
663refresh_token = "refresh_token"
664scope = "offline_access openid profile email"
665token_type = "Bearer"
666"#
667            );
668
669            jail.create_file(settings_file_name, settings_file_contents)
670                .expect("should create test settings.toml");
671            jail.create_file(secrets_file_name, &secrets_file_contents)
672                .expect("should create test settings.toml");
673
674            jail.set_env(
675                "QCS_SETTINGS_FILE_PATH",
676                settings_file_path
677                    .to_str()
678                    .expect("settings file path should be a string"),
679            );
680            jail.set_env(
681                "QCS_SECRETS_FILE_PATH",
682                secrets_file_path
683                    .to_str()
684                    .expect("secrets file path should be a string"),
685            );
686
687            config = ClientConfiguration::load_default().unwrap();
688            Ok(())
689        });
690        assert_eq!(
691            config.get_access_token().await.unwrap(),
692            Some(access_token.to_string())
693        );
694    }
695
696    #[derive(Clone, Debug, Serialize)]
697    struct Claims {
698        exp: i64,
699        aud: String,
700        iss: String,
701        sub: String,
702    }
703
704    impl Default for Claims {
705        fn default() -> Self {
706            Self {
707                exp: 0,
708                aud: QCS_AUDIENCE.to_string(),
709                iss: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
710                sub: "qcs@rigetti.com".to_string(),
711            }
712        }
713    }
714
715    impl Claims {
716        fn new_valid() -> Self {
717            Self {
718                exp: (OffsetDateTime::now_utc() + Duration::seconds(100)).unix_timestamp(),
719                ..Self::default()
720            }
721        }
722
723        fn new_expired() -> Self {
724            Self {
725                exp: (OffsetDateTime::now_utc() - Duration::seconds(100)).unix_timestamp(),
726                ..Self::default()
727            }
728        }
729
730        fn to_encoded(&self) -> String {
731            encode(&Header::default(), &self, &EncodingKey::from_secret(&[])).unwrap()
732        }
733    }
734
735    #[test]
736    fn test_valid_token() {
737        let valid_token = Claims::new_valid().to_encoded();
738        let tokens = OAuthSession::from_refresh_token(
739            RefreshToken::new(valid_token.clone()),
740            AuthServer::default(),
741            Some(valid_token.clone()),
742        );
743        assert_eq!(
744            tokens
745                .validate()
746                .expect("Token should not fail validation."),
747            valid_token
748        );
749    }
750
751    #[test]
752    fn test_expired_token() {
753        let invalid_token = Claims::new_expired().to_encoded();
754        let tokens = OAuthSession::from_refresh_token(
755            RefreshToken::new(invalid_token),
756            AuthServer::default(),
757            None,
758        );
759        assert!(tokens.validate().is_err());
760    }
761
762    #[test]
763    fn test_client_credentials_without_access_token() {
764        let tokens = OAuthSession::from_client_credentials(
765            ClientCredentials::new("client_id".to_string(), "client_secret".to_string()),
766            AuthServer::default(),
767            None,
768        );
769        assert!(tokens.validate().is_err());
770    }
771
772    #[tokio::test]
773    async fn test_session_is_present_with_empty_refresh_token_and_valid_access_token() {
774        let access_token = Claims::new_valid().to_encoded();
775        let mut config = ClientConfiguration::builder().build().unwrap();
776        figment::Jail::expect_with(|jail| {
777            let directory = jail.directory();
778            let settings_file_name = "settings.toml";
779            let settings_file_path = directory.join(settings_file_name);
780            let secrets_file_name = "secrets.toml";
781            let secrets_file_path = directory.join(secrets_file_name);
782
783            let settings_file_contents = r#"
784default_profile_name = "default"
785
786[profiles]
787[profiles.default]
788api_url = ""
789auth_server_name = "default"
790credentials_name = "default"
791
792[auth_servers]
793[auth_servers.default]
794client_id = ""
795issuer = ""
796"#;
797
798            // note this has no `refresh_token` property
799            let secrets_file_contents = format!(
800                r#"
801[credentials]
802[credentials.default]
803[credentials.default.token_payload]
804access_token = "{access_token}"
805expires_in = 3600
806id_token = "id_token"
807scope = "offline_access openid profile email"
808token_type = "Bearer"
809"#
810            );
811
812            jail.create_file(settings_file_name, settings_file_contents)
813                .expect("should create test settings.toml");
814            jail.create_file(secrets_file_name, &secrets_file_contents)
815                .expect("should create test secrets.toml");
816
817            jail.set_env(
818                "QCS_SETTINGS_FILE_PATH",
819                settings_file_path
820                    .to_str()
821                    .expect("settings file path should be a string"),
822            );
823            jail.set_env(
824                "QCS_SECRETS_FILE_PATH",
825                secrets_file_path
826                    .to_str()
827                    .expect("secrets file path should be a string"),
828            );
829
830            config = ClientConfiguration::load_default().unwrap();
831            Ok(())
832        });
833
834        assert_eq!(
835            config.get_bearer_access_token().await.unwrap(),
836            access_token.to_string()
837        );
838    }
839}