diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index a8226a6..6817cc2 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -70,5 +70,9 @@ jobs: - name: Run REST API Integration Tests run: | cargo test --test rest_api_integration_test - + + - name: Destroy Scratch Org + if: always() # This step runs even if 'Run Tests' failed + run: | + sf org delete scratch --target-org SCRATCH --no-prompt \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 36c4293..91190a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,6 +20,12 @@ dependencies = [ "libc", ] +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + [[package]] name = "assert-json-diff" version = "2.0.2" @@ -1217,6 +1223,7 @@ dependencies = [ name = "rustsf" version = "0.0.4" dependencies = [ + "anyhow", "chrono", "compact_str", "html-escape", diff --git a/Cargo.toml b/Cargo.toml index f238da6..d3a6eec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,5 +33,5 @@ chrono = { version = "0.4.45", features = ["serde"] } mockito = "1.7.2" tokio = { version = "1.53.1", features = ["rt-multi-thread", "macros"] } #env_logger = "0.11.3" -#anyhow = "1.0.32" +anyhow = "1.0.32" html-escape = "0.2.15" diff --git a/src/primary_types/mod.rs b/src/primary_types/mod.rs index 3ff0f5d..3575268 100644 --- a/src/primary_types/mod.rs +++ b/src/primary_types/mod.rs @@ -12,6 +12,6 @@ pub trait SObject { fn set_id(&mut self, id: Option<&str>) -> &mut Self; fn set_owner_id(&mut self, id: Option<&str>) -> &mut Self; - + fn get_owner_id(&self) -> Option<&str>; } \ No newline at end of file diff --git a/src/rest_api/sobjects.rs b/src/rest_api/sobjects.rs index d7607aa..20f5339 100644 --- a/src/rest_api/sobjects.rs +++ b/src/rest_api/sobjects.rs @@ -23,6 +23,7 @@ //! # See //! +use std::collections::HashMap; use super::{RestApi, handle_empty_response, handle_json_response}; use crate::Error; use crate::rest_api::responses::create_response::CreateResponse; @@ -32,9 +33,12 @@ use crate::rest_api::responses::describe_sobject_result::DescribeSObjectResult; use crate::rest_api::responses::sobject_info::SObjectInfo; use crate::rest_api::responses::updated_sobjects_response::UpdatedSObjectsResponse; use reqwest::Response; -use serde::Serialize; +use serde::{Serialize}; use serde::de::DeserializeOwned; use std::fmt::Debug; +use serde_json::Value; +use crate::primary_types::SObject; +use crate::rest_api::responses::sobject_attribute::SObjectAttribute; impl RestApi { /// Creates a single new record in a Salesforce with the provided values. @@ -47,8 +51,8 @@ impl RestApi { /// - `params`: An instance of type `T` containing the details of the record to be created. /// /// # Returns - /// - `Result`: - /// - On success, returns a `CreateResponse` containing information about the created record, such as its ID. + /// - `Result`: + /// - On success, returns the updated instance of type 'T' containing the Salesforce record id. /// - On failure, returns an `Error` detailing what went wrong during the request. /// /// # Errors @@ -73,8 +77,8 @@ impl RestApi { /// let mut account = Account::new(); /// account.name = Some("Example Account".to_string()); /// - /// match api.create_sobject("Account", account).await { - /// Ok(response) => println!("Record ID: {}", response.id), + /// match api.create_sobject(account).await { + /// Ok(record) => println!("Record ID: {:?}", record.id()), /// Err(error) => println!("Error creating account: {:?}", error), /// } /// Ok(()) @@ -90,18 +94,28 @@ impl RestApi { /// /// # See /// - pub async fn create_sobject( + pub async fn create_sobject( &mut self, - object_name: &str, - params: T, - ) -> Result { + mut record: T, + ) -> Result { + + // Set the owner_id attribute to the authenticated user's ID if its None + if record.get_owner_id().is_none() { + record.set_owner_id(self.client.get_user_id().as_deref().map(str::to_string).as_deref()); + } + let resource_url = format!( "{}/sobjects/{}", self.client.base_version_path()?, - object_name + record.get_sobject_type() ); - let response = self.client.post(resource_url, params, vec![]).await?; - handle_json_response(response).await + let response = self.client.post(resource_url, record.clone(), vec![]).await?; + let response: CreateResponse = handle_json_response(response).await?; + if response.success { + record.set_id(Some(&response.id)); + } + // Fixme - should we throw an error if the response is not success? + Ok(record) } /// Get Object Metadata Using sObject Basic Information @@ -489,7 +503,10 @@ impl RestApi { /// This function depends on the client's `base_path()` method to obtain the base URL and the /// `get` method to perform the HTTP GET request. The response is then handled by the /// `handle_json_response` utility. - pub async fn fetch_by_id( + /// + /// # See + /// + pub async fn fetch_by_id( // fixme rename into sobject_by_id &mut self, sobject_name: &str, id: &str, @@ -501,7 +518,16 @@ impl RestApi { id ); let response = self.client.get(resource_url, vec![], vec![]).await?; - handle_json_response(response).await + + // Adds the Attribute attribute to the response + let mut attr = SObjectAttribute::new(sobject_name); + attr.set_id(Some(id)); + let json = serde_json::to_value(attr).map_err(Error::from)?; + + let mut hm: HashMap = handle_json_response(response).await?; + hm.insert("attributes".to_string(), json); + let json = serde_json::to_value(hm).map_err(Error::from)?; + serde_json::from_value(json).map_err(Error::from) } /// sObject Get Deleted diff --git a/src/rest_api/test.rs b/src/rest_api/test.rs index b6bd8f5..df1e4d6 100644 --- a/src/rest_api/test.rs +++ b/src/rest_api/test.rs @@ -2,10 +2,12 @@ use mockito::Server; use crate::client::client::{Client}; use crate::errors::Error; use super::*; -use serde::{Deserialize, Serialize}; +use crate as rustsf; use serde_json::json; +use crate::DefSObject; use crate::rest_api::responses::query_response::QueryResponse; +use crate::primary_types::SObject; fn create_test_rest_api(server_url: &str) -> RestApi { let mut client = Client::new(); @@ -50,11 +52,17 @@ async fn test_base_path_not_logged_in() { } } -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "PascalCase")] -struct Account { - id: String, - name: String, +#[DefSObject(sobject_type = "Account", fields="name")] +struct Account { } + +impl Account { + pub fn get_name(&self) -> Option<&str> { + self.name.as_ref().map(|s| s.as_str()) + } + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } } #[tokio::test] @@ -212,9 +220,9 @@ async fn test_find_by_id() { .await; let mut api = create_test_rest_api(&server.url()); - let res = api.fetch_by_id::("Account", "001xx000003DGbX").await.unwrap(); - assert_eq!(res.id, "001xx000003DGbX"); - assert_eq!(res.name, "Acme"); + let account = api.fetch_by_id::("Account", "001xx000003DGbX").await.unwrap(); + assert_eq!(account.id(), Some("001xx000003DGbX")); + assert_eq!(account.get_name(), Some("Acme")); mock.assert_async().await; } @@ -230,11 +238,10 @@ async fn test_create() { .await; let mut api = create_test_rest_api(&server.url()); - let mut params = std::collections::HashMap::new(); - params.insert("Name", "Test Account"); - let res = api.create_sobject("Account", params).await.unwrap(); - assert_eq!(res.id, "001xx000003DGbX"); - assert_eq!(res.success, true); + let mut account = Account::new().set_name("Test Account".to_string()); + + let account = api.create_sobject(account).await.unwrap(); + assert_eq!(account.id(), Some("001xx000003DGbX")); mock.assert_async().await; } diff --git a/tests/common.rs b/tests/common.rs new file mode 100644 index 0000000..75fcde1 --- /dev/null +++ b/tests/common.rs @@ -0,0 +1,25 @@ +use rustsf::{Client, RestApi, DefSObject}; +use std::env; +use anyhow::Result; + +pub async fn get_rest_api_client() -> Result { + let mut client = Client::new(); + client.login_with_sfdx_auth_url(&env::var("SCRATCH_AUTH_URL").expect("SCRATCH_AUTH_URL not set")).await?; + Ok(RestApi::new(client)) +} + +#[DefSObject(sobject_type = "Account", fields="name")] +pub struct Account { + +} + +impl Account { + pub fn set_name(mut self, name: String) -> Self { + self.name = Some(name); + self + } + + pub fn get_name(&self) -> Option<&str> { + self.name.as_deref() + } +} \ No newline at end of file diff --git a/tests/rest_api_integration_test.rs b/tests/rest_api_integration_test.rs new file mode 100644 index 0000000..4f364c2 --- /dev/null +++ b/tests/rest_api_integration_test.rs @@ -0,0 +1,52 @@ +extern crate rustsf; +use anyhow::Result; +use crate::rustsf::primary_types::SObject; + +mod common; + + +/// Check the available API versions +#[tokio::test] +async fn check_versions() -> Result<()> { + let mut client = common::get_rest_api_client().await?; + let versions = client.api_versions().await?; + + assert_ne!(0, versions.len()); + assert_eq!("67.0", versions[versions.len()-1].version); // this forces us to always use the latest version + Ok(()) +} + +/// Create an Account record and Fetch it via Id +#[tokio::test] +async fn create_fetch_delete_sobject() -> Result<()> { + let mut client = common::get_rest_api_client().await?; + + // Create an Account record + let account = client.create_sobject(common::Account::new() + .set_name("Test Account".to_string())).await?; + assert!(account.id().is_some()); + assert_eq!(18, account.id().unwrap().len()); // We got a 18 character Salesforce Id + + // Fetch the Account record + let account = client.fetch_by_id::("Account", &account.id().unwrap()).await?; + assert_eq!("Test Account", account.get_name().unwrap()); + + client.delete_sobject("Account", &account.id().unwrap()).await?; + + match client.fetch_by_id::("Account", &account.id().unwrap()).await { + Ok(_) => panic!("Account record should have been deleted"), + Err(_) => (), // fixme assert the right error message + }; + + Ok(()) +} +/* +/// Fetch the Account record via SOQL + + +/// Create an Account record and Fetch it via Id + + +/// FETCH all Account records via SOQL and delete them + + */ \ No newline at end of file