blob: f8e696faf4c4301f0a444103953ba7e501e7510f [file] [log] [blame]
/*
* 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.sling.resourceresolver.util;
import static java.util.Arrays.asList;
import static org.hamcrest.Matchers.instanceOf;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.withSettings;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import org.apache.sling.api.resource.NonExistingResource;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.resource.ResourceMetadata;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ValueMap;
import org.apache.sling.api.resource.mapping.PathToUriMappingService;
import org.apache.sling.api.wrappers.ValueMapDecorator;
import org.apache.sling.resourceresolver.impl.SimpleValueMapImpl;
import org.apache.sling.resourceresolver.impl.helper.RedirectResource;
import org.apache.sling.resourceresolver.impl.mapping.MapEntry;
import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProvider;
import org.apache.sling.resourceresolver.impl.mapping.StringInterpolationProviderConfiguration;
import org.apache.sling.spi.resource.provider.ResolveContext;
import org.apache.sling.spi.resource.provider.ResourceContext;
import org.apache.sling.spi.resource.provider.ResourceProvider;
import org.mockito.Mockito;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.osgi.framework.BundleContext;
import junit.framework.TestCase;
public class MockTestUtil {
static final String PROP_SLING_TARGET = "sling:target";
static final String PROP_SLING_STATUS = "sling:status";
public static void checkRedirectResource(Resource redirect, String target, int status) {
assertThat("Not a Redirect Resource", redirect, instanceOf(RedirectResource.class));
RedirectResource redirectResource = (RedirectResource) redirect;
ValueMap values = redirectResource.adaptTo(ValueMap.class);
assertEquals("Redirect Target is wrong", target, values.get(PROP_SLING_TARGET, String.class));
assertEquals("Redirect Status is wrong", new Integer(status), values.get(PROP_SLING_STATUS, Integer.class));
}
public static void checkNonExistingResource(Resource redirect, String path) {
assertThat("Not a Non Existing Resource", redirect, instanceOf(NonExistingResource.class));
NonExistingResource nonExistingResource = (NonExistingResource) redirect;
if(path != null) {
assertEquals("Wrong Path for Non Existing Resource", path, nonExistingResource.getPath());
}
}
public static void checkInternalResource(Resource internal, String path) {
assertEquals("Wrong Path for Resource", path, internal.getPath());
}
/**
* Extract the name from a resource path
*
* @param fullPath Full / Aboslute path to the resource
* @return Name of the resource
*/
public static String getResourceName(String fullPath) {
int n = fullPath.lastIndexOf("/");
return fullPath.substring(n + 1);
}
/**
* Creates a Mock Http Servlet Request
* @param url Absolute URL to be used to get the method, host and port
* @return Http Servlet Request if the url is valid otherwise null
*/
public static HttpServletRequest createRequestFromUrl(String url) {
int index = url.indexOf("://");
if(index > 0) {
String method = url.substring(0, index);
int port = 80;
int index2 = url.indexOf(":", index + 3);
int index3 = url.indexOf("/", index2 > index ? index2 : index + 3);
String host = "";
if (index2 > 0) {
port = new Integer(url.substring(index2 + 1, index3));
host = url.substring(index + 3, index2);
} else {
if(index3 > 0) {
host = url.substring(index + 3, index3);
} else {
host = url.substring(index + 3);
}
}
HttpServletRequest request = mock(HttpServletRequest.class);
when(request.getScheme()).thenReturn(method);
when(request.getServerName()).thenReturn(host);
when(request.getServerPort()).thenReturn(port);
return request;
}
return null;
}
/**
* Build a resource with path, parent, provider and resource resolver.
*
* @param fullPath Full Path of the Resource
* @param parent Parent of this resource but it can be null
* @param resourceResolver Resource Resolver of this resource
* @param provider Resource Provider Instance
* @param properties Key / Value pair for resource properties (the number of strings must be even)
* @return Mock Resource able to handle addition of children later on
*/
@SuppressWarnings("unchecked")
public static Resource buildResource(String fullPath, Resource parent, ResourceResolver resourceResolver, ResourceProvider<?> provider, String... properties) {
if (properties != null && properties.length % 2 != 0) {
throw new IllegalArgumentException("List of Resource Properties must be an even number: " + asList(properties));
}
Resource resource = mock(Resource.class, withSettings().name(getResourceName(fullPath)).extraInterfaces(ResourceChildrenAccessor.class));
when(resource.getName()).thenReturn(getResourceName(fullPath));
when(resource.getPath()).thenReturn(fullPath);
ResourceMetadata resourceMetadata = new ResourceMetadata();
when(resource.getResourceMetadata()).thenReturn(resourceMetadata);
when(resource.getResourceResolver()).thenReturn(resourceResolver);
if (parent != null) {
List<Resource> childList = ((ResourceChildrenAccessor) parent).getChildrenList();
childList.add(resource);
}
final List<Resource> childrenList = new ArrayList<>();
when(((ResourceChildrenAccessor) resource).getChildrenList()).thenReturn(childrenList);
// Delay the children list iterator to make sure all children are added beforehand
// Iterators have a modCount that is set when created. Any changes to the underlying list will
// change that modCount and the usage of the iterator will fail due to Concurrent Modification Exception
when(resource.listChildren()).thenAnswer(new Answer<Iterator<Resource>>() {
@Override
public Iterator<Resource> answer(InvocationOnMock invocation) throws Throwable {
return childrenList.iterator();
}
});
// register the resource with the provider
if (provider != null) {
when(provider.listChildren(Mockito.any(ResolveContext.class), Mockito.eq(resource))).thenAnswer(new Answer<Iterator<Resource>>() {
@Override
public Iterator<Resource> answer(InvocationOnMock invocation) throws Throwable {
return childrenList.iterator();
}
});
when(provider.getResource(Mockito.any(ResolveContext.class), Mockito.eq(fullPath), Mockito.any(ResourceContext.class), Mockito.any(Resource.class))).thenReturn(resource);
}
if (properties != null) {
ValueMap vm = new SimpleValueMapImpl();
for (int i = 0; i < properties.length; i += 2) {
resourceMetadata.put(properties[i], properties[i + 1]);
vm.put(properties[i], properties[i + 1]);
}
when(resource.getValueMap()).thenReturn(vm);
when(resource.adaptTo(Mockito.eq(ValueMap.class))).thenReturn(vm);
} else {
when(resource.getValueMap()).thenReturn(ValueMapDecorator.EMPTY);
when(resource.adaptTo(Mockito.eq(ValueMap.class))).thenReturn(ValueMapDecorator.EMPTY);
}
return resource;
}
/**
* Calls a private method that has no parameter like a getter
*
* @param methodName Name of the method
* @param target Target instance
* @return Object that is returned from the method call
*
* @throws UnsupportedOperationException If the call failed because it method is not found, has no access or invocation failed
*/
public static <T> T callInaccessibleMethod(String methodName, Class<T> returnType, Object target) {
return callInaccessibleMethod(methodName, returnType, target, new Class[] {}, new Object[] {});
}
/**
* Calls a private method that has one parameter like a setter method
*
* @param methodName Name of the method
* @param target Target instance
* @param paramsType Parameter Type which cannot be null
* @param param Parameter Value
* @return Object that is returned from the method call
*
* @throws UnsupportedOperationException If the call failed because it method is not found, has no access or invocation failed
*/
public static <T> T callInaccessibleMethod(String methodName, Class<T> returnType, Object target, Class paramsType, Object param) {
return callInaccessibleMethod(methodName, returnType, target, new Class[] {paramsType}, new Object[] {param});
}
/**
* Calls a private method that has none or one parameter like a setter method
*
* ATTENTION: If parameter types of values is null then both are set to null. Also the length of the arrays must
* be the same
*
* @param methodName Name of the method
* @param target Target instance
* @param parameterTypes Parameter Types which must not be null
* @param parameters Parameter Values which must not be null
* @return Object that is returned from the method call
*
* @throws IllegalArgumentException If the parameter types and values do not match
* @throws UnsupportedOperationException If the call failed because it method is not found, has no access or invocation failed
*/
public static <T> T callInaccessibleMethod(String methodName, Class<T> returnType, Object target, Class[] parameterTypes, Object[] parameters) {
if(parameterTypes != null && parameters != null) {
if(parameters.length != parameterTypes.length) { throw new IllegalArgumentException("Number of Parameter Types and Values were not the same"); }
} else {
throw new IllegalArgumentException("Parameter Type and Value Array cannot be null");
}
try {
return getInaccessibleMethod(methodName, returnType, target, parameterTypes).call(parameters);
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Failed to access method: " + methodName, e);
} catch (InvocationTargetException e) {
throw new UnsupportedOperationException("Failed to invoke method: " + methodName, e);
}
}
public static <T> MethodWrapper<T> getInaccessibleMethod(String methodName, Class<T> returnType, Object target, Class...parameterTypes) {
return new MethodWrapper(methodName, returnType, target, parameterTypes);
}
public static class MethodWrapper<T> {
private Method method;
private Object target;
public MethodWrapper(String methodName, Class<T> returnType, Object target, Class[] parameterTypes) {
try {
this.method = target.getClass().getDeclaredMethod(methodName, parameterTypes);
this.method.setAccessible(true);
this.target = target;
if(returnType == null && !this.method.getReturnType().equals(Void.TYPE)) {
throw new IllegalArgumentException("Return Type is null but method does not return Void but: " + this.method.getReturnType());
}
if(returnType != null && !returnType.isAssignableFrom(this.method.getReturnType())) {
throw new IllegalArgumentException("Return Type is not assignable to: " + returnType + ", it returns this: " + this.method.getReturnType());
}
} catch (NoSuchMethodException e) {
throw new UnsupportedOperationException("Failed to find method: " + methodName, e);
}
}
public T call(Object...parameters) throws InvocationTargetException, IllegalAccessException {
return (T) method.invoke(target, parameters);
}
}
/**
* Sets the value of a private field
*
* @param fieldName Name of the field to be set
* @param target Target instance
* @param fieldValue Value to be set
*
* @throws UnsupportedOperationException If the call failed because it field is not found or has no access
*/
public static void setInaccessibleField(String fieldName, Object target, Object fieldValue) throws NoSuchMethodException {
try {
getInaccessibleFieldWrapper(fieldName, target, Object.class).set(fieldValue);
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Failed to access field: " + fieldName, e);
}
}
public static <T> T getInaccessibleField(String fieldName, Object target, Class<T> type) {
try {
return getInaccessibleFieldWrapper(fieldName, target, type).get();
} catch (IllegalAccessException e) {
throw new UnsupportedOperationException("Failed to access field: " + fieldName, e);
}
}
public static <T> FieldWrapper<T> getInaccessibleFieldWrapper(String fieldName, Object target, Class<T> type) {
try {
return new FieldWrapper(fieldName, target, type);
} catch (NoSuchFieldException e) {
throw new UnsupportedOperationException("Failed to find field: " + fieldName, e);
}
}
public static StringInterpolationProviderConfiguration createStringInterpolationProviderConfiguration() {
StringInterpolationProviderConfiguration answer = mock(StringInterpolationProviderConfiguration.class);
when(answer.placeHolderKeyValuePairs()).thenReturn(new String[] {});
return answer;
}
public static void setupStringInterpolationProvider(
StringInterpolationProvider provider, StringInterpolationProviderConfiguration configuration, final String[] placeholderValues
) {
when(configuration.placeHolderKeyValuePairs()).thenReturn(placeholderValues);
BundleContext context = mock(BundleContext.class);
callInaccessibleMethod("activate", Void.TYPE, provider,
new Class[] {BundleContext.class, StringInterpolationProviderConfiguration.class},
new Object[] {context, configuration}
);
}
public static class FieldWrapper<T> {
private Field field;
private Object target;
public FieldWrapper(String fieldName, Object target, Class<T> type) throws NoSuchFieldException {
this.field = target.getClass().getDeclaredField(fieldName);
this.field.setAccessible(true);
this.target = target;
}
public void set(T parameter) throws IllegalAccessException {
field.set(target, parameter);
}
public T get() throws IllegalAccessException {
return (T) field.get(target);
}
}
/**
* Iterator to piggyback the list of Resources onto a Resource Mock
* so that we can add children to them and create the iterators after
* everything is setup
*/
static interface ResourceChildrenAccessor {
public List<Resource> getChildrenList();
}
/**
* Defines the Result of the Etc Mapping for easy testing
*/
public static class ExpectedEtcMapping {
List<ExpectedEtcMapEntry> expectedEtcMapEntries = new ArrayList<>();
public ExpectedEtcMapping() {}
public ExpectedEtcMapping(String...expectedMapping) {
if(expectedMapping.length % 2 != 0) {
throw new IllegalArgumentException("Expect an even number of strings with pattern / redirect");
}
int size = expectedMapping.length / 2;
for(int i = 0; i < size; i++ ) {
expectedEtcMapEntries.add(new ExpectedEtcMapEntry(expectedMapping[2 * i], expectedMapping[2 * i + 1]));
}
}
public ExpectedEtcMapping addEtcMapEntry(String pattern, String redirect) {
addEtcMapEntry(pattern, false, redirect);
return this;
}
public ExpectedEtcMapping addEtcMapEntry(String pattern, boolean internal, String redirect) {
expectedEtcMapEntries.add(new ExpectedEtcMapEntry(pattern, internal, redirect));
return this;
}
public void assertEtcMap(String title, List<MapEntry> mapEntries) {
assertEquals("Wrong Number of Mappings for: " + title, expectedEtcMapEntries.size(), mapEntries.size());
ArrayList<MapEntry> actual = new ArrayList<>(mapEntries);
ArrayList<ExpectedEtcMapEntry> expected = new ArrayList<>(expectedEtcMapEntries);
for(MapEntry actualMapEntry: actual) {
ExpectedEtcMapEntry expectedFound = null;
for(ExpectedEtcMapEntry expectedEtcMapEntry: expected) {
if(expectedEtcMapEntry.pattern.equals(actualMapEntry.getPattern())) {
expectedFound = expectedEtcMapEntry;
break;
}
}
if(expectedFound == null) {
TestCase.fail("This pattern (" + actualMapEntry.getPattern() + ") is not expected for: " + title);
}
expectedFound.assertEtcMap(title, actualMapEntry);
expected.remove(expectedFound);
}
for(ExpectedEtcMapEntry expectedEtcMapEntry: expected) {
TestCase.fail("Expected Map Entry (" + expectedEtcMapEntry.pattern + ") not provided for: " + title);
}
}
}
public static class ExpectedEtcMapEntry {
private String pattern;
private boolean internal;
private String redirect;
public ExpectedEtcMapEntry(String pattern, String redirect) {
this(pattern, false, redirect);
}
public ExpectedEtcMapEntry(String pattern, boolean internal, String redirect) {
this.pattern = pattern;
this.internal = internal;
this.redirect = redirect;
}
public void assertEtcMap(String title, MapEntry mapEntry) {
assertEquals("Wrong Pattern for " + title, pattern, mapEntry.getPattern());
List<String> givenRedirects = new ArrayList<>(Arrays.asList(mapEntry.getRedirect()));
assertEquals("Wrong Number of Redirects for: " + title, 1, givenRedirects.size());
assertEquals("Wrong Redirect for: " + title, this.redirect, givenRedirects.get(0));
assertEquals("Wrong Redirect Type (ext/int) for: " + title, this.internal, mapEntry.isInternal());
}
}
public static PathToUriMappingService createPathToUriMappingServiceMock(final ResourceResolver rr) {
return new TestPathToUriMappingService();
}
}