use super::common::get_credentials;
use busbar_sf_auth::Credentials;
use busbar_sf_bulk::{BulkApiClient, BulkOperation};
use busbar_sf_rest::{QueryBuilder, SalesforceRestClient};
#[tokio::test]
async fn test_bulk_insert_lifecycle() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let csv_data = format!(
"Name,Industry\nBulk Test 1 {},Technology\nBulk Test 2 {},Manufacturing",
chrono::Utc::now().timestamp_millis(),
chrono::Utc::now().timestamp_millis()
);
let result = client
.execute_ingest("Account", BulkOperation::Insert, &csv_data, None)
.await
.expect("Bulk insert should succeed");
assert_eq!(
result.job.number_records_processed, 2,
"Should process 2 records"
);
assert_eq!(
result.job.number_records_failed, 0,
"Should have 0 failures"
);
if let Some(success_results) = result.successful_results {
let lines: Vec<&str> = success_results.lines().collect();
if lines.len() > 1 {
for line in &lines[1..] {
if let Some(id) = line.split(',').next() {
if id.starts_with("001") {
let rest_client =
SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let _ = rest_client.delete("Account", id).await;
}
}
}
}
}
}
#[tokio::test]
async fn test_bulk_query_operation() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name", "Industry"])
.limit(100);
let result = client
.execute_query(query_builder)
.await
.expect("Bulk query should succeed");
assert!(
result.job.number_records_processed >= 0,
"Should process records"
);
if let Some(csv_results) = result.results {
let lines: Vec<&str> = csv_results.lines().collect();
assert!(!lines.is_empty(), "Should have at least header line");
if let Some(header) = lines.first() {
assert!(
header.to_lowercase().contains("id"),
"Header should contain Id"
);
assert!(
header.to_lowercase().contains("name"),
"Header should contain Name"
);
}
}
}
#[tokio::test]
async fn test_bulk_update_operation() {
let creds = get_credentials().await;
let rest_client = SalesforceRestClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create REST client");
let test_name = format!("Bulk Update Test {}", chrono::Utc::now().timestamp_millis());
let account_data = serde_json::json!({
"Name": test_name
});
let account_id = rest_client
.create("Account", &account_data)
.await
.expect("Create should succeed");
let bulk_client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let csv_data = format!("Id,Description\n{},Updated via Bulk API", account_id);
let result = bulk_client
.execute_ingest("Account", BulkOperation::Update, &csv_data, None)
.await
.expect("Bulk update should succeed");
assert_eq!(
result.job.number_records_processed, 1,
"Should process 1 record"
);
assert_eq!(
result.job.number_records_failed, 0,
"Should have 0 failures"
);
let updated: serde_json::Value = rest_client
.get("Account", &account_id, Some(&["Id", "Description"]))
.await
.expect("Get should succeed");
assert_eq!(
updated.get("Description").and_then(|v| v.as_str()),
Some("Updated via Bulk API")
);
let _ = rest_client.delete("Account", &account_id).await;
}
#[tokio::test]
async fn test_bulk_error_invalid_sobject_ingest() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let csv_data = "Name\nBad";
let result = client
.execute_ingest("NoSuchObject__c", BulkOperation::Insert, csv_data, None)
.await;
assert!(result.is_err(), "Ingest with invalid SObject should fail");
}
#[tokio::test]
async fn test_bulk_error_invalid_query_field() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "DefinitelyNotAField__c"])
.limit(10);
let result = client.execute_query(query_builder).await;
assert!(result.is_err(), "Bulk query with invalid field should fail");
}
#[tokio::test]
async fn test_bulk_error_invalid_job_id() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let result = client.get_ingest_job("750000000000000AAA").await;
assert!(result.is_err(), "Invalid job ID should fail");
}
#[tokio::test]
async fn test_parallel_query_results_basic() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name", "Industry"])
.limit(50);
let result = client
.execute_query(query_builder)
.await
.expect("Bulk query should succeed");
match client
.get_parallel_query_results(&result.job.id, None)
.await
{
Ok(batch) => {
if result.job.number_records_processed > 0 {
assert!(
!batch.result_url.is_empty(),
"Should have at least one result URL when records exist"
);
for url in &batch.result_url {
assert!(!url.is_empty(), "Result URL should not be empty");
assert!(
url.contains("/results/") || url.contains("/parallelResults"),
"URL should be a valid results endpoint"
);
}
}
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND"),
"Expected NOT_FOUND error, got: {msg}"
);
}
}
}
#[tokio::test]
async fn test_parallel_query_results_with_max_records() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name"])
.limit(100);
let result = client
.execute_query(query_builder)
.await
.expect("Bulk query should succeed");
match client
.get_parallel_query_results(&result.job.id, Some(3))
.await
{
Ok(batch) => {
if result.job.number_records_processed > 0 {
assert!(
batch.result_url.len() <= 3,
"Should have at most 3 result URLs when maxRecords=3"
);
}
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND"),
"Expected NOT_FOUND error, got: {msg}"
);
}
}
}
#[tokio::test]
async fn test_get_all_query_results_parallel() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name", "Industry"])
.limit(100);
let result = client
.execute_query(query_builder)
.await
.expect("Bulk query should succeed");
match client.get_all_query_results_parallel(&result.job.id).await {
Ok(csv_data) => {
let lines: Vec<&str> = csv_data.lines().collect();
assert!(!lines.is_empty(), "Should have at least header line");
if result.job.number_records_processed > 0 {
assert!(lines.len() > 1, "Should have data rows");
let header = lines[0];
assert!(
header.to_lowercase().contains("id"),
"Header should contain Id"
);
assert!(
header.to_lowercase().contains("name"),
"Header should contain Name"
);
let data_rows = lines.len() - 1; assert!(
data_rows > 0 && data_rows as i64 <= result.job.number_records_processed,
"Should have correct number of data rows"
);
}
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND"),
"Expected NOT_FOUND error, got: {msg}"
);
}
}
}
#[tokio::test]
async fn test_parallel_vs_serial_results_consistency() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name"])
.limit(50);
let result = client
.execute_query(query_builder)
.await
.expect("Bulk query should succeed");
assert!(
result.job.number_records_processed > 0,
"Should have Account records (created by setup-scratch-org). \
Run: cargo run --bin setup-scratch-org"
);
let serial_results = client
.get_all_query_results(&result.job.id)
.await
.expect("Serial results should succeed");
match client.get_all_query_results_parallel(&result.job.id).await {
Ok(parallel_results) => {
let serial_lines: Vec<&str> = serial_results.lines().collect();
let parallel_lines: Vec<&str> = parallel_results.lines().collect();
assert_eq!(
serial_lines.len(),
parallel_lines.len(),
"Both methods should return same number of lines"
);
assert_eq!(serial_lines[0], parallel_lines[0], "Headers should match");
let serial_data_rows = serial_lines.len() - 1;
let parallel_data_rows = parallel_lines.len() - 1;
assert_eq!(
serial_data_rows, parallel_data_rows,
"Both methods should return same number of data rows"
);
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND"),
"Expected NOT_FOUND error, got: {msg}"
);
}
}
}
#[tokio::test]
async fn test_parallel_query_results_empty_job() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> = QueryBuilder::new("Account")
.expect("QueryBuilder creation should succeed")
.select(&["Id", "Name"])
.where_eq("Id", "000000000000000AAA")
.expect("Where clause should succeed") .limit(10);
let result = client
.execute_query(query_builder)
.await
.expect("Bulk query should succeed");
match client
.get_parallel_query_results(&result.job.id, None)
.await
{
Ok(_batch) => {
match client.get_all_query_results_parallel(&result.job.id).await {
Ok(csv_data) => {
let lines: Vec<&str> = csv_data.lines().collect();
assert!(!lines.is_empty(), "Should have at least header line");
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND"),
"Expected NOT_FOUND error, got: {msg}"
);
}
}
}
Err(e) => {
let msg = e.to_string();
assert!(
msg.contains("NOT_FOUND"),
"Expected NOT_FOUND error, got: {msg}"
);
}
}
}
#[cfg(feature = "dependencies")]
#[tokio::test]
async fn test_bulk_query_metadata_component_dependencies() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> =
QueryBuilder::new("MetadataComponentDependency")
.expect("QueryBuilder creation should succeed")
.select(&[
"MetadataComponentId",
"MetadataComponentName",
"MetadataComponentType",
"RefMetadataComponentId",
"RefMetadataComponentName",
"RefMetadataComponentType",
])
.limit(1000);
let result = client.execute_query(query_builder).await;
assert!(
result.is_err(),
"MetadataComponentDependency should not be queryable via standard Bulk API"
);
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("not supported") || err_msg.contains("API_ERROR"),
"Error should indicate unsupported sObject type, got: {err_msg}"
);
}
#[cfg(feature = "dependencies")]
#[tokio::test]
async fn test_bulk_query_metadata_component_dependencies_with_filter() {
let creds = get_credentials().await;
let client = BulkApiClient::new(creds.instance_url(), creds.access_token())
.expect("Failed to create Bulk client");
let query_builder: QueryBuilder<serde_json::Value> =
QueryBuilder::new("MetadataComponentDependency")
.expect("QueryBuilder creation should succeed")
.select(&[
"MetadataComponentId",
"MetadataComponentName",
"MetadataComponentType",
])
.where_eq("MetadataComponentType", "ApexClass")
.expect("where_eq should succeed")
.limit(100);
let result = client.execute_query(query_builder).await;
assert!(
result.is_err(),
"Filtered MetadataComponentDependency should not be queryable via standard Bulk API"
);
}
#[cfg(feature = "dependencies")]
#[tokio::test]
async fn test_bulk_metadata_component_dependency_type_deserialization() {
let sample = busbar_sf_client::MetadataComponentDependency {
metadata_component_id: Some("0Adxx0000000001".to_string()),
metadata_component_name: Some("MyClass".to_string()),
metadata_component_namespace: None,
metadata_component_type: Some("ApexClass".to_string()),
ref_metadata_component_id: Some("0Adxx0000000002".to_string()),
ref_metadata_component_name: Some("OtherClass".to_string()),
ref_metadata_component_namespace: None,
ref_metadata_component_type: Some("ApexClass".to_string()),
};
assert_eq!(
sample.metadata_component_type,
Some("ApexClass".to_string())
);
assert_eq!(
sample.ref_metadata_component_name,
Some("OtherClass".to_string())
);
}