/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.salesforce;
import java.io.InputStream;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import com.thoughtworks.xstream.annotations.XStreamAlias;
import org.apache.camel.CamelExecutionException;
import org.apache.camel.Processor;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.component.salesforce.api.NoSuchSObjectException;
import org.apache.camel.component.salesforce.api.SalesforceException;
import org.apache.camel.component.salesforce.api.SalesforceMultipleChoicesException;
import org.apache.camel.component.salesforce.api.dto.AbstractDTOBase;
import org.apache.camel.component.salesforce.api.dto.CreateSObjectResult;
import org.apache.camel.component.salesforce.api.dto.GlobalObjects;
import org.apache.camel.component.salesforce.api.dto.RestResources;
import org.apache.camel.component.salesforce.api.dto.SObjectBasicInfo;
import org.apache.camel.component.salesforce.api.dto.SObjectDescription;
import org.apache.camel.component.salesforce.api.dto.SearchResult;
import org.apache.camel.component.salesforce.api.dto.SearchResults;
import org.apache.camel.component.salesforce.api.dto.Version;
import org.apache.camel.component.salesforce.api.dto.Versions;
import org.apache.camel.component.salesforce.dto.generated.Document;
import org.apache.camel.component.salesforce.dto.generated.Line_Item__c;
import org.apache.camel.component.salesforce.dto.generated.Merchandise__c;
import org.apache.camel.component.salesforce.dto.generated.QueryRecordsLine_Item__c;
import org.apache.camel.component.salesforce.dto.generated.Task;
import org.apache.camel.util.jsse.SSLContextParameters;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.api.ContentResponse;
import org.eclipse.jetty.client.api.Request;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.util.ssl.SslContextFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.experimental.categories.Category;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameter;
import org.junit.runners.Parameterized.Parameters;
@Category(Standalone.class)
@RunWith(Parameterized.class)
public class RestApiIntegrationTest extends AbstractSalesforceTestBase {
/**
* Request DTO for Salesforce APEX REST calls. See
* https://www.salesforce.com/us/developer/docs/apexcode/Content/apex_rest_methods.htm.
*/
@XStreamAlias("request")
public static class MerchandiseRequest extends AbstractDTOBase {
private Merchandise__c merchandise;
public MerchandiseRequest(final Merchandise__c merchandise) {
this.merchandise = merchandise;
}
public Merchandise__c getMerchandise() {
return merchandise;
}
public void setMerchandise(final Merchandise__c merchandise) {
this.merchandise = merchandise;
}
}
/**
* Response DTO for Salesforce APEX REST calls. See
* https://www.salesforce.com/us/developer/docs/apexcode/Content/apex_rest_methods.htm.
*/
@XStreamAlias("response")
public static class MerchandiseResponse extends Merchandise__c {
// XML response contains a type string with the SObject type name
private String type;
public String getType() {
return type;
}
public void setType(final String type) {
this.type = type;
}
}
private static final AtomicInteger NEW_LINE_ITEM_ID = new AtomicInteger(100);
private static final String TEST_DOCUMENT_ID = "Test Document";
private static final AtomicInteger TEST_LINE_ITEM_ID = new AtomicInteger(1);
@Parameter
public String format;
private String testId;
@After
public void removeData() {
template.request("salesforce:deleteSObject?sObjectName=Merchandise__c&sObjectId=" + testId, (Processor) (e) -> {
// NOOP
});
template.request("direct:deleteLineItems", (Processor) (e) -> {
// NOOP
});
}
@Before
public void setupData() {
final Merchandise__c merchandise = new Merchandise__c();
merchandise.setName("Test Merchandise");
merchandise.setPrice__c(10.0);
merchandise.setTotal_Inventory__c(100.0);
final CreateSObjectResult result = template().requestBody("salesforce:createSObject", merchandise,
CreateSObjectResult.class);
testId = result.getId();
}
@Test
public void testApexCall() throws Exception {
// request merchandise with id in URI template
Merchandise__c merchandise = template().requestBodyAndHeader("direct:apexCallGet", null, "id", testId,
Merchandise__c.class);
assertNotNull(merchandise);
// request merchandise with id as query param
merchandise = template().requestBodyAndHeader("direct:apexCallGetWithId", null,
SalesforceEndpointConfig.APEX_QUERY_PARAM_PREFIX + "id", testId, Merchandise__c.class);
assertNotNull(merchandise);
// patch merchandise
// clear fields that won't be modified
merchandise.clearBaseFields();
merchandise.setId(testId);
merchandise.setPrice__c(null);
merchandise.setTotal_Inventory__c(null);
merchandise = template().requestBody("direct:apexCallPatch", new MerchandiseRequest(merchandise),
Merchandise__c.class);
assertNotNull(merchandise);
}
@Test
public void testCreateUpdateDelete() throws Exception {
final Merchandise__c merchandise = new Merchandise__c();
merchandise.setName("Wee Wee Wee Plane");
merchandise.setDescription__c("Microlite plane");
merchandise.setPrice__c(2000.0);
merchandise.setTotal_Inventory__c(50.0);
final CreateSObjectResult result = template().requestBody("direct:createSObject", merchandise,
CreateSObjectResult.class);
assertNotNull(result);
assertTrue("Create success", result.getSuccess());
// test JSON update
// make the plane cheaper
merchandise.setPrice__c(1500.0);
// change inventory to half
merchandise.setTotal_Inventory__c(25.0);
// also need to set the Id
merchandise.setId(result.getId());
assertNull(template().requestBodyAndHeader("direct:updateSObject", merchandise,
SalesforceEndpointConfig.SOBJECT_ID, result.getId()));
// delete the newly created SObject
assertNull(template().requestBody("direct:deleteSObject", result.getId()));
}
@Test
public void testCreateUpdateDeleteTasks() throws Exception {
final Task taken = new Task();
taken.setDescription("Task1");
taken.setActivityDate(ZonedDateTime.of(1700, 1, 2, 3, 4, 5, 6, ZoneId.systemDefault()));
final CreateSObjectResult result = template().requestBody("direct:createSObject", taken,
CreateSObjectResult.class);
assertNotNull(result);
assertTrue("Create success", result.getSuccess());
// test JSON update
// make the plane cheaper
taken.setId(result.getId());
taken.setActivityDate(ZonedDateTime.of(1991, 1, 2, 3, 4, 5, 6, ZoneId.systemDefault()));
assertNull(template().requestBodyAndHeader("direct:updateSObject", taken, SalesforceEndpointConfig.SOBJECT_ID,
result.getId()));
// delete the newly created SObject
assertNull(template().requestBody("direct:deleteSObjectTaken", result.getId()));
}
@Test
public void testCreateUpdateDeleteWithId() throws Exception {
Line_Item__c lineItem = new Line_Item__c();
final String lineItemId = String.valueOf(TEST_LINE_ITEM_ID.incrementAndGet());
lineItem.setName(lineItemId);
CreateSObjectResult result = template().requestBody("direct:createLineItem", lineItem,
CreateSObjectResult.class);
assertNotNull(result);
assertTrue(result.getSuccess());
// get line item with Name 1
lineItem = template().requestBody("direct:getSObjectWithId", lineItemId, Line_Item__c.class);
assertNotNull(lineItem);
// test insert with id
// set the unit price and sold
lineItem.setUnit_Price__c(1000.0);
lineItem.setUnits_Sold__c(50.0);
// update line item with Name NEW_LINE_ITEM_ID
final String newLineItemId = String.valueOf(NEW_LINE_ITEM_ID.incrementAndGet());
lineItem.setName(newLineItemId);
result = template().requestBodyAndHeader("direct:upsertSObject", lineItem,
SalesforceEndpointConfig.SOBJECT_EXT_ID_VALUE, newLineItemId, CreateSObjectResult.class);
assertNotNull(result);
assertTrue(result.getSuccess());
// clear read only parent type fields
lineItem.setInvoice_Statement__c(null);
lineItem.setMerchandise__c(null);
// change the units sold
lineItem.setUnits_Sold__c(25.0);
// update line item with Name NEW_LINE_ITEM_ID
result = template().requestBodyAndHeader("direct:upsertSObject", lineItem,
SalesforceEndpointConfig.SOBJECT_EXT_ID_VALUE, newLineItemId, CreateSObjectResult.class);
assertNull(result);
// delete the SObject with Name NEW_LINE_ITEM_ID
assertNull(template().requestBody("direct:deleteSObjectWithId", newLineItemId));
}
@Test
public void testGetBasicInfo() throws Exception {
final SObjectBasicInfo objectBasicInfo = template().requestBody("direct:getBasicInfo", null,
SObjectBasicInfo.class);
assertNotNull(objectBasicInfo);
// set test Id for testGetSObject
assertFalse("RecentItems is empty", objectBasicInfo.getRecentItems().isEmpty());
testId = objectBasicInfo.getRecentItems().get(0).getId();
}
@Test
public void testGetBlobField() throws Exception {
// get document with Name "Test Document"
final HashMap<String, Object> headers = new HashMap<>();
headers.put(SalesforceEndpointConfig.SOBJECT_NAME, "Document");
headers.put(SalesforceEndpointConfig.SOBJECT_EXT_ID_NAME, "Name");
final Document document = template().requestBodyAndHeaders("direct:getSObjectWithId", TEST_DOCUMENT_ID, headers,
Document.class);
assertNotNull(document);
// get Body field for this document
try (final InputStream body = template().requestBody("direct:getBlobField", document, InputStream.class)) {
assertNotNull(body);
assertTrue(body.available() > 0);
}
}
@Test
public void testGetDescription() throws Exception {
final SObjectDescription sObjectDescription = template().requestBody("direct:getDescription", null,
SObjectDescription.class);
assertNotNull(sObjectDescription);
}
@Test
public void testGetGlobalObjects() throws Exception {
final GlobalObjects globalObjects = template().requestBody("direct:getGlobalObjects", null,
GlobalObjects.class);
assertNotNull(globalObjects);
}
@Test
public void testGetResources() throws Exception {
final RestResources resources = template().requestBody("direct:getResources", null, RestResources.class);
assertNotNull(resources);
}
@Test
public void testGetSObject() throws Exception {
final Merchandise__c merchandise = template().requestBody("direct:getSObject", testId, Merchandise__c.class);
assertNotNull(merchandise);
assertNull(merchandise.getTotal_Inventory__c());
assertNotNull(merchandise.getPrice__c());
}
@Test
public void testGetVersions() throws Exception {
// test getVersions doesn't need a body
// assert expected result
final Object o = template().requestBody("direct:getVersions", (Object) null);
List<Version> versions = null;
if (o instanceof Versions) {
versions = ((Versions) o).getVersions();
} else {
@SuppressWarnings("unchecked")
final List<Version> tmp = (List<Version>) o;
versions = tmp;
}
assertNotNull(versions);
}
@Test
public void testQuery() throws Exception {
final QueryRecordsLine_Item__c queryRecords = template().requestBody("direct:query", null,
QueryRecordsLine_Item__c.class);
assertNotNull(queryRecords);
}
@Test
public void testQueryAll() throws Exception {
final QueryRecordsLine_Item__c queryRecords = template().requestBody("direct:queryAll", null,
QueryRecordsLine_Item__c.class);
assertNotNull(queryRecords);
}
@Test
public void testRetry() throws Exception {
final SalesforceComponent sf = context().getComponent("salesforce", SalesforceComponent.class);
final String accessToken = sf.getSession().getAccessToken();
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setSslContext(new SSLContextParameters().createSSLContext(context));
final HttpClient httpClient = new HttpClient(sslContextFactory);
httpClient.setConnectTimeout(60000);
httpClient.start();
final String uri = sf.getLoginConfig().getLoginUrl() + "/services/oauth2/revoke?token=" + accessToken;
final Request logoutGet = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(1, TimeUnit.MINUTES);
final ContentResponse response = logoutGet.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
testGetGlobalObjects();
}
@Test
public void testRetryFailure() throws Exception {
final SalesforceComponent sf = context().getComponent("salesforce", SalesforceComponent.class);
final String accessToken = sf.getSession().getAccessToken();
final SslContextFactory sslContextFactory = new SslContextFactory();
sslContextFactory.setSslContext(new SSLContextParameters().createSSLContext(context));
final HttpClient httpClient = new HttpClient(sslContextFactory);
httpClient.setConnectTimeout(60000);
httpClient.start();
final String uri = sf.getLoginConfig().getLoginUrl() + "/services/oauth2/revoke?token=" + accessToken;
final Request logoutGet = httpClient.newRequest(uri).method(HttpMethod.GET).timeout(1, TimeUnit.MINUTES);
final ContentResponse response = logoutGet.send();
assertEquals(HttpStatus.OK_200, response.getStatus());
// set component config to bad password to cause relogin attempts to fail
final String password = sf.getLoginConfig().getPassword();
sf.getLoginConfig().setPassword("bad_password");
try {
testGetGlobalObjects();
fail("Expected CamelExecutionException!");
} catch (final CamelExecutionException e) {
if (e.getCause() instanceof SalesforceException) {
final SalesforceException cause = (SalesforceException) e.getCause();
assertEquals("Expected 400 on authentication retry failure", HttpStatus.BAD_REQUEST_400,
cause.getStatusCode());
} else {
fail("Expected SalesforceException!");
}
} finally {
// reset password and retries to allow other tests to pass
sf.getLoginConfig().setPassword(password);
}
}
@Test
public void testSearch() throws Exception {
final Object obj = template().requestBody("direct:search", (Object) null);
List<SearchResult> searchResults = null;
if (obj instanceof SearchResults) {
final SearchResults results = (SearchResults) obj;
searchResults = results.getResults();
} else {
@SuppressWarnings("unchecked")
final List<SearchResult> tmp = (List<SearchResult>) obj;
searchResults = tmp;
}
assertNotNull(searchResults);
}
@Test
public void testStatus300() throws Exception {
// get test merchandise
// note that the header value overrides sObjectFields in endpoint
final Merchandise__c merchandise = template().requestBodyAndHeader("direct:getSObject", testId, "sObjectFields",
"Name,Description__c,Price__c,Total_Inventory__c", Merchandise__c.class);
assertNotNull(merchandise);
assertNotNull(merchandise.getName());
assertNotNull(merchandise.getPrice__c());
assertNotNull(merchandise.getTotal_Inventory__c());
CreateSObjectResult result = null;
try {
merchandise.clearBaseFields();
result = template().requestBody("direct:createSObject", merchandise, CreateSObjectResult.class);
assertNotNull(result);
assertNotNull(result.getId());
// look by external Id to cause 300 error
// note that the request SObject overrides settings on the endpoint for LineItem__c
try {
template().requestBody("direct:getSObjectWithId", merchandise, Merchandise__c.class);
fail("Expected SalesforceException with statusCode 300");
} catch (final CamelExecutionException e) {
final Throwable cause = e.getCause();
assertTrue(cause instanceof SalesforceMultipleChoicesException);
final SalesforceMultipleChoicesException multipleChoices = (SalesforceMultipleChoicesException) cause;
assertEquals(300, multipleChoices.getStatusCode());
final List<String> choices = multipleChoices.getChoices();
assertNotNull(choices);
assertFalse(choices.isEmpty());
}
} finally {
// delete the test clone
if (result != null) {
template().requestBody("direct:deleteSObject", result.getId());
}
}
}
@Test
public void testStatus400() throws Exception {
// get test merchandise
// note that the header value overrides sObjectFields in endpoint
final Merchandise__c merchandise = template().requestBodyAndHeader("direct:getSObject", testId, "sObjectFields",
"Description__c,Price__c", Merchandise__c.class);
assertNotNull(merchandise);
assertNotNull(merchandise.getPrice__c());
assertNull(merchandise.getTotal_Inventory__c());
merchandise.clearBaseFields();
// required field Total_Inventory__c is missing
CreateSObjectResult result = null;
try {
result = template().requestBody("direct:createSObject", merchandise, CreateSObjectResult.class);
fail("Expected SalesforceException with statusCode 400");
} catch (final CamelExecutionException e) {
final Throwable cause = e.getCause();
assertTrue(cause instanceof SalesforceException);
final SalesforceException badRequest = (SalesforceException) cause;
assertEquals(400, badRequest.getStatusCode());
assertEquals(1, badRequest.getErrors().size());
assertEquals("[Total_Inventory__c]", badRequest.getErrors().get(0).getFields().toString());
} finally {
// delete the clone if created
if (result != null) {
template().requestBody("direct:deleteSObject", result.getId());
}
}
}
@Test
public void testStatus404() {
// try to get a non existent SObject
try {
template().requestBody("direct:getSObject", "ILLEGAL_ID", Merchandise__c.class);
fail("Expected SalesforceException");
} catch (final CamelExecutionException e) {
final Throwable cause = e.getCause();
assertTrue(cause instanceof NoSuchSObjectException);
final NoSuchSObjectException noSuchObject = (NoSuchSObjectException) cause;
assertEquals(404, noSuchObject.getStatusCode());
assertEquals(1, noSuchObject.getErrors().size());
}
}
@Override
protected RouteBuilder doCreateRouteBuilder() throws Exception {
// create test route
return new RouteBuilder() {
@Override
public void configure() {
// testGetVersion
from("direct:getVersions").to("salesforce:getVersions?format=" + format);
// testGetResources
from("direct:getResources").to("salesforce:getResources?format=" + format);
// testGetGlobalObjects
from("direct:getGlobalObjects").to("salesforce:getGlobalObjects?format=" + format);
// testGetBasicInfo
from("direct:getBasicInfo").to("salesforce:getBasicInfo?sObjectName=Merchandise__c&format=" + format);
// testGetDescription
from("direct:getDescription")
.to("salesforce:getDescription?sObjectName=Merchandise__c&format=" + format);
// testGetSObject
from("direct:getSObject")
.to("salesforce:getSObject?sObjectName=Merchandise__c&sObjectFields=Description__c,Price__c&format="
+ format);
// testCreateSObject
from("direct:createSObject").to("salesforce:createSObject?sObjectName=Merchandise__c&format=" + format);
// testUpdateSObject
from("direct:updateSObject").to("salesforce:updateSObject?sObjectName=Merchandise__c&format=" + format);
// testDeleteSObject
from("direct:deleteSObject").to("salesforce:deleteSObject?sObjectName=Merchandise__c&format=" + format);
from("direct:deleteSObjectTaken").to("salesforce:deleteSObject?sObjectName=Task&format=" + format);
// testGetSObjectWithId
from("direct:getSObjectWithId")
.to("salesforce:getSObjectWithId?sObjectName=Line_Item__c&sObjectIdName=Name&format=" + format);
// testUpsertSObject
from("direct:deleteLineItems")
.to("salesforce:query?sObjectQuery=SELECT Id FROM Line_Item__C&sObjectClass="
+ QueryRecordsLine_Item__c.class.getName())
.transform(simple("${body.records}")).split(body()).transform(simple("${body.id}"))
.to("salesforce:deleteSObject?sObjectName=Line_Item__c");
from("direct:createLineItem").to("salesforce:createSObject?sObjectName=Line_Item__c");
from("direct:upsertSObject")
.to("salesforce:upsertSObject?sObjectName=Line_Item__c&sObjectIdName=Name&format=" + format);
// testDeleteSObjectWithId
from("direct:deleteSObjectWithId")
.to("salesforce:deleteSObjectWithId?sObjectName=Line_Item__c&sObjectIdName=Name&format=" + format);
// testGetBlobField
from("direct:getBlobField")
.to("salesforce:getBlobField?sObjectName=Document&sObjectBlobFieldName=Body&format=" + format);
// testQuery
from("direct:query").to("salesforce:query?sObjectQuery=SELECT name from Line_Item__c&sObjectClass="
+ QueryRecordsLine_Item__c.class.getName() + "&format=" + format);
// testQueryAll
from("direct:queryAll")
.to("salesforce:queryAll?sObjectQuery=SELECT name from Line_Item__c&sObjectClass="
+ QueryRecordsLine_Item__c.class.getName() + "&format=" + format);
// testSearch
from("direct:search").to("salesforce:search?sObjectSearch=FIND {Wee}&format=" + format);
// testApexCall
from("direct:apexCallGet")
.to("salesforce:apexCall?apexMethod=GET&apexUrl=Merchandise/{id}&sObjectName=Merchandise__c&format="
+ format);
from("direct:apexCallGetWithId").to("salesforce:apexCall/Merchandise/?apexMethod=GET&id=dummyId&format="
+ format + "&sObjectClass=" + Merchandise__c.class.getName());
from("direct:apexCallPatch").to("salesforce:apexCall/Merchandise/?format=" + format
+ "&apexMethod=PATCH&sObjectClass=" + MerchandiseResponse.class.getName());
}
};
}
@Parameters(name = "format = {0}")
public static Iterable<String> parameters() {
return Arrays.asList("XML", "JSON");
}
}