From 311add227036f886dc4636235197496979804a8e Mon Sep 17 00:00:00 2001 From: Luke Taylor Date: Wed, 30 Jan 2008 16:15:02 +0000 Subject: [PATCH] SEC-300: Applied Andreas Senft's patch for unwrapping exceptions in ExceptionTranslationFilter to obtain the cause. --- .../security/SpringSecurityException.java | 4 +- .../ui/ExceptionTranslationFilter.java | 77 +++-- .../security/util/ThrowableAnalyzer.java | 270 +++++++++++++++++ .../util/ThrowableCauseExtractor.java | 25 ++ .../ui/ExceptionTranslationFilterTests.java | 14 +- .../security/util/ThrowableAnalyzerTests.java | 274 ++++++++++++++++++ 6 files changed, 629 insertions(+), 35 deletions(-) create mode 100755 core/src/main/java/org/springframework/security/util/ThrowableAnalyzer.java create mode 100755 core/src/main/java/org/springframework/security/util/ThrowableCauseExtractor.java create mode 100644 core/src/test/java/org/springframework/security/util/ThrowableAnalyzerTests.java diff --git a/core/src/main/java/org/springframework/security/SpringSecurityException.java b/core/src/main/java/org/springframework/security/SpringSecurityException.java index 2160ebe79e..56a5e7c1d3 100644 --- a/core/src/main/java/org/springframework/security/SpringSecurityException.java +++ b/core/src/main/java/org/springframework/security/SpringSecurityException.java @@ -28,7 +28,7 @@ import org.springframework.core.NestedRuntimeException; public abstract class SpringSecurityException extends NestedRuntimeException { //~ Constructors =================================================================================================== -/** + /** * Constructs an SpringSecurityException with the specified * message and root cause. * @@ -39,7 +39,7 @@ public abstract class SpringSecurityException extends NestedRuntimeException { super(msg, t); } -/** + /** * Constructs an SpringSecurityException with the specified * message and no root cause. * diff --git a/core/src/main/java/org/springframework/security/ui/ExceptionTranslationFilter.java b/core/src/main/java/org/springframework/security/ui/ExceptionTranslationFilter.java index 9ce835bea4..711a5de4ed 100644 --- a/core/src/main/java/org/springframework/security/ui/ExceptionTranslationFilter.java +++ b/core/src/main/java/org/springframework/security/ui/ExceptionTranslationFilter.java @@ -25,6 +25,8 @@ import org.springframework.security.context.SecurityContextHolder; import org.springframework.security.ui.savedrequest.SavedRequest; import org.springframework.security.util.PortResolver; import org.springframework.security.util.PortResolverImpl; +import org.springframework.security.util.ThrowableAnalyzer; +import org.springframework.security.util.ThrowableCauseExtractor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -92,7 +94,8 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements private AuthenticationEntryPoint authenticationEntryPoint; private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl(); private PortResolver portResolver = new PortResolverImpl(); - private boolean createSessionAllowed = true; + private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer(); + private boolean createSessionAllowed = true; //~ Methods ======================================================================================================== @@ -100,7 +103,8 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint must be specified"); Assert.notNull(portResolver, "portResolver must be specified"); Assert.notNull(authenticationTrustResolver, "authenticationTrustResolver must be specified"); - } + Assert.notNull(throwableAnalyzer, "throwableAnalyzer must be specified"); + } public void doFilterHttp(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { @@ -112,24 +116,31 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements logger.debug("Chain processed normally"); } } - catch (AuthenticationException ex) { - handleException(request, response, chain, ex); - } - catch (AccessDeniedException ex) { - handleException(request, response, chain, ex); - } - catch (ServletException ex) { - if (ex.getRootCause() instanceof AuthenticationException - || ex.getRootCause() instanceof AccessDeniedException) { - handleException(request, response, chain, (SpringSecurityException) ex.getRootCause()); - } - else { - throw ex; - } - } - catch (IOException ex) { - throw ex; - } + catch (IOException ex) { + throw ex; + } + catch (Exception ex) { + // Try to extract a SpringSecurityException from the stacktrace + Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(ex); + SpringSecurityException ase = (SpringSecurityException) + this.throwableAnalyzer.getFirstThrowableOfType(SpringSecurityException.class, causeChain); + + if (ase != null) { + handleException(request, response, chain, ase); + } + else { + // Rethrow ServletExceptions and RuntimeExceptions as-is + if (ex instanceof ServletException) { + throw (ServletException) ex; + } + else if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + + // Wrap other Exceptions. These are not expected to happen + throw new RuntimeException(ex); + } + } } public AuthenticationEntryPoint getAuthenticationEntryPoint() { @@ -235,7 +246,33 @@ public class ExceptionTranslationFilter extends SpringSecurityFilter implements this.portResolver = portResolver; } + public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) { + this.throwableAnalyzer = throwableAnalyzer; + } + public int getOrder() { return FilterChainOrder.EXCEPTION_TRANSLATION_FILTER; } + + /** + * Default implementation of ThrowableAnalyzer which is capable of also unwrapping + * ServletExceptions. + */ + private static final class DefaultThrowableAnalyzer extends ThrowableAnalyzer { + /** + * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap() + */ + protected void initExtractorMap() { + super.initExtractorMap(); + + registerExtractor(ServletException.class, new ThrowableCauseExtractor() { + public Throwable extractCause(Throwable throwable) { + ThrowableAnalyzer.verifyThrowableHierarchy(throwable, ServletException.class); + return ((ServletException) throwable).getRootCause(); + } + }); + } + + } + } diff --git a/core/src/main/java/org/springframework/security/util/ThrowableAnalyzer.java b/core/src/main/java/org/springframework/security/util/ThrowableAnalyzer.java new file mode 100755 index 0000000000..fab60bc62d --- /dev/null +++ b/core/src/main/java/org/springframework/security/util/ThrowableAnalyzer.java @@ -0,0 +1,270 @@ +package org.springframework.security.util; + +import java.lang.reflect.InvocationTargetException; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Handler for analyzing {@link Throwable} instances. + * + * Can be subclassed to customize its behavior. + * + * @author Andreas Senft + * @since 2.0 + * @version $Id$ + */ +public class ThrowableAnalyzer { + + /** + * Default extractor for {@link Throwable} instances. + * + * @see Throwable#getCause() + */ + public static final ThrowableCauseExtractor DEFAULT_EXTRACTOR + = new ThrowableCauseExtractor() { + public Throwable extractCause(Throwable throwable) { + return throwable.getCause(); + } + }; + + /** + * Default extractor for {@link InvocationTargetException} instances. + * + * @see InvocationTargetException#getTargetException() + */ + public static final ThrowableCauseExtractor INVOCATIONTARGET_EXTRACTOR + = new ThrowableCauseExtractor() { + public Throwable extractCause(Throwable throwable) { + verifyThrowableHierarchy(throwable, InvocationTargetException.class); + return ((InvocationTargetException) throwable).getTargetException(); + } + }; + + /** + * Comparator to order classes ascending according to their hierarchy relation. + * If two classes have a hierarchical relation, the "higher" class is considered + * to be greater by this comparator.
+ * For hierarchically unrelated classes their fully qualified name will be compared. + */ + private static final Comparator CLASS_HIERARCHY_COMPARATOR = new Comparator() { + + public int compare(Object o1, Object o2) { + Class class1 = (Class) o1; + Class class2 = (Class) o2; + + if (class1.isAssignableFrom(class2)) { + return 1; + } else if (class2.isAssignableFrom(class1)) { + return -1; + } else { + return class1.getName().compareTo(class2.getName()); + } + } + + }; + + + /** + * Map of registered cause extractors. + * key: Class; value: ThrowableCauseExctractor + */ + private final Map extractorMap; + + + /** + * Creates a new ThrowableAnalyzer instance. + */ + public ThrowableAnalyzer() { + this.extractorMap = new TreeMap(CLASS_HIERARCHY_COMPARATOR); + + initExtractorMap(); + } + + /** + * Registers a ThrowableCauseExtractor for the specified type. + * Can be used in subclasses overriding {@link #initExtractorMap()}. + * + * @param throwableType the type (has to be a subclass of Throwable) + * @param extractor the associated ThrowableCauseExtractor (not null) + * + * @throws IllegalArgumentException if one of the arguments is invalid + */ + protected final void registerExtractor(Class throwableType, ThrowableCauseExtractor extractor) { + verifyThrowableType(throwableType); + + if (extractor == null) { + throw new IllegalArgumentException("Invalid extractor: null"); + } + + this.extractorMap.put(throwableType, extractor); + } + + /** + * Initializes associations between Throwables and ThrowableCauseExtractors. + * The default implementation performs the following registrations: + *
  • {@link #DEFAULT_EXTRACTOR} for {@link Throwable}
  • + *
  • {@link #INVOCATIONTARGET_EXTRACTOR} for {@link InvocationTargetException}
  • + *
    + * Subclasses overriding this method are encouraged to invoke the super method to perform the + * default registrations. They can register additional extractors as required. + *

    + * Note: An extractor registered for a specific type is applicable for that type and all subtypes thereof. + * However, extractors registered to more specific types are guaranteed to be resolved first. + * So in the default case InvocationTargetExceptions will be handled by {@link #INVOCATIONTARGET_EXTRACTOR} + * while all other throwables are handled by {@link #DEFAULT_EXTRACTOR}. + * + * @see #registerExtractor(Class, ThrowableCauseExtractor) + */ + protected void initExtractorMap() { + registerExtractor(InvocationTargetException.class, INVOCATIONTARGET_EXTRACTOR); + registerExtractor(Throwable.class, DEFAULT_EXTRACTOR); + } + + /** + * Returns an array containing the classes for which extractors are registered. + * The order of the classes is the order in which comparisons will occur for + * resolving a matching extractor. + * + * @return the types for which extractors are registered + */ + final Class[] getRegisteredTypes() { + List typeList = new ArrayList(this.extractorMap.keySet()); + return (Class[]) typeList.toArray(new Class[typeList.size()]); + } + + /** + * Determines the cause chain of the provided Throwable. + * The returned array contains all throwables extracted from the stacktrace, using the registered + * {@link ThrowableCauseExtractor extractors}. The elements of the array are ordered: + * The first element is the passed in throwable itself. The following elements + * appear in their order downward the stacktrace. + *

    + * Note: If no {@link ThrowableCauseExtractor} is registered for this instance + * then the returned array will always only contain the passed in throwable. + * + * @param throwable the Throwable to analyze + * @return an array of all determined throwables from the stacktrace + * + * @throws IllegalArgumentException if the throwable is null + * + * @see #initExtractorMap() + */ + public final Throwable[] determineCauseChain(Throwable throwable) { + if (throwable == null) { + throw new IllegalArgumentException("Invalid throwable: null"); + } + + List chain = new ArrayList(); + Throwable currentThrowable = throwable; + + while (currentThrowable != null) { + chain.add(currentThrowable); + currentThrowable = extractCause(currentThrowable); + } + + return (Throwable[]) chain.toArray(new Throwable[chain.size()]); + } + + /** + * Extracts the cause of the given throwable using an appropriate extractor. + * + * @param throwable the Throwable (not null + * @return the cause, may be null if none could be resolved + */ + private Throwable extractCause(Throwable throwable) { + for (Iterator iter = this.extractorMap.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry entry = (Map.Entry) iter.next(); + + Class throwableType = (Class) entry.getKey(); + if (throwableType.isInstance(throwable)) { + ThrowableCauseExtractor extractor = (ThrowableCauseExtractor) entry.getValue(); + return extractor.extractCause(throwable); + } + } + + return null; + } + + /** + * Returns the first throwable from the passed in array that is assignable to the provided type. + * A returned instance is safe to be cast to the specified type. + *

    + * If the passed in array is null or empty this method returns null. + * + * @param throwableType the type to look for + * @param chain the array (will be processed in element order) + * @return the found Throwable, null if not found + * + * @throws IllegalArgumentException if the provided type is null + * or no subclass of Throwable + */ + public final Throwable getFirstThrowableOfType(Class throwableType, Throwable[] chain) { + verifyThrowableType(throwableType); + + if (chain != null) { + for (int i = 0; i < chain.length; ++i) { + Throwable t = chain[i]; + + if ((t != null) && throwableType.isInstance(t)) { + return t; + } + } + } + + return null; + } + + /** + * Convenience method for verifying that the passed in class refers to a valid + * subclass of Throwable. + * + * @param throwableType the type to check + * + * @throws IllegalArgumentException if typeToCheck is either null + * or not assignable to expectedBaseType + */ + private static void verifyThrowableType(Class throwableType) { + if (throwableType == null) { + throw new IllegalArgumentException("Invalid type: null"); + } + if (!Throwable.class.isAssignableFrom(throwableType)) { + throw new IllegalArgumentException("Invalid type: '" + + throwableType.getName() + + "'. Has to be a subclass of '" + Throwable.class.getName() + "'"); + } + } + + /** + * Verifies that the provided throwable is a valid subclass of the provided type (or of the type itself). + * If expectdBaseType is null, no check will be performed. + *

    + * Can be used for verification purposes in implementations + * of {@link ThrowableCauseExtractor extractors}. + * + * @param throwable the Throwable to check + * @param expectedBaseType the type to check against + * + * @throws IllegalArgumentException if throwable is either null + * or its type is not assignable to expectedBaseType + */ + public static final void verifyThrowableHierarchy(Throwable throwable, Class expectedBaseType) { + if (expectedBaseType == null) { + return; + } + + if (throwable == null) { + throw new IllegalArgumentException("Invalid throwable: null"); + } + Class throwableType = throwable.getClass(); + + if (!expectedBaseType.isAssignableFrom(throwableType)) { + throw new IllegalArgumentException("Invalid type: '" + + throwableType.getName() + + "'. Has to be a subclass of '" + expectedBaseType.getName() + "'"); + } + } +} diff --git a/core/src/main/java/org/springframework/security/util/ThrowableCauseExtractor.java b/core/src/main/java/org/springframework/security/util/ThrowableCauseExtractor.java new file mode 100755 index 0000000000..3ac13ecb7f --- /dev/null +++ b/core/src/main/java/org/springframework/security/util/ThrowableCauseExtractor.java @@ -0,0 +1,25 @@ +package org.springframework.security.util; + + +/** + * Interface for handlers extracting the cause out of a specific {@link Throwable} type. + * + * @author Andreas Senft + * @since 2.0 + * @version $Id$ + * + * @see ThrowableAnalyzer + */ +public interface ThrowableCauseExtractor { + + /** + * Extracts the cause from the provided Throwable. + * + * @param throwable the Throwable + * @return the extracted cause (maybe null) + * + * @throws IllegalArgumentException if throwable is null + * or otherwise considered invalid for the implementation + */ + Throwable extractCause(Throwable throwable); +} diff --git a/core/src/test/java/org/springframework/security/ui/ExceptionTranslationFilterTests.java b/core/src/test/java/org/springframework/security/ui/ExceptionTranslationFilterTests.java index 8f3279f5a2..f8404ebbb7 100644 --- a/core/src/test/java/org/springframework/security/ui/ExceptionTranslationFilterTests.java +++ b/core/src/test/java/org/springframework/security/ui/ExceptionTranslationFilterTests.java @@ -46,19 +46,8 @@ import javax.servlet.ServletResponse; * benalex $ */ public class ExceptionTranslationFilterTests extends TestCase { - //~ Constructors =================================================================================================== - - public ExceptionTranslationFilterTests() { - super(); - } - - public ExceptionTranslationFilterTests(String arg0) { - super(arg0); - } - //~ Methods ======================================================================================================== - protected void tearDown() throws Exception { super.tearDown(); SecurityContextHolder.clearContext(); @@ -312,8 +301,7 @@ public class ExceptionTranslationFilterTests extends TestCase { } } - // ~ Inner Classes - // ================================================================================================== + // ~ Inner Classes ================================================================================================= private class MockFilterChain implements FilterChain { private boolean throwAccessDenied; diff --git a/core/src/test/java/org/springframework/security/util/ThrowableAnalyzerTests.java b/core/src/test/java/org/springframework/security/util/ThrowableAnalyzerTests.java new file mode 100644 index 0000000000..0392638483 --- /dev/null +++ b/core/src/test/java/org/springframework/security/util/ThrowableAnalyzerTests.java @@ -0,0 +1,274 @@ +package org.springframework.security.util; + +import java.lang.reflect.InvocationTargetException; + +import junit.framework.TestCase; + +/** + * Testcases for {@link ThrowableAnalyzer}. + * + * @author Andreas Senft + */ +public class ThrowableAnalyzerTests extends TestCase { + + /** + * Exception for testing purposes. The cause is not retrievable by {@link #getCause()}. + */ + public static final class NonStandardException extends Exception { + + private Throwable cause; + + public NonStandardException(String message, Throwable cause) { + super(message); + this.cause = cause; + } + + public Throwable resolveCause() { + return this.cause; + } + } + + /** + * ThrowableCauseExtractor for handling NonStandardException instances. + */ + public static final class NonStandardExceptionCauseExtractor implements ThrowableCauseExtractor { + + public Throwable extractCause(Throwable throwable) { + ThrowableAnalyzer.verifyThrowableHierarchy(throwable, NonStandardException.class); + return ((NonStandardException) throwable).resolveCause(); + } + + } + + + /** + * An array of nested throwables for testing. + * The cause of element 0 is element 1, the cause of element 1 is element 2 and so on. + */ + private Throwable[] testTrace; + + /** + * Plain ThrowableAnalyzer. + */ + private ThrowableAnalyzer standardAnalyzer; + + /** + * Enhanced ThrowableAnalyzer capable to process + * NonStandardExceptions. + */ + private ThrowableAnalyzer nonstandardAnalyzer; + + /** + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception { + super.setUp(); + + // Set up test trace + this.testTrace = new Throwable[7]; + this.testTrace[6] = new IllegalArgumentException("Test_6"); + this.testTrace[5] = new Throwable("Test_5",this.testTrace[6]); + this.testTrace[4] = new InvocationTargetException(this.testTrace[5], "Test_4"); + this.testTrace[3] = new Exception("Test_3", this.testTrace[4]); + this.testTrace[2] = new NonStandardException("Test_2", this.testTrace[3]); + this.testTrace[1] = new RuntimeException("Test_1", this.testTrace[2]); + this.testTrace[0] = new Exception("Test_0", this.testTrace[1]); + + // Set up standard analyzer + this.standardAnalyzer = new ThrowableAnalyzer(); + + // Set up nonstandard analyzer + this.nonstandardAnalyzer = new ThrowableAnalyzer() { + /** + * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap() + */ + @Override + protected void initExtractorMap() { + super.initExtractorMap(); + // register extractor for NonStandardException + registerExtractor(NonStandardException.class, new NonStandardExceptionCauseExtractor()); + } + }; + } + + + /** + * @see junit.framework.TestCase#tearDown() + */ + @Override + protected void tearDown() throws Exception { + super.tearDown(); + } + + + public void testRegisterExtractorWithInvalidClass() { + try { + new ThrowableAnalyzer() { + + /** + * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap() + */ + @Override + protected void initExtractorMap() { + // Object is no subclass of Throwable + super.registerExtractor(Object.class, DEFAULT_EXTRACTOR); + } + }; + + fail("IllegalArgumentExpected"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testRegisterExtractorWithInvalidExtractor() { + try { + new ThrowableAnalyzer() { + + /** + * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap() + */ + @Override + protected void initExtractorMap() { + // null is no valid extractor + super.registerExtractor(Exception.class, null); + } + }; + + fail("IllegalArgumentExpected"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testGetRegisteredTypes() { + + Class[] registeredTypes = this.nonstandardAnalyzer.getRegisteredTypes(); + + for (int i = 0; i < registeredTypes.length; ++i) { + Class clazz = registeredTypes[i]; + + // The most specific types have to occur first. + for (int j = 0; j < i; ++j) { + Class prevClazz = registeredTypes[j]; + + assertFalse("Unexpected order of registered classes: " + + prevClazz + " is assignable from " + clazz, prevClazz.isAssignableFrom(clazz)); + } + } + } + + public void testDetermineCauseChainWithNoExtractors() { + ThrowableAnalyzer analyzer = new ThrowableAnalyzer() { + + /** + * @see org.springframework.security.util.ThrowableAnalyzer#initExtractorMap() + */ + @Override + protected void initExtractorMap() { + // skip default initialization + } + }; + + assertEquals("Unexpected number of registered types", 0, analyzer.getRegisteredTypes().length); + + Throwable t = this.testTrace[0]; + Throwable[] chain = analyzer.determineCauseChain(t); + // Without extractors only the root throwable is available + assertEquals("Unexpected chain size", 1, chain.length); + assertEquals("Unexpected chain entry", t, chain[0]); + } + + public void testDetermineCauseChainWithDefaultExtractors() { + ThrowableAnalyzer analyzer = this.standardAnalyzer; + + assertEquals("Unexpected number of registered types", 2, analyzer.getRegisteredTypes().length); + + Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]); + + // Element at index 2 is a NonStandardException which cannot be analyzed further by default + assertEquals("Unexpected chain size", 3, chain.length); + for (int i = 0; i < 3; ++i) { + assertEquals("Unexpected chain entry: " + i, this.testTrace[i], chain[i]); + } + } + + public void testDetermineCauseChainWithCustomExtractors() { + ThrowableAnalyzer analyzer = this.nonstandardAnalyzer; + + Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]); + + assertEquals("Unexpected chain size", this.testTrace.length, chain.length); + for (int i = 0; i < chain.length; ++i) { + assertEquals("Unexpected chain entry: " + i, this.testTrace[i], chain[i]); + } + } + + public void testGetFirstThrowableOfTypeWithSuccess1() { + ThrowableAnalyzer analyzer = this.nonstandardAnalyzer; + + Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]); + + Throwable result = analyzer.getFirstThrowableOfType(Exception.class, chain); + + assertNotNull("null not expected", result); + assertEquals("Unexpected throwable found", this.testTrace[0], result); + } + + public void testGetFirstThrowableOfTypeWithSuccess2() { + ThrowableAnalyzer analyzer = this.nonstandardAnalyzer; + + Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]); + + Throwable result = analyzer.getFirstThrowableOfType(NonStandardException.class, chain); + + assertNotNull("null not expected", result); + assertEquals("Unexpected throwable found", this.testTrace[2], result); + } + + public void testGetFirstThrowableOfTypeWithFailure() { + ThrowableAnalyzer analyzer = this.nonstandardAnalyzer; + + Throwable[] chain = analyzer.determineCauseChain(this.testTrace[0]); + + // IllegalStateException not in trace + Throwable result = analyzer.getFirstThrowableOfType(IllegalStateException.class, chain); + + assertNull("null expected", result); + } + + public void testVerifyThrowableHierarchyWithExactType() { + + Throwable throwable = new IllegalStateException("Test"); + ThrowableAnalyzer.verifyThrowableHierarchy(throwable, IllegalStateException.class); + // No exception expected + } + + public void testVerifyThrowableHierarchyWithCompatibleType() { + + Throwable throwable = new IllegalStateException("Test"); + ThrowableAnalyzer.verifyThrowableHierarchy(throwable, Exception.class); + // No exception expected + } + + public void testVerifyThrowableHierarchyWithNull() { + try { + ThrowableAnalyzer.verifyThrowableHierarchy(null, Throwable.class); + fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + // ok + } + } + + public void testVerifyThrowableHierarchyWithNonmatchingType() { + + Throwable throwable = new IllegalStateException("Test"); + try { + ThrowableAnalyzer.verifyThrowableHierarchy(throwable, InvocationTargetException.class); + fail("IllegalArgumentException expected"); + } catch (IllegalArgumentException e) { + // ok + } + } +}