use crate::configuration::{secrets::SecretAccessToken, tokens::insecure_validate_token_exp};
#[cfg(feature = "tracing-config")]
use crate::tracing_configuration::TracingConfiguration;
use derive_builder::Builder;
use std::{env, path::PathBuf};
use tokio_util::sync::CancellationToken;
#[cfg(feature = "stubs")]
use pyo3_stub_gen::derive::gen_stub_pyclass;
use self::{
secrets::{Credential, Secrets, TokenPayload},
settings::Settings,
};
pub(crate) mod error;
mod oidc;
mod pkce;
mod secret_string;
pub mod secrets;
pub mod settings;
pub mod tokens;
pub use error::{LoadError, TokenError};
#[cfg(feature = "python")]
pub(crate) mod py;
use settings::AuthServer;
use tokens::{OAuthGrant, OAuthSession, PkceFlow, RefreshToken, TokenDispatcher};
pub const DEFAULT_PROFILE_NAME: &str = "default";
pub const PROFILE_NAME_VAR: &str = "QCS_PROFILE_NAME";
fn env_or_default_profile_name() -> String {
env::var(PROFILE_NAME_VAR).unwrap_or_else(|_| DEFAULT_PROFILE_NAME.to_string())
}
pub const DEFAULT_API_URL: &str = "https://api.qcs.rigetti.com";
pub const API_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_API_URL";
fn env_or_default_api_url() -> String {
env::var(API_URL_VAR).unwrap_or_else(|_| DEFAULT_API_URL.to_string())
}
pub const DEFAULT_GRPC_API_URL: &str = "https://grpc.qcs.rigetti.com";
pub const GRPC_API_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_GRPC_URL";
fn env_or_default_grpc_url() -> String {
env::var(GRPC_API_URL_VAR).unwrap_or_else(|_| DEFAULT_GRPC_API_URL.to_string())
}
pub const DEFAULT_QVM_URL: &str = "http://127.0.0.1:5000";
pub const QVM_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_QVM_URL";
fn env_or_default_qvm_url() -> String {
env::var(QVM_URL_VAR).unwrap_or_else(|_| DEFAULT_QVM_URL.to_string())
}
pub const DEFAULT_QUILC_URL: &str = "tcp://127.0.0.1:5555";
pub const QUILC_URL_VAR: &str = "QCS_SETTINGS_APPLICATIONS_QUILC_URL";
fn env_or_default_quilc_url() -> String {
env::var(QUILC_URL_VAR).unwrap_or_else(|_| DEFAULT_QUILC_URL.to_string())
}
#[derive(Clone, Debug, Builder)]
#[cfg_attr(
not(feature = "stubs"),
builder_struct_attr(optipy::strip_pyo3(only_stubs)),
optipy::strip_pyo3(only_stubs)
)]
#[cfg_attr(
not(feature = "python"),
builder_struct_attr(optipy::strip_pyo3),
optipy::strip_pyo3
)]
#[cfg_attr(
feature = "stubs",
builder_struct_attr(gen_stub_pyclass),
gen_stub_pyclass
)]
#[cfg_attr(
feature = "python",
builder_struct_attr(pyo3::pyclass(module = "qcs_api_client_common.configuration")),
pyo3::pyclass(module = "qcs_api_client_common.configuration")
)]
pub struct ClientConfiguration {
#[builder(private, default = "env_or_default_profile_name()")]
#[builder_field_attr(gen_stub(skip))]
profile: String,
#[doc = "The URL for the QCS REST API."]
#[builder(default = "env_or_default_api_url()")]
#[builder_field_attr(pyo3(get, set))]
#[pyo3(get)]
api_url: String,
#[doc = "The URL for the QCS gRPC API."]
#[builder(default = "env_or_default_grpc_url()")]
#[builder_field_attr(pyo3(get, set))]
#[pyo3(get)]
grpc_api_url: String,
#[doc = "The URL of the quilc server."]
#[builder(default = "env_or_default_quilc_url()")]
#[builder_field_attr(pyo3(get, set))]
#[pyo3(get)]
quilc_url: String,
#[doc = "The URL of the QVM server."]
#[builder(default = "env_or_default_qvm_url()")]
#[builder_field_attr(pyo3(get, set))]
#[pyo3(get)]
qvm_url: String,
#[builder(default, setter(custom))]
#[builder_field_attr(pyo3(get))]
pub(crate) oauth_session: Option<TokenDispatcher>,
#[builder(private, default = "ConfigSource::Builder")]
#[builder_field_attr(gen_stub(skip))]
source: ConfigSource,
#[cfg(feature = "tracing-config")]
#[builder(default)]
#[builder_field_attr(gen_stub(skip))]
tracing_configuration: Option<TracingConfiguration>,
}
impl ClientConfigurationBuilder {
pub fn oauth_session(&mut self, oauth_session: Option<OAuthSession>) -> &mut Self {
self.oauth_session = Some(oauth_session.map(Into::into));
self
}
}
struct ConfigurationContext {
builder: ClientConfigurationBuilder,
auth_server: AuthServer,
credential: Option<Credential>,
}
impl ConfigurationContext {
fn from_profile(profile_name: Option<String>) -> Result<Self, LoadError> {
#[cfg(feature = "tracing-config")]
match profile_name.as_ref() {
None => tracing::debug!("loading default QCS profile"),
Some(profile) => tracing::debug!("loading QCS profile {profile}"),
}
let settings = Settings::load()?;
let secrets = Secrets::load()?;
Self::from_sources(settings, secrets, profile_name)
}
fn from_sources(
settings: Settings,
mut secrets: Secrets,
profile_name: Option<String>,
) -> Result<Self, LoadError> {
let Settings {
default_profile_name,
mut profiles,
mut auth_servers,
file_path: settings_path,
} = settings;
let profile_name = profile_name
.or_else(|| env::var(PROFILE_NAME_VAR).ok())
.unwrap_or(default_profile_name);
let profile = profiles
.remove(&profile_name)
.ok_or(LoadError::ProfileNotFound(profile_name.clone()))?;
let auth_server = auth_servers
.remove(&profile.auth_server_name)
.ok_or_else(|| LoadError::AuthServerNotFound(profile.auth_server_name.clone()))?;
let secrets_path = secrets.file_path;
let credential = secrets.credentials.remove(&profile.credentials_name);
let api_url = env::var(API_URL_VAR)
.unwrap_or(profile.api_url)
.trim_end_matches('/')
.to_string();
let quilc_url = env::var(QUILC_URL_VAR).unwrap_or(profile.applications.pyquil.quilc_url);
let qvm_url = env::var(QVM_URL_VAR).unwrap_or(profile.applications.pyquil.qvm_url);
let grpc_api_url = env::var(GRPC_API_URL_VAR)
.unwrap_or(profile.grpc_api_url)
.trim_end_matches('/')
.to_string();
#[cfg(feature = "tracing-config")]
let tracing_configuration =
TracingConfiguration::from_env().map_err(LoadError::TracingFilterParseError)?;
let source = match (settings_path, secrets_path) {
(Some(settings_path), Some(secrets_path)) => ConfigSource::File {
settings_path,
secrets_path,
},
_ => ConfigSource::Default,
};
let mut builder = ClientConfiguration::builder();
builder
.profile(profile_name)
.source(source)
.api_url(api_url)
.quilc_url(quilc_url)
.qvm_url(qvm_url)
.grpc_api_url(grpc_api_url);
#[cfg(feature = "tracing-config")]
{
builder.tracing_configuration(tracing_configuration);
}
Ok(Self {
builder,
auth_server,
credential,
})
}
}
fn credential_to_oauth_session(
credential: Option<Credential>,
auth_server: AuthServer,
) -> Option<OAuthSession> {
match credential {
Some(Credential {
token_payload:
Some(TokenPayload {
access_token,
refresh_token,
..
}),
}) => Some(OAuthSession::new(
OAuthGrant::RefreshToken(RefreshToken::new(refresh_token.unwrap_or_default())),
auth_server,
access_token,
)),
_ => None,
}
}
impl ClientConfiguration {
#[cfg(test)]
fn new(
settings: Settings,
secrets: Secrets,
profile_name: Option<String>,
) -> Result<Self, LoadError> {
let ConfigurationContext {
mut builder,
auth_server,
credential,
} = ConfigurationContext::from_sources(settings, secrets, profile_name)?;
let oauth_session = credential_to_oauth_session(credential, auth_server);
Ok(builder.oauth_session(oauth_session).build()?)
}
pub fn load_default() -> Result<Self, LoadError> {
let base_config = Self::load(None)?;
Ok(base_config)
}
pub fn load_profile(profile_name: String) -> Result<Self, LoadError> {
Self::load(Some(profile_name))
}
pub async fn load_with_login(
cancel_token: CancellationToken,
profile_name: Option<String>,
) -> Result<Self, LoadError> {
let ConfigurationContext {
mut builder,
auth_server,
credential,
} = ConfigurationContext::from_profile(profile_name)?;
if let Some(Credential {
token_payload:
Some(TokenPayload {
access_token,
refresh_token,
..
}),
}) = credential
{
if let Some(access_token) = access_token {
if insecure_validate_token_exp(&access_token).is_ok() {
let refresh_token = refresh_token.unwrap_or_default();
let oauth_session = OAuthSession::new(
OAuthGrant::RefreshToken(RefreshToken::new(refresh_token)),
auth_server,
Some(access_token),
);
return Ok(builder.oauth_session(Some(oauth_session)).build()?);
}
}
if let Some(refresh_token) = refresh_token {
if !refresh_token.is_empty() {
let mut refresh_token = RefreshToken::new(refresh_token);
if let Ok(access_token) = refresh_token.request_access_token(&auth_server).await
{
let oauth_session = OAuthSession::new(
OAuthGrant::RefreshToken(refresh_token),
auth_server,
Some(access_token),
);
return Ok(builder.oauth_session(Some(oauth_session)).build()?);
}
}
}
}
let pkce_flow = PkceFlow::new_login_flow(cancel_token, &auth_server).await?;
let access_token = pkce_flow.access_token.clone();
let oauth_session =
OAuthSession::from_pkce_flow(pkce_flow, auth_server, Some(access_token));
Ok(builder.oauth_session(Some(oauth_session)).build()?)
}
fn load(profile_name: Option<String>) -> Result<Self, LoadError> {
let ConfigurationContext {
mut builder,
auth_server,
credential,
} = ConfigurationContext::from_profile(profile_name)?;
let oauth_session = credential_to_oauth_session(credential, auth_server);
Ok(builder.oauth_session(oauth_session).build()?)
}
#[must_use]
pub fn builder() -> ClientConfigurationBuilder {
ClientConfigurationBuilder::default()
}
#[must_use]
pub fn profile(&self) -> &str {
&self.profile
}
#[must_use]
pub fn api_url(&self) -> &str {
&self.api_url
}
#[must_use]
pub fn grpc_api_url(&self) -> &str {
&self.grpc_api_url
}
#[must_use]
pub fn quilc_url(&self) -> &str {
&self.quilc_url
}
#[must_use]
pub fn qvm_url(&self) -> &str {
&self.qvm_url
}
#[cfg(feature = "tracing-config")]
#[must_use]
pub const fn tracing_configuration(&self) -> Option<&TracingConfiguration> {
self.tracing_configuration.as_ref()
}
#[must_use]
pub const fn source(&self) -> &ConfigSource {
&self.source
}
pub async fn oauth_session(&self) -> Result<OAuthSession, TokenError> {
Ok(self
.oauth_session
.as_ref()
.ok_or(TokenError::NoRefreshToken)?
.tokens()
.await)
}
pub async fn get_bearer_access_token(&self) -> Result<SecretAccessToken, TokenError> {
let dispatcher = self
.oauth_session
.as_ref()
.ok_or_else(|| TokenError::NoCredentials)?;
match dispatcher.validate().await {
Ok(tokens) => Ok(tokens),
#[allow(unused_variables)]
Err(e) => {
#[cfg(feature = "tracing-config")]
tracing::debug!("Refreshing access token because current one is invalid: {e}");
dispatcher
.refresh(self.source(), self.profile())
.await
.map(|e| e.access_token().cloned())?
}
}
}
pub async fn refresh(&self) -> Result<OAuthSession, TokenError> {
self.oauth_session
.as_ref()
.ok_or(TokenError::NoRefreshToken)?
.refresh(self.source(), self.profile())
.await
}
}
#[derive(Clone, Debug)]
pub enum ConfigSource {
Builder,
File {
settings_path: PathBuf,
secrets_path: PathBuf,
},
Default,
}
fn expand_path_from_env_or_default(
env_var_name: &str,
default: &str,
) -> Result<PathBuf, LoadError> {
match env::var(env_var_name) {
Ok(path) => {
let expanded_path = shellexpand::env(&path).map_err(LoadError::from)?;
let path_buf: PathBuf = expanded_path.as_ref().into();
if !path_buf.exists() {
return Err(LoadError::Path {
path: path_buf,
message: format!("The given path does not exist: {path}"),
});
}
Ok(path_buf)
}
Err(env::VarError::NotPresent) => {
let expanded_path = shellexpand::tilde_with_context(default, || {
env::home_dir().map(|path| path.display().to_string())
});
let path_buf: PathBuf = expanded_path.as_ref().into();
if !path_buf.exists() {
return Err(LoadError::Path {
path: path_buf,
message: format!(
"Could not find a QCS configuration at the default path: {default}"
),
});
}
Ok(path_buf)
}
Err(other_error) => Err(LoadError::EnvVar {
variable_name: env_var_name.to_string(),
message: other_error.to_string(),
}),
}
}
#[cfg(test)]
mod test {
#![allow(clippy::result_large_err, reason = "happens in figment tests")]
use jsonwebtoken::{encode, EncodingKey, Header};
use serde::Serialize;
use time::{Duration, OffsetDateTime};
use tokio_util::sync::CancellationToken;
use crate::configuration::{
expand_path_from_env_or_default,
pkce::tests::PkceTestServerHarness,
secrets::{
SecretAccessToken, SecretRefreshToken, Secrets, SECRETS_PATH_VAR, SECRETS_READ_ONLY_VAR,
},
settings::{Settings, SETTINGS_PATH_VAR},
tokens::TokenRefresher,
AuthServer, ClientConfiguration, OAuthSession, RefreshToken, API_URL_VAR,
DEFAULT_QUILC_URL, GRPC_API_URL_VAR, QUILC_URL_VAR, QVM_URL_VAR,
};
use super::{settings::QCS_DEFAULT_AUTH_ISSUER_PRODUCTION, tokens::ClientCredentials};
#[test]
fn expands_env_var() {
figment::Jail::expect_with(|jail| {
let dir = jail.create_dir("~/blah/blah/")?;
jail.create_file(dir.join("file.toml"), "")?;
jail.set_env("SOME_PATH", "blah/blah");
jail.set_env("SOME_VAR", "~/$SOME_PATH/file.toml");
let secrets_path = expand_path_from_env_or_default("SOME_VAR", "default").unwrap();
assert_eq!(secrets_path.to_str().unwrap(), "~/blah/blah/file.toml");
Ok(())
});
}
#[test]
fn uses_env_var_overrides() {
figment::Jail::expect_with(|jail| {
let quilc_url = "tcp://quilc:5555";
let qvm_url = "http://qvm:5000";
let grpc_url = "http://grpc:80";
let api_url = "http://api:80";
jail.set_env(QUILC_URL_VAR, quilc_url);
jail.set_env(QVM_URL_VAR, qvm_url);
jail.set_env(API_URL_VAR, api_url);
jail.set_env(GRPC_API_URL_VAR, grpc_url);
let config = ClientConfiguration::new(
Settings::default(),
Secrets::default(),
Some("default".to_string()),
)
.expect("Should be able to build default config.");
assert_eq!(config.quilc_url, quilc_url);
assert_eq!(config.qvm_url, qvm_url);
assert_eq!(config.grpc_api_url, grpc_url);
Ok(())
});
}
#[tokio::test]
async fn test_default_uses_env_var_overrides() {
figment::Jail::expect_with(|jail| {
let quilc_url = "quilc_url";
let qvm_url = "qvm_url";
let grpc_url = "grpc_url";
let api_url = "api_url";
jail.set_env(QUILC_URL_VAR, quilc_url);
jail.set_env(QVM_URL_VAR, qvm_url);
jail.set_env(GRPC_API_URL_VAR, grpc_url);
jail.set_env(API_URL_VAR, api_url);
let config = ClientConfiguration::load_default().unwrap();
assert_eq!(config.quilc_url, quilc_url);
assert_eq!(config.qvm_url, qvm_url);
assert_eq!(config.grpc_api_url, grpc_url);
assert_eq!(config.api_url, api_url);
Ok(())
});
}
#[test]
fn test_default_loads_settings_with_partial_profile_applications() {
figment::Jail::expect_with(|jail| {
let directory = jail.directory();
let settings_file_name = "settings.toml";
let settings_file_path = directory.join(settings_file_name);
let quilc_url_env_var = "env-var://quilc.url/after";
let settings_file_contents = r#"
default_profile_name = "default"
[profiles]
[profiles.default]
api_url = ""
auth_server_name = "default"
credentials_name = "default"
applications = {}
[auth_servers]
[auth_servers.default]
client_id = ""
issuer = ""
"#;
jail.create_file(settings_file_name, settings_file_contents)
.expect("should create test settings.toml");
jail.set_env(
"QCS_SETTINGS_FILE_PATH",
settings_file_path
.to_str()
.expect("settings file path should be a string"),
);
let config = ClientConfiguration::load_default().unwrap();
assert_eq!(config.quilc_url, DEFAULT_QUILC_URL);
jail.set_env("QCS_SETTINGS_APPLICATIONS_QUILC_URL", quilc_url_env_var);
let config = ClientConfiguration::load_default().unwrap();
assert_eq!(config.quilc_url, quilc_url_env_var);
Ok(())
});
}
#[test]
fn test_default_loads_settings_with_partial_profile_applications_pyquil() {
figment::Jail::expect_with(|jail| {
let directory = jail.directory();
let settings_file_name = "settings.toml";
let settings_file_path = directory.join(settings_file_name);
let quilc_url_settings_toml = "settings-toml://quilc.url";
let quilc_url_env_var = "env-var://quilc.url/after";
let settings_file_contents = format!(
r#"
default_profile_name = "default"
[profiles]
[profiles.default]
api_url = ""
auth_server_name = "default"
credentials_name = "default"
applications.pyquil.quilc_url = "{quilc_url_settings_toml}"
[auth_servers]
[auth_servers.default]
client_id = ""
issuer = ""
"#
);
jail.create_file(settings_file_name, &settings_file_contents)
.expect("should create test settings.toml");
jail.set_env(
"QCS_SETTINGS_FILE_PATH",
settings_file_path
.to_str()
.expect("settings file path should be a string"),
);
let config = ClientConfiguration::load_default().unwrap();
assert_eq!(config.quilc_url, quilc_url_settings_toml);
jail.set_env("QCS_SETTINGS_APPLICATIONS_QUILC_URL", quilc_url_env_var);
let config = ClientConfiguration::load_default().unwrap();
assert_eq!(config.quilc_url, quilc_url_env_var);
Ok(())
});
}
#[tokio::test]
async fn test_hydrate_access_token_on_load() {
let mut config = ClientConfiguration::builder().build().unwrap();
let access_token = "test_access_token";
figment::Jail::expect_with(|jail| {
let directory = jail.directory();
let settings_file_name = "settings.toml";
let settings_file_path = directory.join(settings_file_name);
let secrets_file_name = "secrets.toml";
let secrets_file_path = directory.join(secrets_file_name);
let settings_file_contents = r#"
default_profile_name = "default"
[profiles]
[profiles.default]
api_url = ""
auth_server_name = "default"
credentials_name = "default"
[auth_servers]
[auth_servers.default]
client_id = ""
issuer = ""
"#;
let secrets_file_contents = format!(
r#"
[credentials]
[credentials.default]
[credentials.default.token_payload]
access_token = "{access_token}"
expires_in = 3600
id_token = "id_token"
refresh_token = "refresh_token"
scope = "offline_access openid profile email"
token_type = "Bearer"
"#
);
jail.create_file(settings_file_name, settings_file_contents)
.expect("should create test settings.toml");
jail.create_file(secrets_file_name, &secrets_file_contents)
.expect("should create test settings.toml");
jail.set_env(
"QCS_SETTINGS_FILE_PATH",
settings_file_path
.to_str()
.expect("settings file path should be a string"),
);
jail.set_env(
"QCS_SECRETS_FILE_PATH",
secrets_file_path
.to_str()
.expect("secrets file path should be a string"),
);
config = ClientConfiguration::load_default().unwrap();
Ok(())
});
assert_eq!(
config.get_access_token().await.unwrap().unwrap(),
SecretAccessToken::from(access_token)
);
}
#[derive(Clone, Debug, Serialize)]
struct Claims {
exp: i64,
iss: String,
sub: String,
}
impl Default for Claims {
fn default() -> Self {
Self {
exp: 0,
iss: QCS_DEFAULT_AUTH_ISSUER_PRODUCTION.to_string(),
sub: "qcs@rigetti.com".to_string(),
}
}
}
impl Claims {
fn new_valid() -> Self {
Self {
exp: (OffsetDateTime::now_utc() + Duration::seconds(100)).unix_timestamp(),
..Self::default()
}
}
fn new_expired() -> Self {
Self {
exp: (OffsetDateTime::now_utc() - Duration::seconds(100)).unix_timestamp(),
..Self::default()
}
}
fn to_encoded(&self) -> String {
encode(&Header::default(), &self, &EncodingKey::from_secret(&[])).unwrap()
}
fn to_access_token(&self) -> SecretAccessToken {
SecretAccessToken::from(self.to_encoded())
}
}
#[test]
fn test_valid_token() {
let valid_token = Claims::new_valid().to_access_token();
let tokens = OAuthSession::from_refresh_token(
RefreshToken::new(SecretRefreshToken::from("unused")),
AuthServer::default(),
Some(valid_token.clone()),
);
assert_eq!(
tokens
.validate()
.expect("Token should not fail validation."),
valid_token
);
}
#[test]
fn test_expired_token() {
let invalid_token = Claims::new_expired().to_access_token();
let tokens = OAuthSession::from_refresh_token(
RefreshToken::new(SecretRefreshToken::from("unused")),
AuthServer::default(),
Some(invalid_token),
);
assert!(tokens.validate().is_err());
}
#[test]
fn test_client_credentials_without_access_token() {
let tokens = OAuthSession::from_client_credentials(
ClientCredentials::new("client_id", "client_secret"),
AuthServer::default(),
None,
);
assert!(tokens.validate().is_err());
}
#[tokio::test]
async fn test_session_is_present_with_empty_refresh_token_and_valid_access_token() {
let access_token = Claims::new_valid().to_encoded();
let mut config = ClientConfiguration::builder().build().unwrap();
figment::Jail::expect_with(|jail| {
let directory = jail.directory();
let settings_file_name = "settings.toml";
let settings_file_path = directory.join(settings_file_name);
let secrets_file_name = "secrets.toml";
let secrets_file_path = directory.join(secrets_file_name);
let settings_file_contents = r#"
default_profile_name = "default"
[profiles]
[profiles.default]
api_url = ""
auth_server_name = "default"
credentials_name = "default"
[auth_servers]
[auth_servers.default]
client_id = ""
issuer = ""
"#;
let secrets_file_contents = format!(
r#"
[credentials]
[credentials.default]
[credentials.default.token_payload]
access_token = "{access_token}"
expires_in = 3600
id_token = "id_token"
scope = "offline_access openid profile email"
token_type = "Bearer"
"#
);
jail.create_file(settings_file_name, settings_file_contents)
.expect("should create test settings.toml");
jail.create_file(secrets_file_name, &secrets_file_contents)
.expect("should create test secrets.toml");
jail.set_env(
"QCS_SETTINGS_FILE_PATH",
settings_file_path
.to_str()
.expect("settings file path should be a string"),
);
jail.set_env(
"QCS_SECRETS_FILE_PATH",
secrets_file_path
.to_str()
.expect("secrets file path should be a string"),
);
config = ClientConfiguration::load_default().unwrap();
Ok(())
});
assert_eq!(
config.get_bearer_access_token().await.unwrap(),
SecretAccessToken::from(access_token)
);
}
#[test]
#[serial_test::serial(oauth2_test_server)]
fn test_pkce_flow_persists_token() {
let runtime = tokio::runtime::Runtime::new().expect("should create runtime");
let PkceTestServerHarness {
server,
client,
discovery: _,
redirect_port: _,
} = runtime.block_on(PkceTestServerHarness::new());
let client_id = client.client_id;
let issuer = server.issuer().to_string();
figment::Jail::expect_with(|jail| {
jail.set_env(SECRETS_READ_ONLY_VAR, "false");
let directory = jail.directory();
let settings_file_name = "settings.toml";
let settings_file_path = directory.join(settings_file_name);
let secrets_file_name = "secrets.toml";
let secrets_file_path = directory.join(secrets_file_name);
let settings_file_contents = format!(
r#"
default_profile_name = "default"
[profiles]
[profiles.default]
api_url = ""
auth_server_name = "default"
credentials_name = "default"
[auth_servers]
[auth_servers.default]
client_id = "{client_id}"
issuer = "{issuer}"
"#
);
let secrets_file_contents = r#"
[credentials]
[credentials.default]
[credentials.default.token_payload]
access_token = ""
"#;
jail.create_file(settings_file_name, &settings_file_contents)
.expect("should create test settings.toml");
jail.set_env(
SETTINGS_PATH_VAR,
settings_file_path
.to_str()
.expect("settings file path should be a string"),
);
jail.create_file(secrets_file_name, secrets_file_contents)
.expect("should create test secrets.toml");
jail.set_env(
SECRETS_PATH_VAR,
secrets_file_path
.to_str()
.expect("secrets file path should be a string"),
);
runtime.block_on(async {
let cancel_token = CancellationToken::new();
let configuration = ClientConfiguration::load_with_login(cancel_token, None)
.await
.expect("should load configuration");
let oauth_session = configuration.refresh().await.expect("should refresh");
let token = oauth_session.validate().expect("token should be valid");
let configuration =
ClientConfiguration::load_default().expect("should load configuration");
let oauth_session = configuration
.oauth_session()
.await
.expect("should get oauth session");
let token_payload = Secrets::load_from_path(&secrets_file_path)
.expect("should load secrets")
.credentials
.remove("default")
.expect("should get default credentials")
.token_payload
.expect("should get token payload");
assert_eq!(
token,
oauth_session.validate().expect("should contain token"),
"session: {oauth_session:?}, token_payload: {token_payload:?}",
);
assert_eq!(
token_payload.access_token,
Some(token),
"session: {oauth_session:?}, token_payload: {token_payload:?}"
);
assert_ne!(
token_payload.refresh_token, None,
"session: {oauth_session:?}, token_payload: {token_payload:?}"
);
});
Ok(())
});
drop(server);
}
}