use super::common::get_credentials;
use busbar_sf_auth::Credentials;
use busbar_sf_rest::{CompositeRequest, CompositeSubrequest, QueryBuilder, SalesforceRestClient};
use serde::{Deserialize, Serialize};
#[tokio::test]
async fn test_00_setup_test_data() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let ids = super::common::ensure_test_accounts(&client).await;
assert!(
!ids.is_empty(),
"Should have created or found test accounts"
);
println!("Test data setup: {} accounts ready", ids.len());
}
#[tokio::test]
async fn test_rest_composite_api() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let composite_request = CompositeRequest {
all_or_none: true,
collate_subrequests: false,
subrequests: vec![
CompositeSubrequest {
method: "POST".to_string(),
url: format!("/services/data/v{}/sobjects/Account", creds.api_version()),
reference_id: "NewAccount".to_string(),
body: Some(serde_json::json!({
"Name": format!(
"Composite Test Account {}",
chrono::Utc::now().timestamp_millis()
)
})),
},
CompositeSubrequest {
method: "GET".to_string(),
url: format!(
"/services/data/v{}/sobjects/Account/@{{NewAccount.id}}",
creds.api_version()
),
reference_id: "GetNewAccount".to_string(),
body: None,
},
],
};
let response = client
.composite(&composite_request)
.await
.expect("Composite request should succeed");
assert_eq!(response.responses.len(), 2, "Should have 2 sub-responses");
if let Some(first_response) = response.responses.first() {
if let Some(id) = first_response.body.get("id").and_then(|v| v.as_str()) {
let _ = client.delete("Account", id).await;
}
}
}
#[tokio::test]
async fn test_rest_search_sosl() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let search_query = "FIND {test*} IN NAME FIELDS RETURNING Account(Id, Name), Contact(Id, Name)";
let result = client.search::<serde_json::Value>(search_query).await;
assert!(result.is_ok(), "SOSL search should succeed");
}
#[tokio::test]
async fn test_rest_batch_operations() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_accounts = vec![
serde_json::json!({
"Name": format!("Batch Test 1 {}", chrono::Utc::now().timestamp_millis())
}),
serde_json::json!({
"Name": format!("Batch Test 2 {}", chrono::Utc::now().timestamp_millis())
}),
serde_json::json!({
"Name": format!("Batch Test 3 {}", chrono::Utc::now().timestamp_millis())
}),
];
let create_results = client
.create_multiple("Account", &test_accounts, false)
.await
.expect("create_multiple should succeed");
assert_eq!(create_results.len(), 3, "Should create 3 accounts");
let ids: Vec<String> = create_results.iter().filter_map(|r| r.id.clone()).collect();
assert_eq!(ids.len(), 3, "Should have 3 account IDs");
let id_refs: Vec<&str> = ids.iter().map(|s| s.as_str()).collect();
let get_results: Vec<serde_json::Value> = client
.get_multiple("Account", &id_refs, &["Id", "Name"])
.await
.expect("get_multiple should succeed");
assert_eq!(get_results.len(), 3, "Should retrieve 3 accounts");
let updates: Vec<(String, serde_json::Value)> = ids
.iter()
.map(|id| {
(
id.clone(),
serde_json::json!({
"Description": "Updated by batch test"
}),
)
})
.collect();
let update_results = client
.update_multiple("Account", &updates, false)
.await
.expect("update_multiple should succeed");
assert_eq!(update_results.len(), 3, "Should update 3 accounts");
let delete_results = client
.delete_multiple(&id_refs, false)
.await
.expect("delete_multiple should succeed");
assert_eq!(delete_results.len(), 3, "Should delete 3 accounts");
}
#[tokio::test]
async fn test_rest_query_pagination() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.query::<serde_json::Value>("SELECT Id, Name FROM Account LIMIT 5")
.await
.expect("Query should succeed");
assert!(
result.done || result.next_records_url.is_some(),
"Should indicate completion or pagination"
);
let all_records: Vec<serde_json::Value> = client
.query_all("SELECT Id, Name FROM Account LIMIT 100")
.await
.expect("query_all should succeed");
assert!(all_records.len() <= 100, "Should respect LIMIT");
}
#[tokio::test]
async fn test_rest_upsert_operation() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let unique_id = format!("TEST-{}", chrono::Utc::now().timestamp_millis());
let account_data = serde_json::json!({
"Name": format!("Upsert Test {}", unique_id)
});
let upsert_result = client
.upsert(
"Account",
"BusbarIntTestExtId__c",
&unique_id,
&account_data,
)
.await
.expect("First upsert should succeed");
assert!(upsert_result.created, "First upsert should create record");
let account_id = upsert_result.id.clone();
let updated_data = serde_json::json!({
"Name": format!("Upsert Test Updated {}", unique_id)
});
let upsert_result2 = client
.upsert(
"Account",
"BusbarIntTestExtId__c",
&unique_id,
&updated_data,
)
.await
.expect("Second upsert should succeed");
assert!(
!upsert_result2.created,
"Second upsert should update record"
);
assert_eq!(upsert_result2.id, account_id, "Should be same account ID");
let _ = client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_query_builder_injection_prevention() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let malicious_input = "Test' OR '1'='1";
let result: Result<Vec<serde_json::Value>, _> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name"])
.where_eq("Name", malicious_input)
.expect("where_eq should succeed")
.limit(10)
.execute(&client)
.await;
assert!(result.is_ok(), "Query should succeed with escaped input");
let accounts = result.unwrap();
assert_eq!(
accounts.len(),
0,
"Should not find any accounts with malicious input"
);
}
#[tokio::test]
async fn test_query_builder_like_escaping() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let pattern = "test%";
let result: Result<Vec<serde_json::Value>, _> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name"])
.where_like("Name", pattern)
.expect("where_like should succeed")
.limit(10)
.execute(&client)
.await;
assert!(result.is_ok(), "LIKE query should succeed");
}
#[tokio::test]
async fn test_rest_error_invalid_field_query() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.query::<serde_json::Value>("SELECT Id, InvalidFieldName123 FROM Account")
.await;
assert!(result.is_err(), "Query with invalid field should fail");
}
#[tokio::test]
async fn test_rest_error_invalid_sobject_describe() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.describe_sobject("NonExistentObject__c").await;
assert!(
result.is_err(),
"Describing non-existent object should fail"
);
}
#[tokio::test]
async fn test_rest_error_invalid_record_id_get() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result: Result<serde_json::Value, _> = client.get("Account", "bad-id", None).await;
assert!(result.is_err(), "Getting non-existent record should fail");
}
#[tokio::test]
async fn test_rest_error_invalid_sobject_create() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.create(
"Account'; DROP TABLE--",
&serde_json::json!({"Name": "Bad"}),
)
.await;
assert!(result.is_err(), "Create with invalid SObject should fail");
}
#[tokio::test]
async fn test_rest_error_invalid_id_update_delete() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let update_result = client
.update("Account", "bad-id", &serde_json::json!({"Name": "Bad"}))
.await;
assert!(update_result.is_err(), "Update with invalid ID should fail");
let delete_result = client.delete("Account", "bad-id").await;
assert!(delete_result.is_err(), "Delete with invalid ID should fail");
}
#[tokio::test]
async fn test_rest_error_invalid_upsert_field() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.upsert(
"Account",
"AccountNumber; DROP",
"bad",
&serde_json::json!({"Name": "Bad"}),
)
.await;
assert!(result.is_err(), "Upsert with invalid field should fail");
}
#[tokio::test]
async fn test_rest_error_invalid_sosl() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.search::<serde_json::Value>("FIND {test} RETURNING")
.await;
assert!(result.is_err(), "Invalid SOSL should fail");
}
#[tokio::test]
async fn test_rest_error_batch_invalid_input() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let create_result = client
.create_multiple("Account; DROP", &Vec::<serde_json::Value>::new(), false)
.await;
assert!(
create_result.is_err(),
"create_multiple invalid sobject should fail"
);
let update_result = client
.update_multiple(
"Account",
&[("bad-id".to_string(), serde_json::json!({"Name": "Bad"}))],
false,
)
.await;
assert!(
update_result.is_err(),
"update_multiple invalid ID should fail"
);
let delete_result = client.delete_multiple(&["bad-id"], false).await;
assert!(
delete_result.is_err(),
"delete_multiple invalid ID should fail"
);
let get_result: Result<Vec<serde_json::Value>, _> =
client.get_multiple("Account", &["bad-id"], &["Id"]).await;
assert!(get_result.is_err(), "get_multiple invalid ID should fail");
}
#[tokio::test]
async fn test_rest_composite_error_subrequest() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let composite_request = CompositeRequest {
all_or_none: false,
collate_subrequests: false,
subrequests: vec![CompositeSubrequest {
method: "GET".to_string(),
url: format!("/services/data/v{}/sobjects/Nope__c", creds.api_version()),
reference_id: "BadRequest".to_string(),
body: None,
}],
};
let response = client
.composite(&composite_request)
.await
.expect("Composite request should succeed at transport level");
let sub = response
.responses
.first()
.expect("Should have one response");
assert!(sub.http_status_code >= 400, "Subrequest should error");
}
#[tokio::test]
async fn test_credentials_redaction() {
let creds = get_credentials().await;
let debug_output = format!("{:?}", creds);
assert!(
debug_output.contains("[REDACTED]") || !debug_output.contains(creds.access_token()),
"Debug output should not contain actual token"
);
}
#[tokio::test]
async fn test_client_debug_redaction() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let debug_output = format!("{:?}", client);
assert!(
!debug_output.contains(creds.access_token()),
"Client debug output should not contain actual token"
);
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct TestAccount {
#[serde(rename = "Id", skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(rename = "Name")]
name: String,
#[serde(rename = "Industry", skip_serializing_if = "Option::is_none")]
industry: Option<String>,
}
#[tokio::test]
async fn test_type_safe_crud() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let account = TestAccount {
id: None,
name: format!("Type Safe Test {}", chrono::Utc::now().timestamp_millis()),
industry: Some("Technology".to_string()),
};
let id = client
.create("Account", &account)
.await
.expect("Create should succeed");
let retrieved: TestAccount = client
.get("Account", &id, Some(&["Id", "Name", "Industry"]))
.await
.expect("Get should succeed");
assert_eq!(retrieved.name, account.name);
assert_eq!(retrieved.industry, account.industry);
let update_data = serde_json::json!({
"Industry": "Finance"
});
client
.update("Account", &id, &update_data)
.await
.expect("Update should succeed");
client
.delete("Account", &id)
.await
.expect("Delete should succeed");
}
#[tokio::test]
async fn test_type_safe_query() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let accounts: Vec<TestAccount> = client
.query_all("SELECT Id, Name, Industry FROM Account LIMIT 10")
.await
.expect("Query should succeed");
for account in &accounts {
assert!(account.id.is_some(), "Account should have ID");
assert!(!account.name.is_empty(), "Account should have name");
}
}
#[tokio::test]
async fn test_rest_describe_layouts() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.describe_layouts("Account")
.await
.expect("describe_layouts should succeed for Account");
assert!(
result.is_object(),
"Layout response should be a JSON object"
);
assert!(
result.get("layouts").is_some(),
"Response should contain layouts"
);
}
#[tokio::test]
async fn test_rest_describe_layouts_contact() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.describe_layouts("Contact")
.await
.expect("describe_layouts should succeed for Contact");
assert!(
result.is_object(),
"Layout response should be a JSON object"
);
}
#[tokio::test]
async fn test_rest_describe_approval_layouts() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.describe_approval_layouts("Account")
.await
.expect("describe_approval_layouts should succeed");
assert!(
result.is_object(),
"Approval layout response should be a JSON object"
);
assert!(
result.get("approvalLayouts").is_some(),
"Response should contain approvalLayouts"
);
}
#[tokio::test]
async fn test_rest_describe_compact_layouts() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.describe_compact_layouts("Account")
.await
.expect("describe_compact_layouts should succeed");
assert!(
result.is_object(),
"Compact layout response should be a JSON object"
);
assert!(
result.get("compactLayouts").is_some(),
"Response should contain compactLayouts"
);
}
#[tokio::test]
async fn test_rest_describe_global_publisher_layouts() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.describe_global_publisher_layouts()
.await
.expect("describe_global_publisher_layouts should succeed");
assert!(
result.is_object(),
"Global publisher layout response should be a JSON object"
);
assert!(
result.get("layouts").is_some(),
"Response should contain layouts"
);
}
#[tokio::test]
async fn test_rest_describe_named_layout() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let named_result = client
.describe_named_layout("User", "UserAlt")
.await
.expect("describe_named_layout should succeed for User/UserAlt");
assert!(
named_result.is_object(),
"Named layout response should be a JSON object"
);
}
#[tokio::test]
async fn test_rest_describe_layouts_invalid_sobject() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.describe_layouts("InvalidObject__c__c").await;
assert!(
result.is_err(),
"describe_layouts should fail for invalid SObject"
);
}
#[tokio::test]
async fn test_rest_describe_layouts_injection_attempt() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.describe_layouts("Account'; DROP TABLE--").await;
assert!(
result.is_err(),
"describe_layouts should reject SQL injection attempts"
);
}
#[tokio::test]
async fn test_rest_describe_named_layout_special_chars() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client
.describe_named_layout("Account", "Layout With Spaces")
.await;
if let Err(e) = result {
let error_msg = format!("{:?}", e);
assert!(
!error_msg.contains("url") || !error_msg.contains("parse"),
"Should not fail due to URL parsing issues"
);
}
}
#[tokio::test]
async fn test_rest_composite_graph_api() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let request = busbar_sf_rest::CompositeGraphRequest {
graphs: vec![busbar_sf_rest::GraphRequest {
graph_id: "graph1".to_string(),
composite_request: vec![busbar_sf_rest::CompositeSubrequest {
method: "POST".to_string(),
url: format!("/services/data/v{}/sobjects/Account", creds.api_version()),
reference_id: "NewAccount".to_string(),
body: Some(serde_json::json!({
"Name": format!("Graph Test Account {}", chrono::Utc::now().timestamp_millis())
})),
}],
}],
};
let response = client
.composite_graph(&request)
.await
.expect("Composite graph should succeed");
assert!(!response.graphs.is_empty(), "Should have graph responses");
let graph = &response.graphs[0];
assert_eq!(graph.graph_id, "graph1");
if let Some(resp) = graph.graph_response.responses.first() {
if let Some(id) = resp.body.get("id").and_then(|v| v.as_str()) {
let _ = client.delete("Account", id).await;
}
}
}
#[tokio::test]
async fn test_parameterized_search() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
super::common::ensure_test_accounts(&client).await;
let request = busbar_sf_rest::ParameterizedSearchRequest {
q: "BusbarIntTest*".to_string(),
fields: None,
sobjects: Some(vec![busbar_sf_rest::SearchSObjectSpec {
name: "Account".to_string(),
fields: Some(vec!["Id".into(), "Name".into()]),
where_clause: None,
limit: Some(5),
}]),
overall_limit: Some(10),
offset: None,
spell_correction: None,
};
let result = client.parameterized_search(&request).await;
assert!(
result.is_ok(),
"Parameterized search should succeed: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_search_scope_order() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.search_scope_order().await;
assert!(result.is_ok(), "search_scope_order should succeed");
}
#[tokio::test]
async fn test_search_result_layouts() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.search_result_layouts(&["Account", "Contact"]).await;
assert!(
result.is_ok(),
"search_result_layouts should succeed: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_search_suggestions() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
super::common::ensure_test_accounts(&client).await;
let result = client.search_suggestions("Busbar", "Account").await;
assert!(
result.is_ok(),
"search_suggestions should succeed: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_quick_actions_list() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.list_quick_actions("Account").await;
assert!(result.is_ok(), "list_quick_actions should succeed");
}
#[tokio::test]
async fn test_quick_actions_describe() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let actions = client
.list_quick_actions("Account")
.await
.expect("list_quick_actions should succeed");
for action in &actions {
let result = client.describe_quick_action("Account", &action.name).await;
match result {
Ok(describe) => {
assert_eq!(describe.name, action.name);
return; }
Err(e) if e.to_string().contains("NOT_FOUND") => continue,
Err(e) => {
panic!(
"describe_quick_action failed for {} with unexpected error: {}",
action.name, e
);
}
}
}
let global_actions = client
.list_global_quick_actions()
.await
.expect("list_global_quick_actions should succeed");
assert!(
!global_actions.is_empty(),
"Org should have at least one global quick action"
);
let described = client
.describe_global_quick_action(&global_actions[0].name)
.await
.expect("describe_global_quick_action should succeed");
assert_eq!(described.name, global_actions[0].name);
}
#[tokio::test]
async fn test_quick_actions_invoke() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let account_id = client
.create(
"Account",
&serde_json::json!({"Name": format!("QA Test {}", chrono::Utc::now().timestamp_millis())}),
)
.await
.expect("Account creation should succeed");
let actions = client
.list_quick_actions("Account")
.await
.expect("list_quick_actions should succeed");
let mut invoked = false;
for action in &actions {
if action.action_type != "LogACall" && action.action_type != "Update" {
continue;
}
let body = match action.action_type.as_str() {
"LogACall" => serde_json::json!({"record": {"Subject": "Test Call"}}),
"Update" => serde_json::json!({"record": {}}), _ => continue,
};
let result = client
.invoke_quick_action("Account", &action.name, &body)
.await;
match result {
Ok(r) => {
assert!(
r.success || r.context_id.is_some(),
"Quick action result should have success or contextId"
);
invoked = true;
break;
}
Err(e) => {
let msg = e.to_string();
if msg.contains("REQUIRED_FIELD_MISSING")
|| msg.contains("INVALID_FIELD")
|| msg.contains("NOT_FOUND")
{
invoked = true;
break;
}
continue;
}
}
}
let _ = client.delete("Account", &account_id).await;
assert!(
invoked,
"Should have attempted at least one quick action invoke"
);
}
#[tokio::test]
async fn test_quick_actions_error_invalid_sobject() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.list_quick_actions("Bad'; DROP--").await;
assert!(
result.is_err(),
"list_quick_actions with invalid SObject should fail"
);
}
#[tokio::test]
async fn test_list_views_list() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let collection = client
.list_views("Account")
.await
.expect("list_views should succeed for Account");
assert!(
!collection.listviews.is_empty(),
"Account should have list views (deployed by scripts/setup-scratch-org). \
Run: cargo run --bin setup-scratch-org"
);
let first = &collection.listviews[0];
assert!(!first.id.is_empty(), "List view should have an ID");
assert!(!first.label.is_empty(), "List view should have a label");
}
#[tokio::test]
async fn test_list_views_get() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let collection = client
.list_views("Account")
.await
.expect("list_views should succeed");
let lv = collection.listviews.first().expect(
"Account should have list views (deployed by setup-scratch-org). \
Run: cargo run --bin setup-scratch-org",
);
let view = client
.get_list_view("Account", &lv.id)
.await
.expect("get_list_view should succeed");
assert_eq!(view.id, lv.id);
}
#[tokio::test]
async fn test_list_views_describe() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let collection = client
.list_views("Account")
.await
.expect("list_views should succeed");
if let Some(lv) = collection.listviews.first() {
let describe = client
.describe_list_view("Account", &lv.id)
.await
.unwrap_or_else(|e| panic!("describe_list_view failed for ID {}: {e}", lv.id));
assert!(!describe.columns.is_empty(), "Should have columns");
} else {
panic!(
"Account should have list views (deployed by setup-scratch-org). \
Run: cargo run --bin setup-scratch-org"
);
}
}
#[tokio::test]
async fn test_list_views_execute() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let collection = client
.list_views("Account")
.await
.expect("list_views should succeed");
let lv = collection.listviews.first().expect(
"Account should have list views (deployed by setup-scratch-org). \
Run: cargo run --bin setup-scratch-org",
);
let result: busbar_sf_rest::ListViewResult<serde_json::Value> = client
.execute_list_view("Account", &lv.id)
.await
.expect("execute_list_view should succeed");
assert!(result.done, "List view execution should complete");
}
#[tokio::test]
async fn test_list_views_error_invalid_id() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.get_list_view("Account", "bad-id").await;
assert!(result.is_err(), "get_list_view with invalid ID should fail");
}
#[tokio::test]
async fn test_process_rules_list_all() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.list_process_rules().await;
assert!(
result.is_ok(),
"list_process_rules should succeed: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_process_rules_list_for_sobject() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let rules = client
.list_process_rules_for_sobject("Account")
.await
.expect("list_process_rules_for_sobject should succeed");
assert!(!rules.is_empty(), "Should have process rules for Account");
}
#[tokio::test]
async fn test_process_rules_trigger() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!(
"BusbarIntTest_ProcessRule_{}",
chrono::Utc::now().timestamp_millis()
);
let account_id = client
.create("Account", &serde_json::json!({"Name": test_name}))
.await
.expect("Account creation should succeed");
let request = busbar_sf_rest::ProcessRuleRequest {
context_ids: vec![account_id.clone()],
};
let result = client
.trigger_process_rules(&request)
.await
.expect("trigger_process_rules should succeed");
assert!(result.success, "Process rule trigger should succeed");
let _ = client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_process_rules_error_invalid_id() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let request = busbar_sf_rest::ProcessRuleRequest {
context_ids: vec!["bad-id-not-valid".to_string()],
};
let result = client.trigger_process_rules(&request).await;
assert!(
result.is_err(),
"trigger_process_rules with invalid ID should fail"
);
}
#[tokio::test]
async fn test_approvals_list_pending() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.list_pending_approvals().await;
assert!(
result.is_ok(),
"list_pending_approvals should succeed: {:?}",
result.err()
);
}
#[tokio::test]
async fn test_approvals_submit() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!("Approval Test {}", chrono::Utc::now().timestamp_millis());
let account_id = client
.create("Account", &serde_json::json!({"Name": test_name}))
.await
.expect("Account creation should succeed");
let request = busbar_sf_rest::ApprovalRequest {
action_type: busbar_sf_rest::ApprovalActionType::Submit,
context_id: account_id.clone(),
context_actor_id: None,
comments: Some("Integration test submission".to_string()),
next_approver_ids: None,
process_definition_name_or_id: Some("BusbarIntTest_Approval".to_string()),
skip_entry_criteria: Some(true),
};
let result = client
.submit_approval(&request)
.await
.expect("submit_approval should succeed");
assert!(result.success, "Approval submission should succeed");
let _ = client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_approvals_error_invalid_id() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let request = busbar_sf_rest::ApprovalRequest {
action_type: busbar_sf_rest::ApprovalActionType::Submit,
context_id: "bad-id-not-valid".to_string(),
context_actor_id: None,
comments: None,
next_approver_ids: None,
process_definition_name_or_id: None,
skip_entry_criteria: None,
};
let result = client.submit_approval(&request).await;
assert!(
result.is_err(),
"submit_approval with invalid ID should fail"
);
}
#[tokio::test]
async fn test_invocable_actions_list_standard() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let collection = client
.list_standard_actions()
.await
.expect("list_standard_actions should succeed");
assert!(
!collection.actions.is_empty(),
"Should have standard actions available"
);
println!("Standard actions: {} actions", collection.actions.len());
}
#[tokio::test]
async fn test_invocable_actions_describe_standard() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let collection = client
.list_standard_actions()
.await
.expect("list_standard_actions should succeed");
assert!(
!collection.actions.is_empty(),
"Should have standard actions to describe"
);
let action = &collection.actions[0];
let describe = client
.describe_standard_action(&action.name)
.await
.expect("describe_standard_action should succeed");
assert_eq!(describe.name, action.name);
}
#[tokio::test]
async fn test_invocable_actions_invoke_standard() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let describe = client
.describe_standard_action("chatterPost")
.await
.expect("describe_standard_action(chatterPost) should succeed");
assert_eq!(describe.name, "chatterPost");
let required_inputs: Vec<&str> = describe
.inputs
.iter()
.filter(|p| p.required)
.map(|p| p.name.as_str())
.collect();
assert!(
required_inputs.contains(&"text") && required_inputs.contains(&"subjectNameOrId"),
"chatterPost should require 'text' and 'subjectNameOrId', got: {:?}",
required_inputs
);
let users: Vec<serde_json::Value> = client
.query_all("SELECT Id FROM User WHERE IsActive = true LIMIT 1")
.await
.expect("User query should succeed");
let user_id = users[0]
.get("Id")
.and_then(|v| v.as_str())
.expect("Should have User Id");
let request = busbar_sf_rest::InvocableActionRequest {
inputs: vec![serde_json::json!({
"text": "Integration test post",
"subjectNameOrId": user_id
})],
};
let results = client
.invoke_standard_action("chatterPost", &request)
.await
.expect("invoke_standard_action(chatterPost) should succeed");
assert!(!results.is_empty(), "Should have at least one result");
assert!(
results[0].is_success,
"chatterPost should succeed: {:?}",
results[0].errors
);
}
#[tokio::test]
async fn test_invocable_actions_list_custom_types() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let types = client
.list_custom_action_types()
.await
.expect("list_custom_action_types should succeed");
println!("Custom action type categories: {}", types.len());
}
#[tokio::test]
async fn test_invocable_actions_describe_custom() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let types = client
.list_custom_action_types()
.await
.expect("list_custom_action_types should succeed");
for action_type_name in types.keys() {
let collection = client
.list_custom_actions(action_type_name)
.await
.expect("list_custom_actions should succeed");
if let Some(action) = collection.actions.first() {
let describe = client
.describe_custom_action(action_type_name, &action.name)
.await
.expect("describe_custom_action should succeed");
assert_eq!(describe.name, action.name);
return;
}
}
println!("No custom actions found — scratch org has no custom actions deployed");
}
#[tokio::test]
async fn test_invocable_actions_invoke_custom() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let types = client
.list_custom_action_types()
.await
.expect("list_custom_action_types should succeed");
for action_type_name in types.keys() {
let collection = client
.list_custom_actions(action_type_name)
.await
.expect("list_custom_actions should succeed");
for action in &collection.actions {
let describe = client
.describe_custom_action(action_type_name, &action.name)
.await
.expect("describe_custom_action should succeed");
let mut input = serde_json::Map::new();
let mut has_unsatisfiable_input = false;
for param in &describe.inputs {
if !param.required {
continue;
}
match param.param_type.as_str() {
"STRING" | "TEXTAREA" => {
input.insert(param.name.clone(), serde_json::json!("test"));
}
"BOOLEAN" => {
input.insert(param.name.clone(), serde_json::json!(false));
}
"NUMBER" | "INTEGER" | "DOUBLE" | "DECIMAL" | "CURRENCY" => {
input.insert(param.name.clone(), serde_json::json!(0));
}
_ => {
has_unsatisfiable_input = true;
break;
}
}
}
if has_unsatisfiable_input {
continue; }
let request = busbar_sf_rest::InvocableActionRequest {
inputs: vec![serde_json::Value::Object(input)],
};
match client
.invoke_custom_action(action_type_name, &action.name, &request)
.await
{
Ok(results) => {
assert!(!results.is_empty(), "Should have at least one result");
return;
}
Err(e) => {
let msg = e.to_string();
if msg.contains("UNKNOWN_EXCEPTION")
|| msg.contains("INVALID_INPUT")
|| msg.contains("flow interview")
|| msg.contains("400")
{
return;
}
continue;
}
}
}
}
let total_actions: usize = types.len();
if total_actions == 0 {
return;
}
}
#[tokio::test]
async fn test_invocable_actions_error_invalid_name() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.describe_standard_action("Bad'; DROP--").await;
assert!(
result.is_err(),
"describe_standard_action with invalid name should fail"
);
}
#[tokio::test]
async fn test_consent_read() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let accounts: Vec<serde_json::Value> = client
.query_all("SELECT Id FROM Account WHERE Name LIKE 'BusbarIntTest_%' LIMIT 1")
.await
.expect("Account query should succeed");
assert!(
!accounts.is_empty(),
"Should have at least one BusbarIntTest account (created by setup-scratch-org)"
);
let account_id = accounts[0]["Id"].as_str().expect("Account should have Id");
let _response = client
.read_consent("email", &[account_id])
.await
.expect("read_consent should succeed");
}
#[tokio::test]
async fn test_knowledge_settings() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let settings = client
.knowledge_settings()
.await
.expect("knowledge_settings should succeed");
if settings.knowledge_enabled {
assert!(settings.default_language.is_some() || settings.languages.is_empty());
}
}
#[tokio::test]
async fn test_data_category_groups() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.data_category_groups(None).await;
match result {
Ok(groups) => {
assert!(
!groups.category_groups.is_empty(),
"Should have data category groups when endpoint is available"
);
}
Err(e) => {
let err_str = e.to_string();
assert!(
err_str.contains("NOT_FOUND"),
"Expected NOT_FOUND when Knowledge data categories not configured, got: {err_str}"
);
}
}
}
#[tokio::test]
async fn test_user_password_status() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let users: Vec<serde_json::Value> = client
.query_all("SELECT Id FROM User WHERE IsActive = true LIMIT 1")
.await
.expect("User query should succeed");
let user = users.first().expect("Should have at least one active user");
let user_id = user
.get("Id")
.and_then(|v| v.as_str())
.expect("User should have an Id field");
let _status = client
.get_user_password_status(user_id)
.await
.expect("get_user_password_status should succeed");
}
#[tokio::test]
async fn test_rest_tabs() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let tabs = client.tabs().await.expect("tabs should succeed");
assert!(!tabs.is_empty(), "Should have at least one tab");
}
#[tokio::test]
async fn test_rest_theme() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let theme = client.theme().await.expect("theme should succeed");
assert!(theme.is_object(), "Theme should be a JSON object");
}
#[tokio::test]
async fn test_rest_recent_items() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let _items = client
.recent_items()
.await
.expect("recent_items should succeed");
}
#[tokio::test]
async fn test_rest_relevant_items() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let _items = client
.relevant_items()
.await
.expect("relevant_items should succeed");
}
#[tokio::test]
async fn test_rest_app_menu() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let result = client.app_menu("AppSwitcher").await;
assert!(result.is_ok(), "app_menu should succeed for AppSwitcher");
}
#[tokio::test]
async fn test_rest_lightning_usage() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
match client.lightning_usage().await {
Ok(usage) => {
assert!(
usage.is_object() || usage.is_array(),
"Lightning usage should return JSON object or array"
);
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND") || msg.contains("404"),
"Lightning usage error should be NOT_FOUND in unsupported orgs, got: {msg}"
);
}
}
}
#[tokio::test]
async fn test_get_deleted_records() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!("Delete Test {}", chrono::Utc::now().timestamp_millis());
let account_id = client
.create("Account", &serde_json::json!({"Name": test_name}))
.await
.expect("Account creation should succeed");
client
.delete("Account", &account_id)
.await
.expect("Delete should succeed");
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let now = chrono::Utc::now();
let start = (now - chrono::Duration::minutes(5)).to_rfc3339();
let end = now.to_rfc3339();
let result = client
.get_deleted("Account", &start, &end)
.await
.expect("get_deleted should succeed");
assert!(!result.earliest_date_available.is_empty());
assert!(!result.latest_date_covered.is_empty());
}
#[tokio::test]
async fn test_get_updated_records() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!("Update Test {}", chrono::Utc::now().timestamp_millis());
let account_id = client
.create("Account", &serde_json::json!({"Name": test_name}))
.await
.expect("Account creation should succeed");
client
.update(
"Account",
&account_id,
&serde_json::json!({"Description": "Updated"}),
)
.await
.expect("Update should succeed");
tokio::time::sleep(tokio::time::Duration::from_secs(2)).await;
let now = chrono::Utc::now();
let start = (now - chrono::Duration::minutes(5)).to_rfc3339();
let end = now.to_rfc3339();
let result = client
.get_updated("Account", &start, &end)
.await
.expect("get_updated should succeed");
assert!(!result.latest_date_covered.is_empty());
let _ = client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_get_blob_content() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_content = b"Test file content for blob retrieval";
use base64::Engine as _;
let base64_content = base64::engine::general_purpose::STANDARD.encode(test_content);
let content_version_id = client
.create(
"ContentVersion",
&serde_json::json!({
"Title": format!("Test Blob {}", chrono::Utc::now().timestamp_millis()),
"PathOnClient": "test.txt",
"VersionData": base64_content,
}),
)
.await
.expect("ContentVersion creation should succeed");
let query_result: Vec<serde_json::Value> = client
.query_all(&format!(
"SELECT ContentDocumentId FROM ContentVersion WHERE Id = '{}'",
content_version_id
))
.await
.expect("Query should succeed");
if let Some(cv) = query_result.first() {
let content_document_id = cv
.get("ContentDocumentId")
.and_then(|v| v.as_str())
.expect("Should have ContentDocumentId");
let blob_data = client
.get_blob("ContentVersion", &content_version_id, "VersionData")
.await
.expect("get_blob should succeed");
assert!(!blob_data.is_empty(), "Blob data should not be empty");
assert_eq!(
blob_data, test_content,
"Retrieved content should match uploaded content"
);
let _ = client.delete("ContentDocument", content_document_id).await;
}
}
#[tokio::test]
async fn test_get_relationship_child() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!(
"Relationship Test {}",
chrono::Utc::now().timestamp_millis()
);
let account_id = client
.create("Account", &serde_json::json!({"Name": test_name}))
.await
.expect("Account creation should succeed");
let contact_id = client
.create(
"Contact",
&serde_json::json!({"LastName": "Test Contact", "AccountId": account_id}),
)
.await
.expect("Contact creation should succeed");
let contacts_result: busbar_sf_rest::QueryResult<serde_json::Value> = client
.get_relationship("Account", &account_id, "Contacts")
.await
.expect("get_relationship should succeed");
assert!(
contacts_result.total_size > 0,
"Should have at least one contact"
);
let _ = client.delete("Contact", &contact_id).await;
let _ = client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_get_relationship_parent() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!("Parent Test {}", chrono::Utc::now().timestamp_millis());
let account_id = client
.create("Account", &serde_json::json!({"Name": test_name}))
.await
.expect("Account creation should succeed");
let contact_id = client
.create(
"Contact",
&serde_json::json!({"LastName": "Test Contact", "AccountId": account_id}),
)
.await
.expect("Contact creation should succeed");
let account_result: serde_json::Value = client
.get_relationship("Contact", &contact_id, "Account")
.await
.expect("get_relationship should succeed");
assert!(account_result.get("Id").is_some(), "Should have account ID");
let _ = client.delete("Contact", &contact_id).await;
let _ = client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_get_sobject_basic_info() {
let creds = get_credentials().await;
let client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let info = client
.get_sobject_basic_info("Account")
.await
.expect("get_sobject_basic_info should succeed");
assert_eq!(info.object_describe.name, "Account");
assert!(!info.object_describe.label.is_empty());
assert!(info.object_describe.key_prefix.is_some());
assert!(!info.object_describe.urls.is_empty());
assert!(info.object_describe.createable);
assert!(info.object_describe.queryable);
}