/*
* Copyright © 2013. Palomino Labs (http://palominolabs.com)
*
* Licensed 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 com.palominolabs.crm.sf.soap;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.AsyncRequestState;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.DocumentFolder;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.FilterItem;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.FilterOperation;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.FolderAccessTypes;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.Metadata;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.StaticResource;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.StaticResourceCacheControl;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.UpdateMetadata;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.WorkflowActionReference;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.WorkflowActionType;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.WorkflowOutboundMessage;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.WorkflowRule;
import com.palominolabs.crm.sf.soap.jaxwsstub.metadata.WorkflowTriggerTypes;
import com.palominolabs.crm.sf.soap.jaxwsstub.partner.UnexpectedErrorFault_Exception;
import com.palominolabs.crm.sf.testutil.ConnectionTestSfUserProps;
import com.palominolabs.crm.sf.testutil.TestFixtureUtils;
import org.apache.commons.codec.binary.Base64;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import javax.xml.namespace.QName;
import javax.xml.soap.SOAPFault;
import javax.xml.ws.soap.SOAPFaultException;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static com.palominolabs.testutil.CollectionAssert.assertSetEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class MetadataConnectionImplTest {
private MetadataConnection mdconn;
private ConnectionBundleImpl bundle;
private String username;
@Before
public void setUp() throws ApiException {
this.username = ConnectionTestSfUserProps.getPropVal("com.palominolabs.test.crm.sf.conn.metadata.user");
String password = ConnectionTestSfUserProps.getPropVal("com.palominolabs.test.crm.sf.conn.metadata.password");
this.bundle = TestConnectionUtils.getConnectionBundle(username, password);
this.mdconn = bundle.getMetadataConnection();
// com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump = true;
}
@After
public void tearDown() {
// com.sun.xml.ws.transport.http.client.HttpTransportPipe.dump = false;
}
@Test
public void testCreateAndDeleteWorkflowRule()
throws ApiException, InterruptedException, InvocationTargetException, NoSuchMethodException,
InstantiationException, IllegalAccessException {
deleteAllOfType("WorkflowOutboundMessage", WorkflowOutboundMessage.class);
deleteAllOfType("WorkflowRule", WorkflowRule.class);
WorkflowOutboundMessage message = new WorkflowOutboundMessage();
message.setApiVersion(ApiVersion.API_VERSION_DOUBLE);
message.setDescription("test-action-desc");
message.setEndpointUrl("http://foo.com/bar");
message.getFields().addAll(Arrays.asList("FirstName", "LastName"));
String actionName = "test-action-name";
message.setName(actionName);
message.setIntegrationUser(this.username);
message.setFullName("Contact.testactionfullname");
List<Metadata> messageMdList = new ArrayList<Metadata>();
messageMdList.add(message);
createMetadata(messageMdList);
WorkflowRule rule = new WorkflowRule();
WorkflowActionReference actionRef = new WorkflowActionReference();
actionRef.setName("testactionfullname");
actionRef.setType(WorkflowActionType.OUTBOUND_MESSAGE);
rule.getActions().add(actionRef);
FilterItem filterItem = new FilterItem();
filterItem.setField("Contact.FirstName");
filterItem.setValue("asdf");
filterItem.setOperation(FilterOperation.CONTAINS);
rule.getCriteriaItems().add(filterItem);
rule.setActive(true);
rule.setDescription("wf desc");
rule.setFullName("Contact.testwf");
rule.setTriggerType(WorkflowTriggerTypes.ON_ALL_CHANGES);
List<Metadata> mdList = new ArrayList<Metadata>();
mdList.add(rule);
createMetadata(mdList);
}
@Test
public void testCreateAndDeleteFolder() throws ApiException, InterruptedException {
List<Metadata> mdList = new ArrayList<Metadata>();
DocumentFolder folder = new DocumentFolder();
mdList.add(folder);
folder.setFullName("testFolderFullName");
folder.setAccessType(FolderAccessTypes.PUBLIC);
folder.setName("testFolderName");
createAndDeleteMetadata(mdList);
}
@Test
public void testUpdateAndDeleteFolder() throws ApiException, InterruptedException {
List<Metadata> mdList = new ArrayList<Metadata>();
DocumentFolder folder = new DocumentFolder();
mdList.add(folder);
folder.setFullName("testFolderFullName");
folder.setAccessType(FolderAccessTypes.PUBLIC);
folder.setName("testFolderName");
try {
createMetadata(mdList);
UpdateMetadata updateMetadata = new UpdateMetadata();
updateMetadata.setMetadata(folder);
folder.setFullName(folder.getFullName() + "updated");
updateMetadata.setCurrentName("testFolderFullName");
checkResults(mdconn.update(Arrays.asList(updateMetadata)));
List<FileProperties> propsList =
mdconn.listMetadata(Arrays.asList(new ListMetadataQuery("DocumentFolder")));
FileProperties props = propsList.get(0);
assertEquals(folder.getFullName(), props.getFullName());
} finally {
// delete will use the updated folder name
deleteMetadata(mdList);
}
}
@Test
public void testCreateAndDeleteStaticResource() throws ApiException, InterruptedException {
List<Metadata> mdList = new ArrayList<Metadata>();
StaticResource metadata = new StaticResource();
mdList.add(metadata);
// have to be able to create folders since this requires a folder name
metadata.setFullName("testStaticResource");
metadata.setCacheControl(StaticResourceCacheControl.PUBLIC);
metadata.setContent("Foobar".getBytes());
metadata.setContentType("text/plain");
createAndDeleteMetadata(mdList);
}
@Test
@SuppressWarnings("unchecked")
public void testListMetadataGetCustomFieldMetadata() throws ApiException {
ListMetadataQuery query = new ListMetadataQuery("CustomField");
List<FileProperties> actual = this.mdconn.listMetadata(Collections.singletonList(query));
List<FileProperties> expected = (List<FileProperties>) TestFixtureUtils
.loadFixtures("/sObjectFixtures/MetadataConnectionTests/metadataCustomFieldInfo.xml");
Set<String> expectedStrings = new HashSet<String>();
for (FileProperties fileProperties : expected) {
expectedStrings.add(fileProperties.toString());
}
Set<String> actualStrings = new HashSet<String>();
for (FileProperties fileProperties : actual) {
actualStrings.add(fileProperties.toString());
}
// compare the strings since implementing equals() on FileProperties would be a pain
assertSetEquals(expectedStrings, actualStrings);
}
@Test
public void testGetFilePropertiesIdMayBeNull() throws ApiException {
/* WSDL declares FileProperties.id as required and non-null but sometimes it shows up as "" over the wire.
This should be exposed as a null id. Unfortunately I cannot repro on my data set but Edwin's email of 2012-03-04 contains
a demonstration that the problem is real. All I can do is check that getting FP's id doesn't crash.
*/
List<FileProperties> fpList = mdconn.listMetadata(Arrays.asList(new ListMetadataQuery("CustomField")));
for (FileProperties fileProperties : fpList) {
// doesn't crash
fileProperties.getId();
}
}
@Test
public void testListMetadataWithInvalidSessionId()
throws NoSuchFieldException, UnexpectedErrorFault_Exception, IllegalAccessException, ApiException,
InterruptedException {
// this should be the connection that was used to make the metadata connection
logout();
Thread.sleep(1000);
// the session is now dead
ListMetadataQuery query = new ListMetadataQuery("CustomField");
try {
this.mdconn.listMetadata(Collections.singletonList(query));
fail();
} catch (ApiException e) {
assertInvalidSession(e);
}
}
@Test
public void testCreateWithExpiredSession()
throws NoSuchFieldException, UnexpectedErrorFault_Exception, IllegalAccessException, InterruptedException,
ApiException {
logout();
Thread.sleep(1000);
List<Metadata> mdList = new ArrayList<Metadata>();
StaticResource metadata = new StaticResource();
mdList.add(metadata);
// have to be able to create folders since this requires a folder name
metadata.setFullName("testStaticResource");
metadata.setCacheControl(StaticResourceCacheControl.PUBLIC);
metadata.setContent("Foobar".getBytes());
metadata.setContentType("text/plain");
try {
mdconn.create(mdList);
fail();
} catch (ApiException e) {
assertInvalidSession(e);
}
}
@SuppressWarnings("unchecked")
@Test
public void testRetrieve() throws ApiException, InterruptedException, IOException {
// very basic retrieve that only gets package.xml
final UnpackagedComponents unpackagedComponents =
new UnpackagedComponents(null, null, null, null, new ArrayList<ProfileObjectPermissions>(), null,
new ArrayList<UnpackagedComponent>(), "1234");
final RetrieveRequest req =
new RetrieveRequest(ApiVersion.API_VERSION_DOUBLE, new ArrayList<String>(), new ArrayList<String>(),
unpackagedComponents);
final AsyncResult asyncResult = mdconn.retrieve(req);
final WaitForAsyncResult waitResult = mdconn.waitForAsyncResults(Arrays.asList(asyncResult), 20000);
assertTrue(!waitResult.getComplete().isEmpty());
final RetrieveResult result = mdconn.getRetrieveResult(asyncResult.getId());
assertTrue(result.getRetrieveMessages().isEmpty());
Map<String, byte[]> expected =
(Map<String, byte[]>) TestFixtureUtils
.loadFixtures("/sObjectFixtures/MetadataConnectionTests/retrieve.xml");
Map<String, String> expectedHex = new HashMap<String, String>();
for (Map.Entry<String, byte[]> stringEntry : expected.entrySet()) {
expectedHex.put(stringEntry.getKey(), Base64.encodeBase64String(stringEntry.getValue()));
}
Map<String, String> actualHex = new HashMap<String, String>();
for (Map.Entry<String, byte[]> stringEntry : result.getZipFileEntryBytes().entrySet()) {
actualHex.put(stringEntry.getKey(), Base64.encodeBase64String(stringEntry.getValue()));
}
assertEquals(expectedHex, actualHex);
}
@Test
public void testDescribeMetadata() throws ApiException {
final DescribeMetadataResult actual = mdconn.describeMetadata();
DescribeMetadataResult expected =
(DescribeMetadataResult) TestFixtureUtils
.loadFixtures("/sObjectFixtures/MetadataConnectionTests/describeMetadata.xml");
assertEquals(expected.getOrganizationNamespace(), actual.getOrganizationNamespace());
assertEquals(expected.isPartialSaveAllowed(), actual.isPartialSaveAllowed());
assertEquals(expected.isTestRequired(), actual.isTestRequired());
assertEquals(expected.getObjectList().size(), actual.getObjectList().size());
for (int i = 0; i < expected.getObjectList().size(); i++) {
final DescribeMetadataObject expectedObj = expected.getObjectList().get(i);
final DescribeMetadataObject actualObj = actual.getObjectList().get(i);
String message = "Expected: " + expectedObj.getXmlName() + "; actual: " + actualObj.getXmlName();
assertEquals(message, expectedObj.getChildXmlNames(), actualObj.getChildXmlNames());
assertEquals(message, expectedObj.getDirectoryName(), actualObj.getDirectoryName());
assertEquals(message, expectedObj.isInFolder(), actualObj.isInFolder());
assertEquals(message, expectedObj.isMetaFile(), actualObj.isMetaFile());
assertEquals(message, expectedObj.getSuffix(), actualObj.getSuffix());
assertEquals(message, expectedObj.getXmlName(), actualObj.getXmlName());
}
}
/**
* This will only delete objects whose fullname includes ".test" as in Contact.testFoo.
*
* @param type the type string to use to find instances in salesforce
* @param typeClass the class used to create objects to pass into delete (should match the type string). Will be
* used to reflectively create an instance.
*
* @throws InterruptedException
* @throws NoSuchMethodException
* @throws InvocationTargetException
* @throws IllegalAccessException
* @throws InstantiationException
*/
@SuppressWarnings("JavaDoc")
private void deleteAllOfType(String type, Class<? extends Metadata> typeClass)
throws ApiException, InterruptedException, NoSuchMethodException, InvocationTargetException,
IllegalAccessException, InstantiationException {
List<FileProperties> propertiesList = mdconn.listMetadata(Arrays.asList(new ListMetadataQuery(type)));
List<Metadata> toDelete = new ArrayList<Metadata>();
Constructor<? extends Metadata> ctor = typeClass.getConstructor();
for (FileProperties msgProp : propertiesList) {
Metadata md = ctor.newInstance();
if (msgProp.getFullName().contains(".test")) {
// only delete test ones
md.setFullName(msgProp.getFullName());
toDelete.add(md);
}
}
if (toDelete.isEmpty()) {
return;
}
deleteMetadata(toDelete);
}
private static void assertInvalidSession(ApiException e) {
assertEquals("Call failed", e.getMessage());
Throwable cause = e.getCause();
assertTrue(cause instanceof SOAPFaultException);
SOAPFaultException soapFaultException = (SOAPFaultException) cause;
String expectedMsg =
"INVALID_SESSION_ID: Invalid Session ID found in SessionHeader: Illegal Session. Session not found, missing session key: ";
String actualMsg = soapFaultException.getMessage();
assertEquals(expectedMsg, truncateSessionId(actualMsg));
SOAPFault fault = soapFaultException.getFault();
QName codeQname = fault.getFaultCodeAsQName();
assertEquals("INVALID_SESSION_ID", codeQname.getLocalPart());
String faultMsg = fault.getFaultString();
assertEquals(expectedMsg, truncateSessionId(faultMsg));
}
/**
* Session IDs start with 00D, so this just returns everything before 00D
*
* @param message the fault message to remove the session id from
*
* @return the message with the session ID chopped off
*/
private static String truncateSessionId(String message) {
//noinspection DynamicRegexReplaceableByCompiledPattern
return message.split("00D")[0];
}
private void logout() throws ApiException {
PartnerConnectionImplTest.logout(this.bundle.getPartnerConnection());
}
private void createMetadata(List<Metadata> mdList) throws ApiException, InterruptedException {
checkResults(mdconn.create(mdList));
}
private void checkResults(List<AsyncResult> asyncResults)
throws ApiException, InterruptedException {
WaitForAsyncResult newResults = mdconn.waitForAsyncResults(asyncResults, 20000);
List<AsyncResult> resultList = newResults.getAll();
assertEquals(asyncResults.size(), resultList.size());
for (int i = 0; i < asyncResults.size(); i++) {
AsyncResult result = resultList.get(i);
assertTrue(result.isDone());
assertEquals(result.getMessage(), AsyncRequestState.COMPLETED, result.getState());
}
}
private void createAndDeleteMetadata(List<Metadata> mdList) throws ApiException, InterruptedException {
try {
createMetadata(mdList);
} finally {
deleteMetadata(mdList);
}
}
private void deleteMetadata(List<Metadata> mdList) throws ApiException, InterruptedException {
checkResults(mdconn.delete(mdList));
}
}