#![allow(dead_code)]
use async_trait::async_trait;
use force::auth::{AccessToken, Authenticator, TokenResponse};
use force::client::{ForceClient, builder};
use force::config::ClientConfig;
use force::error::Result;
#[cfg(feature = "jwt")]
use force::auth::JwtBearerFlow;
pub const LIVE_TEST_PREFIX: &str = "force-rs-live-test";
#[derive(Debug, Clone)]
pub enum LiveAuth {
#[cfg(feature = "jwt")]
Jwt(JwtBearerFlow),
ClientCredentials(force::auth::ClientCredentials),
#[cfg(feature = "username_password")]
UsernamePassword(force::auth::UsernamePassword),
Token(EnvAuthenticator),
}
#[async_trait]
impl Authenticator for LiveAuth {
async fn authenticate(&self) -> Result<AccessToken> {
match self {
#[cfg(feature = "jwt")]
Self::Jwt(flow) => flow.authenticate().await,
Self::ClientCredentials(flow) => flow.authenticate().await,
#[cfg(feature = "username_password")]
Self::UsernamePassword(flow) => flow.authenticate().await,
Self::Token(env) => env.authenticate().await,
}
}
async fn refresh(&self) -> Result<AccessToken> {
match self {
#[cfg(feature = "jwt")]
Self::Jwt(flow) => flow.refresh().await,
Self::ClientCredentials(flow) => flow.refresh().await,
#[cfg(feature = "username_password")]
Self::UsernamePassword(flow) => flow.refresh().await,
Self::Token(env) => env.refresh().await,
}
}
}
impl std::fmt::Display for LiveAuth {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
#[cfg(feature = "jwt")]
Self::Jwt(_) => write!(f, "JWT Bearer"),
Self::ClientCredentials(_) => write!(f, "Client Credentials"),
#[cfg(feature = "username_password")]
Self::UsernamePassword(_) => write!(f, "Username-Password"),
Self::Token(_) => write!(f, "Access Token"),
}
}
}
#[derive(Debug, Clone)]
pub struct EnvAuthenticator {
access_token: String,
instance_url: String,
}
#[async_trait]
impl Authenticator for EnvAuthenticator {
async fn authenticate(&self) -> Result<AccessToken> {
Ok(AccessToken::from_response(TokenResponse {
access_token: secrecy::SecretString::new(self.access_token.clone().into()),
instance_url: self.instance_url.clone(),
token_type: "Bearer".to_string(),
issued_at: chrono::Utc::now().timestamp_millis().to_string(),
expires_in: Some(7_200),
refresh_token: None,
signature: "live-test".to_string(),
}))
}
async fn refresh(&self) -> Result<AccessToken> {
self.authenticate().await
}
}
pub fn env_string(key: &str) -> Option<String> {
std::env::var(key).ok().and_then(|v| {
let v = v.trim().to_string();
if v.is_empty() { None } else { Some(v) }
})
}
pub fn skip(test_name: &str, reason: &str) {
eprintln!("SKIP {test_name}: {reason}");
}
fn normalize_token_url(raw: &str) -> String {
let trimmed = raw.trim().trim_end_matches('/');
let base = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("https://{trimmed}")
};
if base.ends_with("/services/oauth2/token") {
base
} else {
format!("{base}/services/oauth2/token")
}
}
#[cfg(feature = "jwt")]
fn jwt_endpoints(login_url: &str) -> (String, String) {
let trimmed = login_url.trim().trim_end_matches('/');
let base = if trimmed.contains("://") {
trimmed.to_string()
} else {
format!("https://{trimmed}")
};
let audience = base
.trim_end_matches("/services/oauth2/token")
.trim_end_matches('/')
.to_string();
let token_url = format!("{audience}/services/oauth2/token");
(audience, token_url)
}
#[cfg(feature = "jwt")]
fn resolve_key_path(raw: &str) -> std::path::PathBuf {
let path = std::path::PathBuf::from(raw);
if path.exists() || !path.is_relative() {
return path;
}
let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") else {
return path;
};
let from_manifest = std::path::PathBuf::from(&manifest_dir).join(&path);
if from_manifest.exists() {
return from_manifest;
}
if let Some(root) = std::path::PathBuf::from(&manifest_dir)
.parent()
.and_then(|p| p.parent())
{
let from_root = root.join(&path);
if from_root.exists() {
return from_root;
}
}
path
}
pub struct CoreConfig {
pub auth: LiveAuth,
pub api_version: String,
}
#[cfg(feature = "jwt")]
fn try_jwt_auth() -> Option<LiveAuth> {
let client_id = env_string("SF_JWT_CLIENT_ID")?;
let username = env_string("SF_JWT_USERNAME")?;
let private_key_path = env_string("SF_JWT_PRIVATE_KEY_PATH")?;
let resolved = resolve_key_path(&private_key_path);
let private_key_pem = std::fs::read_to_string(&resolved).ok()?;
let login_url = env_string("SF_JWT_LOGIN_URL")
.unwrap_or_else(|| "https://login.salesforce.com".to_string());
let (audience, token_url) = jwt_endpoints(&login_url);
let flow = JwtBearerFlow::new(
&client_id,
&username,
&private_key_pem,
audience.as_str(),
token_url.as_str(),
)
.ok()?;
Some(LiveAuth::Jwt(flow))
}
fn try_client_credentials_auth() -> Option<LiveAuth> {
let client_id = env_string("SF_CLIENT_ID")?;
let client_secret = env_string("SF_CLIENT_SECRET")?;
let token_url = normalize_token_url(&env_string("SF_TOKEN_URL")?);
Some(LiveAuth::ClientCredentials(
force::auth::ClientCredentials::new(client_id, client_secret, token_url),
))
}
#[cfg(feature = "username_password")]
fn try_username_password_auth() -> Option<LiveAuth> {
let client_id = env_string("SF_UP_CLIENT_ID")?;
let client_secret = env_string("SF_UP_CLIENT_SECRET")?;
let username = env_string("SF_UP_USERNAME")?;
let password = env_string("SF_UP_PASSWORD")?;
let security_token = env_string("SF_UP_SECURITY_TOKEN").unwrap_or_default();
let token_url = normalize_token_url(
&env_string("SF_UP_TOKEN_URL")
.unwrap_or_else(|| "https://login.salesforce.com".to_string()),
);
Some(LiveAuth::UsernamePassword(
force::auth::UsernamePassword::new(
client_id,
client_secret,
username,
password,
security_token,
token_url,
),
))
}
fn try_token_auth() -> Option<LiveAuth> {
let access_token = env_string("SF_ACCESS_TOKEN")?;
let instance_url = env_string("SF_INSTANCE_URL")?;
Some(LiveAuth::Token(EnvAuthenticator {
access_token,
instance_url,
}))
}
#[derive(Debug, serde::Deserialize)]
struct SfCliOrgDisplayEnvelope {
result: SfCliOrgDisplayResult,
}
#[derive(Debug, serde::Deserialize)]
struct SfCliOrgDisplayResult {
#[serde(rename = "accessToken")]
access_token: Option<String>,
#[serde(rename = "instanceUrl")]
instance_url: Option<String>,
}
const fn sf_cli_command_candidates() -> &'static [&'static str] {
#[cfg(windows)]
{
&["sf.cmd", "sf"]
}
#[cfg(not(windows))]
{
&["sf"]
}
}
fn try_sf_cli_auth() -> Option<LiveAuth> {
let target_org = env_string("SF_TARGET_ORG");
for command_name in sf_cli_command_candidates() {
let mut command = std::process::Command::new(command_name);
command.args(["org", "display", "--verbose", "--json"]);
if let Some(ref org) = target_org {
command.args(["--target-org", org]);
}
let Ok(output) = command.output() else {
continue;
};
if !output.status.success() {
continue;
}
let Ok(stdout) = String::from_utf8(output.stdout) else {
continue;
};
let Ok(envelope) = serde_json::from_str::<SfCliOrgDisplayEnvelope>(&stdout) else {
continue;
};
if let (Some(access_token), Some(instance_url)) =
(envelope.result.access_token, envelope.result.instance_url)
{
return Some(LiveAuth::Token(EnvAuthenticator {
access_token,
instance_url,
}));
}
}
None
}
fn load_live_auth() -> Option<LiveAuth> {
#[cfg(feature = "jwt")]
if let Some(auth) = try_jwt_auth() {
return Some(auth);
}
if let Some(auth) = try_client_credentials_auth() {
return Some(auth);
}
#[cfg(feature = "username_password")]
if let Some(auth) = try_username_password_auth() {
return Some(auth);
}
if let Some(auth) = try_token_auth() {
return Some(auth);
}
try_sf_cli_auth()
}
pub fn load_core_config() -> Option<CoreConfig> {
let auth = load_live_auth()?;
let api_version = env_string("SF_API_VERSION").unwrap_or_else(|| "v62.0".to_string());
Some(CoreConfig { auth, api_version })
}
pub async fn create_client(config: &CoreConfig) -> Result<ForceClient<LiveAuth>> {
let client_config = ClientConfig {
api_version: config.api_version.clone(),
..Default::default()
};
builder()
.config(client_config)
.authenticate(config.auth.clone())
.build()
.await
}
#[cfg(feature = "data_cloud")]
pub fn load_data_cloud() -> Option<force::auth::DataCloudConfig> {
let enabled = std::env::var("SF_DATA_CLOUD").is_ok_and(|v| {
matches!(
v.trim().to_ascii_lowercase().as_str(),
"1" | "true" | "yes" | "on"
)
});
if !enabled {
return None;
}
Some(force::auth::DataCloudConfig {
token_exchange_url: env_string("SF_DATA_CLOUD_TOKEN_URL"),
api_version: env_string("SF_DATA_CLOUD_API_VERSION"),
})
}
pub fn apex_rest_path() -> Option<String> {
env_string("SF_APEX_REST_PATH")
}
pub fn consent_params() -> Option<(String, Vec<String>)> {
let action = env_string("SF_CONSENT_ACTION")?;
let ids: Vec<String> = env_string("SF_CONSENT_IDS")?
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
if ids.is_empty() {
return None;
}
Some((action, ids))
}
pub fn models_model() -> Option<String> {
env_string("SF_MODELS_MODEL")
}
pub fn agent_id() -> Option<String> {
env_string("SF_AGENT_ID")
}
pub fn account_engagement_bu() -> Option<String> {
env_string("SF_AE_BUSINESS_UNIT_ID")
}
pub fn cpq_quote_id() -> Option<String> {
env_string("SF_CPQ_QUOTE_ID")
}