View Javadoc
1   /*
2    * Copyright 2019 the original author or authors.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.bremersee.test.web;
18  
19  import static java.lang.String.format;
20  import static org.bremersee.test.web.RestApiAssertionType.ANNOTATION_MUST_NOT_BE_NULL;
21  import static org.bremersee.test.web.RestApiAssertionType.CLASS_MUST_BE_INTERFACE;
22  import static org.bremersee.test.web.RestApiAssertionType.METHOD_MUST_NOT_BE_NULL;
23  import static org.bremersee.test.web.RestApiAssertionType.SAME_ANNOTATION_ATTRIBUTES_SIZE;
24  import static org.bremersee.test.web.RestApiAssertionType.SAME_ANNOTATION_ATTRIBUTE_VALUE;
25  import static org.bremersee.test.web.RestApiAssertionType.SAME_ANNOTATION_SIZE;
26  import static org.bremersee.test.web.RestApiAssertionType.SAME_METHOD_SIZE;
27  import static org.bremersee.test.web.RestApiTesterExclusion.isExcluded;
28  import static org.bremersee.test.web.RestApiTesterPath.PathType.ANNOTATION;
29  import static org.bremersee.test.web.RestApiTesterPath.PathType.ATTRIBUTE;
30  import static org.bremersee.test.web.RestApiTesterPath.PathType.CLASS;
31  import static org.bremersee.test.web.RestApiTesterPath.PathType.METHOD;
32  import static org.bremersee.test.web.RestApiTesterPath.PathType.METHOD_PARAMETER;
33  import static org.bremersee.test.web.RestApiTesterPath.pathBuilder;
34  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
35  import static org.junit.jupiter.api.Assertions.assertEquals;
36  import static org.junit.jupiter.api.Assertions.assertNotNull;
37  import static org.junit.jupiter.api.Assertions.assertNull;
38  import static org.junit.jupiter.api.Assertions.assertTrue;
39  
40  import java.lang.annotation.Annotation;
41  import java.lang.reflect.AnnotatedElement;
42  import java.lang.reflect.Method;
43  import java.lang.reflect.Parameter;
44  import java.util.Arrays;
45  import java.util.Map;
46  import java.util.stream.Collectors;
47  import org.slf4j.Logger;
48  import org.slf4j.LoggerFactory;
49  import org.springframework.core.annotation.AnnotationUtils;
50  import org.springframework.util.ReflectionUtils;
51  import org.springframework.util.StringUtils;
52  
53  /**
54   * The rest api tester.
55   *
56   * @author Christian Bremer
57   */
58  public class RestApiTester {
59  
60    private static final Logger log = LoggerFactory.getLogger(RestApiTester.class);
61  
62    private RestApiTester() {
63    }
64  
65    /**
66     * Assert same api.
67     *
68     * @param expected the expected
69     * @param actual the actual
70     * @param exclusions the exclusions
71     */
72    public static void assertSameApi(
73        final Class<?> expected,
74        final Class<?> actual,
75        final RestApiTesterExclusion... exclusions) {
76  
77      log.info("Assert same api: expected = {}, actual = {}", name(expected),
78          name(actual));
79      assertNotNull(expected, "Expected api class must not be null.");
80      assertNotNull(actual, "Actual api class must not be null.");
81  
82      RestApiTesterPath path = pathBuilder().add(CLASS, expected.getSimpleName()).build();
83      if (!isExcluded(path, CLASS_MUST_BE_INTERFACE, exclusions)) {
84        log.info("Assert given class is an interface:"
85                + "\n  - path = {}"
86                + "\n  - type = {}",
87            path, CLASS_MUST_BE_INTERFACE);
88        assertTrue(expected.isInterface(), "Expected api class must be an interface.");
89      }
90  
91      path = pathBuilder().add(CLASS, actual.getSimpleName()).build();
92      if (!isExcluded(path, CLASS_MUST_BE_INTERFACE, exclusions)) {
93        log.info("Assert given class is an interface:"
94                + "\n  - path = {}"
95                + "\n  - type = {}",
96            path, CLASS_MUST_BE_INTERFACE);
97        assertTrue(actual.isInterface(), "Actual api class must be an interface.");
98      }
99  
100     assertSameClassAnnotations(expected, actual, exclusions);
101     assertSameMethodAnnotations(expected, actual, exclusions);
102   }
103 
104   private static void assertSameClassAnnotations(
105       final Class<?> expected,
106       final Class<?> actual,
107       final RestApiTesterExclusion... exclusions) {
108 
109     final Annotation[] expectedAnnotations = expected.getAnnotations();
110     final Annotation[] actualAnnotations = actual.getAnnotations();
111     assertSameAnnotations(
112         pathBuilder().add(CLASS, actual.getSimpleName()).build(),
113         expectedAnnotations,
114         actualAnnotations,
115         actual,
116         exclusions);
117   }
118 
119   private static void assertSameMethodAnnotations(
120       final Class<?> expected,
121       final Class<?> actual,
122       final RestApiTesterExclusion... exclusions) {
123 
124     final RestApiTesterPath classPath = pathBuilder().add(CLASS, actual.getSimpleName()).build();
125     final Method[] expectedMethods = ReflectionUtils.getDeclaredMethods(expected);
126     final Method[] actualMethods = ReflectionUtils.getDeclaredMethods(actual);
127 
128     if (!isExcluded(classPath, SAME_METHOD_SIZE, exclusions)) {
129       log.info("Assert same method size:"
130               + "\n  - path = {}"
131               + "\n  - type = {}",
132           classPath, SAME_METHOD_SIZE);
133       assertEquals(
134           expectedMethods.length,
135           actualMethods.length,
136           format("Methods must have the same size on %s and %s.",
137               expected.getSimpleName(), actual.getSimpleName()));
138     }
139 
140     for (final Method expectedMethod : expectedMethods) {
141       final RestApiTesterPath methodPath = classPath.toPathBuilder()
142           .add(METHOD, expectedMethod.getName())
143           .build();
144       final Method actualMethod = ReflectionUtils.findMethod(
145           actual, expectedMethod.getName(), expectedMethod.getParameterTypes());
146       if (!isExcluded(methodPath, METHOD_MUST_NOT_BE_NULL, exclusions)) {
147         log.info("Assert method exists:"
148                 + "\n  - path = {}"
149                 + "\n  - type = {}",
150             methodPath, METHOD_MUST_NOT_BE_NULL);
151         assertNotNull(
152             actualMethod,
153             format("Method %s (%s) is missing on %s", expectedMethod.getName(),
154                 parameters(expectedMethod.getParameterTypes()), name(actual)));
155       } else if (actualMethod == null) {
156         continue;
157       }
158 
159       final Annotation[] expectedAnnotations = expectedMethod.getAnnotations();
160       final Annotation[] actualAnnotations = actualMethod.getAnnotations();
161       assertSameAnnotations(
162           methodPath,
163           expectedAnnotations,
164           actualAnnotations,
165           actualMethod,
166           exclusions);
167 
168       if (expectedMethod.getParameterCount() > 0) {
169         final Parameter[] expectedParameters = expectedMethod.getParameters();
170         final Parameter[] actualParameters = actualMethod.getParameters();
171         for (int i = 0; i < expectedParameters.length; i++) {
172           final Parameter expectedParameter = expectedParameters[i];
173           final Parameter actualParameter = actualParameters[i];
174           final RestApiTesterPath methodParameterPath = methodPath.toPathBuilder()
175               .add(METHOD_PARAMETER, String.valueOf(i))
176               .build();
177           assertSameAnnotations(
178               methodParameterPath,
179               expectedParameter.getAnnotations(),
180               actualParameter.getAnnotations(),
181               actualParameter,
182               exclusions);
183         }
184       }
185     }
186   }
187 
188   private static void assertSameAnnotations(
189       final RestApiTesterPath path,
190       final Annotation[] expectedAnnotations,
191       final Annotation[] actualAnnotations,
192       final AnnotatedElement actualAnnotatedElement,
193       final RestApiTesterExclusion... exclusions) {
194 
195     if (!isExcluded(path, SAME_ANNOTATION_SIZE, exclusions)) {
196       log.info("Assert same annotation size:"
197               + "\n  - path = {}"
198               + "\n  - type = {}",
199           path, SAME_ANNOTATION_SIZE);
200       assertEquals(
201           expectedAnnotations.length,
202           actualAnnotations.length,
203           format("Annotations must have the same size on %s.", path));
204     }
205     for (final Annotation expectedAnnotation : expectedAnnotations) {
206       final Annotation actualAnnotation = AnnotationUtils.getAnnotation(
207           actualAnnotatedElement, expectedAnnotation.annotationType());
208       assertSameAnnotation(
209           path,
210           expectedAnnotation,
211           actualAnnotation,
212           exclusions);
213     }
214   }
215 
216   private static void assertSameAnnotation(
217       final RestApiTesterPath path,
218       final Annotation expected,
219       final Annotation actual,
220       final RestApiTesterExclusion... exclusions) {
221 
222     final RestApiTesterPath annotationPath = path.toPathBuilder()
223         .add(ANNOTATION, expected.annotationType().getSimpleName())
224         .build();
225     if (!isExcluded(annotationPath, ANNOTATION_MUST_NOT_BE_NULL, exclusions)) {
226       log.info("Assert annotation is not null:"
227               + "\n  - path = {}"
228               + "\n  - type = {}",
229           annotationPath, ANNOTATION_MUST_NOT_BE_NULL);
230       assertNotNull(
231           actual,
232           format("Annotation %s is missing on %s.", name(expected), path));
233     }
234     if (actual == null) {
235       return;
236     }
237     final Map<String, Object> expectedAttributes = AnnotationUtils.getAnnotationAttributes(
238         expected, true, false);
239     final Map<String, Object> actualAttributes = AnnotationUtils.getAnnotationAttributes(
240         actual, true, false);
241     assertSameAttributes(
242         annotationPath,
243         expectedAttributes,
244         actualAttributes,
245         exclusions);
246   }
247 
248   private static void assertSameAttributes(
249       final RestApiTesterPath annotationPath,
250       final Map<String, Object> expected,
251       final Map<String, Object> actual,
252       final RestApiTesterExclusion... exclusions) {
253 
254     if (!isExcluded(annotationPath, SAME_ANNOTATION_ATTRIBUTES_SIZE, exclusions)) {
255       log.info("Assert same annotation attributes size:"
256               + "\n  - path = {}"
257               + "\n  - type = {}",
258           annotationPath, SAME_ANNOTATION_ATTRIBUTES_SIZE);
259       assertEquals(
260           expected.size(),
261           actual.size(),
262           format("Attributes of annotation (%s) must have the same size.", annotationPath));
263     }
264     for (final Map.Entry<String, Object> expectedAttribute : expected.entrySet()) {
265       final Object expectedAttributeValue = expectedAttribute.getValue();
266       final Object actualAttributeValue = actual.get(expectedAttribute.getKey());
267       assertSameAttributeValues(
268           annotationPath.toPathBuilder().add(ATTRIBUTE, expectedAttribute.getKey()).build(),
269           expectedAttributeValue,
270           actualAttributeValue,
271           exclusions);
272     }
273   }
274 
275   private static void assertSameAttributeValues(
276       final RestApiTesterPath attributePath,
277       final Object expected,
278       final Object actual,
279       final RestApiTesterExclusion... exclusions) {
280 
281     if (!isExcluded(attributePath, SAME_ANNOTATION_ATTRIBUTE_VALUE, exclusions)) {
282       log.info("Assert same attribute values:"
283               + "\n  - path     = {}"
284               + "\n  - expected = {}"
285               + "\n  - actual   = {}"
286               + "\n  - type     = {}",
287           attributePath, expected, actual, SAME_ANNOTATION_ATTRIBUTE_VALUE);
288 
289       if (expected == null) {
290         assertNull(
291             actual,
292             format("Attribute (%s) must be null.", attributePath));
293       } else if (expected instanceof Annotation[]) {
294         assertTrue(actual instanceof Annotation[]);
295         final Annotation[] expectedAnnotations = (Annotation[]) expected;
296         final Annotation[] actualAnnotations = (Annotation[]) actual;
297         assertEquals(expectedAnnotations.length, actualAnnotations.length);
298         for (int i = 0; i < actualAnnotations.length; i++) {
299           final Annotation expectedAnnotation = expectedAnnotations[i];
300           final Annotation actualAnnotation = actualAnnotations[i];
301           assertSameAnnotation(
302               attributePath,
303               expectedAnnotation,
304               actualAnnotation,
305               exclusions);
306         }
307       } else if (expected instanceof Annotation) {
308         final Annotation expectedAnnotation = (Annotation) expected;
309         final Annotation actualAnnotation = (Annotation) actual;
310         assertSameAnnotation(
311             attributePath,
312             expectedAnnotation,
313             actualAnnotation,
314             exclusions);
315       } else {
316         if (expected.getClass().isArray()) {
317           final Object[] expectedArray = (Object[]) expected;
318           final Object[] actualArray = (Object[]) actual;
319           assertArrayEquals(expectedArray, actualArray);
320         } else {
321           assertEquals(
322               expected, actual);
323         }
324       }
325     }
326   }
327 
328   private static String name(final Annotation annotation) {
329     return annotation != null ? name(annotation.annotationType()) : "null";
330   }
331 
332   private static String name(final Class<?> clazz) {
333     return clazz != null ? clazz.getName() : "null";
334   }
335 
336   private static String parameters(final Class<?>[] parameters) {
337     if (parameters == null || parameters.length == 0) {
338       return "";
339     }
340     return StringUtils.collectionToCommaDelimitedString(Arrays.stream(parameters)
341         .map(RestApiTester::name).collect(Collectors.toList()));
342   }
343 
344 }