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.spring.test.api.comparator;
18  
19  import static java.util.Objects.isNull;
20  import static java.util.Objects.nonNull;
21  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.ANNOTATION_MUST_NOT_BE_NULL;
22  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.CLASS_MUST_BE_INTERFACE;
23  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.METHOD_MUST_NOT_BE_NULL;
24  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_ANNOTATION_ATTRIBUTES_SIZE;
25  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_ANNOTATION_ATTRIBUTE_VALUE;
26  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_ANNOTATION_SIZE;
27  import static org.bremersee.spring.test.api.comparator.RestApiComparatorAssertionType.SAME_METHOD_SIZE;
28  import static org.bremersee.spring.test.api.comparator.RestApiComparatorExclusion.isExcluded;
29  import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.ANNOTATION;
30  import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.ATTRIBUTE;
31  import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.CLASS;
32  import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.METHOD;
33  import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.PathType.METHOD_PARAMETER;
34  import static org.bremersee.spring.test.api.comparator.RestApiComparatorPath.pathBuilder;
35  import static org.springframework.util.ObjectUtils.isEmpty;
36  
37  import java.lang.annotation.Annotation;
38  import java.lang.reflect.AnnotatedElement;
39  import java.lang.reflect.Method;
40  import java.lang.reflect.Parameter;
41  import java.util.Arrays;
42  import java.util.Map;
43  import java.util.stream.Collectors;
44  import org.assertj.core.api.SoftAssertions;
45  import org.slf4j.Logger;
46  import org.slf4j.LoggerFactory;
47  import org.springframework.core.annotation.AnnotationUtils;
48  import org.springframework.util.Assert;
49  import org.springframework.util.ReflectionUtils;
50  import org.springframework.util.StringUtils;
51  
52  /**
53   * The rest api tester.
54   *
55   * @author Christian Bremer
56   */
57  public class RestApiComparator {
58  
59    private static final Logger log = LoggerFactory.getLogger(RestApiComparator.class);
60  
61    private RestApiComparator() {
62    }
63  
64    /**
65     * Assert same api.
66     *
67     * @param expected the expected
68     * @param actual the actual
69     * @param exclusions the exclusions
70     */
71    public static void assertSameApi(
72        final Class<?> expected,
73        final Class<?> actual,
74        final RestApiComparatorExclusion... exclusions) {
75  
76      assertSameApi(null, true, expected, actual, exclusions);
77    }
78  
79    /**
80     * Assert same api.
81     *
82     * @param softAssertions the soft assertions
83     * @param assertAll the assert all
84     * @param expected the expected
85     * @param actual the actual
86     * @param exclusions the exclusions
87     */
88    public static void assertSameApi(
89        final SoftAssertions softAssertions,
90        final boolean assertAll,
91        final Class<?> expected,
92        final Class<?> actual,
93        final RestApiComparatorExclusion... exclusions) {
94  
95      log.info("Assert same api: expected = {}, actual = {}", name(expected),
96          name(actual));
97      Assert.notNull(expected, "Expected api class must not be null.");
98      Assert.notNull(actual, "Actual api class must not be null.");
99  
100     SoftAssertions softly;
101     boolean internalAssertAll;
102     if (nonNull(softAssertions)) {
103       softly = softAssertions;
104       internalAssertAll = assertAll;
105     } else {
106       softly = new SoftAssertions();
107       internalAssertAll = true;
108     }
109 
110     RestApiComparatorPath path = pathBuilder().add(CLASS, expected.getSimpleName()).build();
111     if (!isExcluded(path, CLASS_MUST_BE_INTERFACE, exclusions)) {
112       log.info("""
113               Assert given class is an interface:
114                 - path = {}
115                 - type = {}""",
116           path, CLASS_MUST_BE_INTERFACE);
117       softly.assertThat(expected.isInterface())
118           .as("Expected api class must be an interface.")
119           .isTrue();
120     }
121 
122     path = pathBuilder().add(CLASS, actual.getSimpleName()).build();
123     if (!isExcluded(path, CLASS_MUST_BE_INTERFACE, exclusions)) {
124       log.info("""
125               Assert given class is an interface:
126                 - path = {}
127                 - type = {}""",
128           path, CLASS_MUST_BE_INTERFACE);
129       softly.assertThat(actual.isInterface())
130           .as("Actual api class must be an interface.")
131           .isTrue();
132     }
133 
134     assertSameClassAnnotations(softly, expected, actual, exclusions);
135     assertSameMethodAnnotations(softly, expected, actual, exclusions);
136 
137     if (internalAssertAll) {
138       softly.assertAll();
139     }
140   }
141 
142   private static void assertSameClassAnnotations(
143       SoftAssertions softly,
144       final Class<?> expected,
145       final Class<?> actual,
146       final RestApiComparatorExclusion... exclusions) {
147 
148     final Annotation[] expectedAnnotations = expected.getAnnotations();
149     final Annotation[] actualAnnotations = actual.getAnnotations();
150     assertSameAnnotations(
151         softly,
152         pathBuilder().add(CLASS, actual.getSimpleName()).build(),
153         expectedAnnotations,
154         actualAnnotations,
155         actual,
156         exclusions);
157   }
158 
159   private static void assertSameMethodAnnotations(
160       SoftAssertions softly,
161       final Class<?> expected,
162       final Class<?> actual,
163       final RestApiComparatorExclusion... exclusions) {
164 
165     final RestApiComparatorPath classPath = pathBuilder().add(CLASS, actual.getSimpleName())
166         .build();
167     final Method[] expectedMethods = ReflectionUtils.getDeclaredMethods(expected);
168     final Method[] actualMethods = ReflectionUtils.getDeclaredMethods(actual);
169 
170     if (!isExcluded(classPath, SAME_METHOD_SIZE, exclusions)) {
171       log.info("""
172               Assert same method size:
173                 - path = {}
174                 - type = {}""",
175           classPath, SAME_METHOD_SIZE);
176       softly.assertThat(actualMethods.length)
177           .as("Methods must have the same size on %s and %s.",
178               expected.getSimpleName(), actual.getSimpleName())
179           .isEqualTo(expectedMethods.length);
180     }
181 
182     for (final Method expectedMethod : expectedMethods) {
183       final RestApiComparatorPath methodPath = classPath.toPathBuilder()
184           .add(METHOD, expectedMethod.getName())
185           .build();
186       final Method actualMethod = ReflectionUtils.findMethod(
187           actual, expectedMethod.getName(), expectedMethod.getParameterTypes());
188       if (!isExcluded(methodPath, METHOD_MUST_NOT_BE_NULL, exclusions)) {
189         log.info("""
190                 Assert method exists:
191                   - path = {}
192                   - type = {}""",
193             methodPath, METHOD_MUST_NOT_BE_NULL);
194         softly.assertThat(actualMethod)
195             .as("Method %s (%s) must be present on %s", expectedMethod.getName(),
196                 parameters(expectedMethod.getParameterTypes()), name(actual))
197             .isNotNull();
198       }
199       if (isNull(actualMethod)) {
200         continue;
201       }
202 
203       final Annotation[] expectedAnnotations = expectedMethod.getAnnotations();
204       final Annotation[] actualAnnotations = actualMethod.getAnnotations();
205       assertSameAnnotations(
206           softly,
207           methodPath,
208           expectedAnnotations,
209           actualAnnotations,
210           actualMethod,
211           exclusions);
212 
213       if (expectedMethod.getParameterCount() > 0) {
214         final Parameter[] expectedParameters = expectedMethod.getParameters();
215         final Parameter[] actualParameters = actualMethod.getParameters();
216         for (int i = 0; i < expectedParameters.length; i++) {
217           final Parameter expectedParameter = expectedParameters[i];
218           final Parameter actualParameter = actualParameters[i];
219           final RestApiComparatorPath methodParameterPath = methodPath.toPathBuilder()
220               .add(METHOD_PARAMETER, String.valueOf(i))
221               .build();
222           assertSameAnnotations(
223               softly,
224               methodParameterPath,
225               expectedParameter.getAnnotations(),
226               actualParameter.getAnnotations(),
227               actualParameter,
228               exclusions);
229         }
230       }
231     }
232   }
233 
234   private static void assertSameAnnotations(
235       final SoftAssertions softly,
236       final RestApiComparatorPath path,
237       final Annotation[] expectedAnnotations,
238       final Annotation[] actualAnnotations,
239       final AnnotatedElement actualAnnotatedElement,
240       final RestApiComparatorExclusion... exclusions) {
241 
242     if (!isExcluded(path, SAME_ANNOTATION_SIZE, exclusions)) {
243       log.info("""
244               Assert same annotation size:
245                 - path = {}
246                 - type = {}""",
247           path, SAME_ANNOTATION_SIZE);
248       softly.assertThat(actualAnnotations.length)
249           .as("Annotations must have the same size on %s.", path)
250           .isEqualTo(expectedAnnotations.length);
251     }
252     for (final Annotation expectedAnnotation : expectedAnnotations) {
253       final Annotation actualAnnotation = AnnotationUtils.getAnnotation(
254           actualAnnotatedElement, expectedAnnotation.annotationType());
255       assertSameAnnotation(
256           softly,
257           path,
258           expectedAnnotation,
259           actualAnnotation,
260           exclusions);
261     }
262   }
263 
264   private static void assertSameAnnotation(
265       final SoftAssertions softly,
266       final RestApiComparatorPath path,
267       final Annotation expected,
268       final Annotation actual,
269       final RestApiComparatorExclusion... exclusions) {
270 
271     final RestApiComparatorPath annotationPath = path.toPathBuilder()
272         .add(ANNOTATION, expected.annotationType().getSimpleName())
273         .build();
274     if (!isExcluded(annotationPath, ANNOTATION_MUST_NOT_BE_NULL, exclusions)) {
275       log.info("""
276               Assert annotation is not null:
277                 - path = {}
278                 - type = {}""",
279           annotationPath, ANNOTATION_MUST_NOT_BE_NULL);
280       softly.assertThat(actual)
281           .as("Annotation %s is missing on %s.", name(expected), path)
282           .isNotNull();
283     }
284     if (isNull(actual)) {
285       return;
286     }
287     final Map<String, Object> expectedAttributes = AnnotationUtils.getAnnotationAttributes(
288         expected, true, false);
289     final Map<String, Object> actualAttributes = AnnotationUtils.getAnnotationAttributes(
290         actual, true, false);
291     assertSameAttributes(
292         softly,
293         annotationPath,
294         expectedAttributes,
295         actualAttributes,
296         exclusions);
297   }
298 
299   private static void assertSameAttributes(
300       final SoftAssertions softly,
301       final RestApiComparatorPath annotationPath,
302       final Map<String, Object> expected,
303       final Map<String, Object> actual,
304       final RestApiComparatorExclusion... exclusions) {
305 
306     if (!isExcluded(annotationPath, SAME_ANNOTATION_ATTRIBUTES_SIZE, exclusions)) {
307       log.info("""
308               Assert same annotation attributes size:
309                 - path = {}
310                 - type = {}""",
311           annotationPath, SAME_ANNOTATION_ATTRIBUTES_SIZE);
312       softly.assertThat(actual.size())
313           .as("Attributes of annotation (%s) must have the same size.", annotationPath)
314           .isEqualTo(expected.size());
315     }
316     for (final Map.Entry<String, Object> expectedAttribute : expected.entrySet()) {
317       final Object expectedAttributeValue = expectedAttribute.getValue();
318       final Object actualAttributeValue = actual.get(expectedAttribute.getKey());
319       assertSameAttributeValues(
320           softly,
321           annotationPath.toPathBuilder().add(ATTRIBUTE, expectedAttribute.getKey()).build(),
322           expectedAttributeValue,
323           actualAttributeValue,
324           exclusions);
325     }
326   }
327 
328   private static void assertSameAttributeValues(
329       final SoftAssertions softly,
330       final RestApiComparatorPath attributePath,
331       final Object expected,
332       final Object actual,
333       final RestApiComparatorExclusion... exclusions) {
334 
335     if (!isExcluded(attributePath, SAME_ANNOTATION_ATTRIBUTE_VALUE, exclusions)) {
336       log.info("""
337               Assert same attribute values:
338                 - path     = {}
339                 - expected = {}
340                 - actual   = {}
341                 - type     = {}""",
342           attributePath, expected, actual, SAME_ANNOTATION_ATTRIBUTE_VALUE);
343 
344       if (isNull(exclusions)) {
345         softly.assertThat(actual)
346             .as("Attribute (%s) must be null.", attributePath)
347             .isNull();
348       } else if (expected instanceof Annotation[] expectedAnnotations) {
349         softly.assertThat(actual)
350             .as("Attributes must be instance of 'Annotation[]' on %s", attributePath)
351             .isInstanceOf(Annotation[].class);
352         final Annotation[] actualAnnotations = (Annotation[]) actual;
353         softly.assertThat(actualAnnotations.length)
354             .as("Annotations must have same size on %s", attributePath)
355             .isEqualTo(expectedAnnotations.length);
356         for (int i = 0; i < actualAnnotations.length; i++) {
357           final Annotation expectedAnnotation = expectedAnnotations[i];
358           final Annotation actualAnnotation = actualAnnotations[i];
359           assertSameAnnotation(
360               softly,
361               attributePath,
362               expectedAnnotation,
363               actualAnnotation,
364               exclusions);
365         }
366       } else if (expected instanceof Annotation expectedAnnotation) {
367         final Annotation actualAnnotation = (Annotation) actual;
368         assertSameAnnotation(
369             softly,
370             attributePath,
371             expectedAnnotation,
372             actualAnnotation,
373             exclusions);
374       } else {
375         if (expected.getClass().isArray()) {
376           final Object[] expectedArray = (Object[]) expected;
377           final Object[] actualArray = (Object[]) actual;
378           softly.assertThat(actualArray)
379               .as("Arrays must be equal on %s", attributePath)
380               .isEqualTo(expectedArray);
381         } else {
382           softly.assertThat(actual)
383               .as("Objects must be equal on %s", attributePath)
384               .isEqualTo(expected);
385         }
386       }
387     }
388   }
389 
390   private static String name(final Annotation annotation) {
391     return nonNull(annotation) ? name(annotation.annotationType()) : "null";
392   }
393 
394   private static String name(final Class<?> clazz) {
395     return nonNull(clazz) ? clazz.getName() : "null";
396   }
397 
398   private static String parameters(final Class<?>[] parameters) {
399     if (isEmpty(parameters)) {
400       return "";
401     }
402     return StringUtils.collectionToCommaDelimitedString(Arrays.stream(parameters)
403         .map(RestApiComparator::name).collect(Collectors.toList()));
404   }
405 
406 }