/** * 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.IOException; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import com.googlecode.junittoolbox.ParallelParameterized; import com.thoughtworks.xstream.annotations.XStreamImplicit; import org.apache.camel.CamelExecutionException; import org.apache.camel.builder.RouteBuilder; import org.apache.camel.component.salesforce.api.dto.AbstractQueryRecordsBase; import org.apache.camel.component.salesforce.api.dto.CreateSObjectResult; import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch; import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatch.Method; import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResponse; import org.apache.camel.component.salesforce.api.dto.composite.SObjectBatchResult; import org.apache.camel.component.salesforce.api.utils.Version; import org.apache.camel.component.salesforce.dto.generated.Account; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized.Parameters; @RunWith(ParallelParameterized.class) public class CompositeApiBatchIntegrationTest extends AbstractSalesforceTestBase { public static class Accounts extends AbstractQueryRecordsBase { @XStreamImplicit private List<Account> records; public List<Account> getRecords() { return records; } public void setRecords(final List<Account> records) { this.records = records; } } private static final Set<String> VERSIONS = new HashSet<>( Arrays.asList(SalesforceEndpointConfig.DEFAULT_VERSION, "34.0", "36.0", "37.0", "39.0")); private String accountId; private final String batchuri; private final String version; public CompositeApiBatchIntegrationTest(final String format, final String version) { this.version = version; batchuri = "salesforce:composite-batch?format=" + format; } @After public void removeRecords() { try { template.sendBody("salesforce:deleteSObject?sObjectName=Account&sObjectId=" + accountId, null); } catch (final CamelExecutionException ignored) { // other tests run in parallel could have deleted the Account } template.request("direct:deleteBatchAccounts", null); } @Before public void setupRecords() { final Account account = new Account(); account.setName("Composite API Batch"); final CreateSObjectResult result = template.requestBody("salesforce:createSObject", account, CreateSObjectResult.class); accountId = result.getId(); } @Test public void shouldSubmitBatchUsingCompositeApi() { final SObjectBatch batch = new SObjectBatch(version); final Account updates = new Account(); updates.setName("NewName"); batch.addUpdate("Account", accountId, updates); final Account newAccount = new Account(); newAccount.setName("Account created from Composite batch API"); batch.addCreate(newAccount); batch.addGet("Account", accountId, "Name", "BillingPostalCode"); batch.addDelete("Account", accountId); final SObjectBatchResponse response = template.requestBody(batchuri, batch, SObjectBatchResponse.class); assertNotNull("Response should be provided", response); assertFalse(response.hasErrors()); } @Test public void shouldSupportGenericBatchRequests() { final SObjectBatch batch = new SObjectBatch(version); batch.addGeneric(Method.GET, "/sobjects/Account/" + accountId); testBatch(batch); } @Test public void shouldSupportLimits() { final SObjectBatch batch = new SObjectBatch(version); batch.addLimits(); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); @SuppressWarnings("unchecked") final Map<String, Object> result = (Map<String, Object>) batchResult.getResult(); // JSON and XML structure are different, XML has `LimitsSnapshot` node, JSON does not @SuppressWarnings("unchecked") final Map<String, Object> limits = (Map<String, Object>) result.getOrDefault("LimitsSnapshot", result); @SuppressWarnings("unchecked") final Map<String, String> apiRequests = (Map<String, String>) limits.get("DailyApiRequests"); // for JSON value will be Integer, for XML (no type information) it will be String assertEquals("15000", String.valueOf(apiRequests.get("Max"))); } @Test public void shouldSupportObjectCreation() { final SObjectBatch batch = new SObjectBatch(version); final Account newAccount = new Account(); newAccount.setName("Account created from Composite batch API"); batch.addCreate(newAccount); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); @SuppressWarnings("unchecked") final Map<String, Object> result = (Map<String, Object>) batchResult.getResult(); // JSON and XML structure are different, XML has `Result` node, JSON does not @SuppressWarnings("unchecked") final Map<String, Object> creationOutcome = (Map<String, Object>) result.getOrDefault("Result", result); assertNotNull(creationOutcome.get("id")); } @Test public void shouldSupportObjectDeletion() { final SObjectBatch batch = new SObjectBatch(version); batch.addDelete("Account", accountId); testBatch(batch); } @Test public void shouldSupportObjectRetrieval() { final SObjectBatch batch = new SObjectBatch(version); batch.addGet("Account", accountId, "Name"); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); @SuppressWarnings("unchecked") final Map<String, Object> result = (Map<String, Object>) batchResult.getResult(); // JSON and XML structure are different, XML has `Account` node, JSON does not @SuppressWarnings("unchecked") final Map<String, String> data = (Map<String, String>) result.getOrDefault("Account", result); assertEquals("Composite API Batch", data.get("Name")); } @Test public void shouldSupportObjectUpdates() { final SObjectBatch batch = new SObjectBatch(version); final Account updates = new Account(); updates.setName("NewName"); updates.setAccountNumber("AC12345"); batch.addUpdate("Account", accountId, updates); testBatch(batch); } @Test public void shouldSupportQuery() { final SObjectBatch batch = new SObjectBatch(version); batch.addQuery("SELECT Id, Name FROM Account"); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); @SuppressWarnings("unchecked") final Map<String, Object> result = (Map<String, Object>) batchResult.getResult(); // JSON and XML structure are different, XML has `QueryResult` node, JSON does not @SuppressWarnings("unchecked") final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result); assertNotNull(data.get("totalSize")); } @Test public void shouldSupportQueryAll() { final SObjectBatch batch = new SObjectBatch(version); batch.addQueryAll("SELECT Id, Name FROM Account"); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); @SuppressWarnings("unchecked") final Map<String, Object> result = (Map<String, Object>) batchResult.getResult(); // JSON and XML structure are different, XML has `QueryResult` node, JSON does not @SuppressWarnings("unchecked") final Map<String, String> data = (Map<String, String>) result.getOrDefault("QueryResult", result); assertNotNull(data.get("totalSize")); } @Test public void shouldSupportRelatedObjectRetrieval() throws IOException { if (Version.create(version).compareTo(Version.create("36.0")) < 0) { return; } final SObjectBatch batch = new SObjectBatch("36.0"); batch.addGetRelated("Account", accountId, "CreatedBy"); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); @SuppressWarnings("unchecked") final Map<String, Object> result = (Map<String, Object>) batchResult.getResult(); // JSON and XML structure are different, XML has `User` node, JSON does not @SuppressWarnings("unchecked") final Map<String, String> data = (Map<String, String>) result.getOrDefault("User", result); final SalesforceLoginConfig loginConfig = LoginConfigHelper.getLoginConfig(); assertEquals(loginConfig.getUserName(), data.get("Username")); } @Test public void shouldSupportSearch() { final SObjectBatch batch = new SObjectBatch(version); // we cannot rely on search returning the `Composite API Batch` account as the search indexer runs // asynchronously to object creation, so that account might not be indexed at this time, so we search for // `United` Account that should be created with developer instance batch.addSearch("FIND {United} IN Name Fields RETURNING Account (Name)"); final SObjectBatchResponse response = testBatch(batch); final List<SObjectBatchResult> results = response.getResults(); final SObjectBatchResult batchResult = results.get(0); final Object firstBatchResult = batchResult.getResult(); final Object searchResult; if (firstBatchResult instanceof Map) { // the JSON and XML responses differ, XML has a root node which can be either SearchResults or // SearchResultWithMetadata // furthermore version 37.0 search results are no longer array, but dictionary of { // "searchRecords": [<array>] } and the XML output changed to <SearchResultWithMetadata><searchRecords>, so // we have: // @formatter:off // | version | format | response syntax | // | 34 | JSON | {attributes={type=Account... | // | 34 | XML | {SearchResults={attributes={type=Account... | // | 37 | JSON | {searchRecords=[{attributes={type=Account... | // | 37 | XML | {SearchResultWithMetadata={searchRecords={attributes={type=Account... | // @formatter:on @SuppressWarnings("unchecked") final Map<String, Object> tmp = (Map<String, Object>) firstBatchResult; @SuppressWarnings("unchecked") final Map<String, Object> nested = (Map<String, Object>) tmp.getOrDefault("SearchResultWithMetadata", tmp); // JSON and XML structure are different, XML has `SearchResults` node, JSON does not searchResult = nested.getOrDefault("searchRecords", nested.getOrDefault("SearchResults", nested)); } else { searchResult = firstBatchResult; } final Map<String, Object> result; if (searchResult instanceof List) { @SuppressWarnings("unchecked") final Map<String, Object> tmp = (Map<String, Object>) ((List) searchResult).get(0); result = tmp; } else { @SuppressWarnings("unchecked") final Map<String, Object> tmp = (Map<String, Object>) searchResult; result = tmp; } assertNotNull(result.get("Name")); } @Override protected RouteBuilder doCreateRouteBuilder() throws Exception { return new RouteBuilder() { @Override public void configure() throws Exception { from("direct:deleteBatchAccounts") .to("salesforce:query?sObjectClass=" + Accounts.class.getName() + "&sObjectQuery=SELECT Id FROM Account WHERE Name = 'Account created from Composite batch API'") .split(simple("${body.records}")).setHeader("sObjectId", simple("${body.id}")) .to("salesforce:deleteSObject?sObjectName=Account").end(); } }; } @Override protected String salesforceApiVersionToUse() { return version; } SObjectBatchResponse testBatch(final SObjectBatch batch) { final SObjectBatchResponse response = template.requestBody(batchuri, batch, SObjectBatchResponse.class); assertNotNull("Response should be provided", response); assertFalse("Received errors in: " + response, response.hasErrors()); return response; } @Parameters(name = "format = {0}, version = {1}") public static Iterable<Object[]> formats() { return VERSIONS.stream().flatMap(v -> Stream.of(new Object[] {"JSON", v}, new Object[] {"XML", v})) .collect(Collectors.toList()); } }