#![allow(missing_docs)]
#![cfg(feature = "rest")]
mod common;
use common::LIVE_TEST_PREFIX;
use force::api::RestOperation;
use force::types::{DynamicSObject, SalesforceId};
use serde_json::json;
fn unique_suffix() -> String {
chrono::Utc::now().timestamp_millis().to_string()
}
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_rest_crud_lifecycle() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip(
"live_core_rest_crud_lifecycle",
"core-tier creds absent (set SF_CLIENT_ID/SF_CLIENT_SECRET/SF_TOKEN_URL or SF_ACCESS_TOKEN+SF_INSTANCE_URL)",
);
return Ok(());
};
let client = common::create_client(&cfg).await?;
let last_name = format!("{LIVE_TEST_PREFIX}-crud-{}", unique_suffix());
let created = client
.rest()
.create("Contact", &json!({ "LastName": last_name }))
.await?;
let Some(id) = created.id else {
anyhow::bail!("create did not return an Id");
};
let outcome = crud_lifecycle_body(&client, &id, &last_name).await;
let _ = client.rest().delete("Contact", &id).await;
outcome
}
async fn crud_lifecycle_body(
client: &force::client::ForceClient<common::LiveAuth>,
id: &SalesforceId,
last_name: &str,
) -> anyhow::Result<()> {
let fetched = client.rest().get("Contact", id).await?;
anyhow::ensure!(
fetched.get("LastName").and_then(|v| v.as_str()) == Some(last_name),
"fetched LastName did not match created value",
);
client
.rest()
.update("Contact", id, &json!({ "Title": "force-rs live tester" }))
.await?;
let after = client.rest().get("Contact", id).await?;
anyhow::ensure!(
after.get("Title").and_then(|v| v.as_str()) == Some("force-rs live tester"),
"update did not persist Title",
);
let soql = format!(
"SELECT Id, LastName FROM Contact WHERE Id = '{}'",
id.as_str()
);
let result = client.rest().query::<DynamicSObject>(&soql).await?;
anyhow::ensure!(
result.total_size >= 1,
"query did not find the created record"
);
Ok(())
}
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_rest_upsert() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip(
"live_core_rest_upsert",
"core-tier creds absent (set SF_CLIENT_ID/SF_CLIENT_SECRET/SF_TOKEN_URL or SF_ACCESS_TOKEN+SF_INSTANCE_URL)",
);
return Ok(());
};
let (Some(sobject), Some(ext_field), Some(ext_value)) = (
common::env_string("SF_UPSERT_SOBJECT"),
common::env_string("SF_UPSERT_EXT_FIELD"),
common::env_string("SF_UPSERT_EXT_VALUE"),
) else {
common::skip(
"live_core_rest_upsert",
"upsert externals absent (set SF_UPSERT_SOBJECT/SF_UPSERT_EXT_FIELD/SF_UPSERT_EXT_VALUE)",
);
return Ok(());
};
let client = common::create_client(&cfg).await?;
let name = format!("{LIVE_TEST_PREFIX}-upsert-{}", unique_suffix());
let response = client
.rest()
.upsert(&sobject, &ext_field, &ext_value, &json!({ "Name": name }))
.await?;
anyhow::ensure!(response.is_success(), "upsert reported non-success");
let _ = client.rest().delete(&sobject, &response.id).await;
Ok(())
}
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_rest_query_more_pagination() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip(
"live_core_rest_query_more_pagination",
"core-tier creds absent",
);
return Ok(());
};
let client = common::create_client(&cfg).await?;
let mut page = client
.rest()
.query::<DynamicSObject>("SELECT Id FROM Account LIMIT 2000")
.await?;
let mut pages = 1;
while !page.done {
let Some(next) = page.next_records_url.clone() else {
break;
};
page = client.rest().query_more::<DynamicSObject>(&next).await?;
pages += 1;
if pages >= 3 {
break; }
}
anyhow::ensure!(pages >= 1, "expected at least one page");
Ok(())
}
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_rest_search() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_rest_search", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let sosl = force::api::rest::SearchQueryBuilder::new()
.find(LIVE_TEST_PREFIX)
.in_all_fields()
.returning("Contact", &["Id"])
.build();
let _ = client.rest().search(&sosl).await?;
Ok(())
}
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_rest_describe() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_rest_describe", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let global = client.rest().describe_global().await?;
anyhow::ensure!(
!global.sobjects.is_empty(),
"describe_global returned no sobjects"
);
let account = client.rest().describe("Account").await?;
anyhow::ensure!(
!account.fields.is_empty(),
"describe(Account) returned no fields"
);
Ok(())
}
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_rest_limits() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_rest_limits", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let _ = client.rest().limits().await?;
Ok(())
}
#[cfg(feature = "composite")]
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_composite_batch() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_composite_batch", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let result = client
.composite()
.batch()
.add_request("GET", "limits", None)?
.add_request("GET", "sobjects/Account/describe", None)?
.execute()
.await?;
anyhow::ensure!(
!result.has_errors,
"composite batch returned errors: {result:?}"
);
anyhow::ensure!(result.results.len() == 2, "expected 2 subresponses");
Ok(())
}
#[cfg(feature = "composite_graph")]
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_composite_graph() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_composite_graph", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let accounts = client
.rest()
.query::<DynamicSObject>("SELECT Id FROM Account LIMIT 1")
.await?;
let Some(account_id) = accounts
.records
.first()
.and_then(|r| r.get_field("Id"))
.and_then(|v| v.as_str())
else {
common::skip(
"live_core_composite_graph",
"no Account records in org to build a supported sObject-GET graph node",
);
return Ok(());
};
let graph =
force::api::composite::Graph::new("live-graph").get("Account", account_id, "acctGet")?;
let response = client
.composite()
.graph()
.add_graph(graph)?
.execute()
.await?;
anyhow::ensure!(
response.graphs.iter().all(|g| g.is_successful),
"composite graph reported an unsuccessful graph: {response:?}",
);
Ok(())
}
#[cfg(feature = "bulk")]
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_bulk_query_stream() -> anyhow::Result<()> {
#[derive(serde::Deserialize)]
struct Row {
#[serde(rename = "Id")]
id: String,
}
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_bulk_query_stream", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let mut stream = client
.bulk()
.bulk_query::<Row>("SELECT Id FROM Account LIMIT 5")
.await?;
let mut seen = 0usize;
while let Some(row) = stream.next().await? {
anyhow::ensure!(!row.id.is_empty(), "bulk row had empty Id");
seen += 1;
if seen >= 5 {
break;
}
}
anyhow::ensure!(seen <= 5, "bulk query returned more than the LIMIT");
Ok(())
}
#[cfg(feature = "tooling")]
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_tooling_query_and_execute() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip(
"live_core_tooling_query_and_execute",
"core-tier creds absent",
);
return Ok(());
};
let client = common::create_client(&cfg).await?;
let _ = client
.tooling()
.query::<serde_json::Value>("SELECT Id FROM ApexClass LIMIT 1")
.await?;
let result = client
.tooling()
.execute_anonymous(&format!("System.debug('{LIVE_TEST_PREFIX}');"))
.await?;
anyhow::ensure!(
result.is_success(),
"execute_anonymous was not successful: {result:?}"
);
Ok(())
}
#[cfg(feature = "ui")]
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_ui_object_info_and_defaults() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip(
"live_core_ui_object_info_and_defaults",
"core-tier creds absent",
);
return Ok(());
};
let client = common::create_client(&cfg).await?;
let info = client.ui().object_info("Account").await?;
anyhow::ensure!(
info.api_name == "Account",
"object_info returned wrong api_name"
);
anyhow::ensure!(
info.fields.contains_key("Id"),
"object_info missing Id field"
);
let _ = client.ui().create_defaults("Contact").await?;
Ok(())
}
#[cfg(feature = "graphql")]
#[tokio::test]
#[ignore = "requires a live Salesforce org"]
async fn live_core_graphql_query() -> anyhow::Result<()> {
let Some(cfg) = common::load_core_config() else {
common::skip("live_core_graphql_query", "core-tier creds absent");
return Ok(());
};
let client = common::create_client(&cfg).await?;
let data = client
.graphql()
.query_raw(
r"{ uiapi { query { Account(first: 1) { edges { node { Id } } } } } }",
None,
)
.await?;
let account = &data["uiapi"]["query"]["Account"];
anyhow::ensure!(
account.is_object(),
"expected Account object, got {account:?}"
);
anyhow::ensure!(
account["edges"].is_array(),
"expected edges array, got {account:?}"
);
if let Some(first) = account["edges"].as_array().and_then(|e| e.first()) {
anyhow::ensure!(
first["node"]["Id"].is_string(),
"expected node.Id to be a scalar string, got {first:?}"
);
}
Ok(())
}