1 | // License: GPL. For details, see LICENSE file.
|
---|
2 | package org.openstreetmap.josm.testutils.annotations;
|
---|
3 |
|
---|
4 | import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.options;
|
---|
5 | import static org.junit.jupiter.api.Assertions.assertTrue;
|
---|
6 | import static org.junit.jupiter.api.Assertions.fail;
|
---|
7 |
|
---|
8 | import java.lang.annotation.Documented;
|
---|
9 | import java.lang.annotation.ElementType;
|
---|
10 | import java.lang.annotation.Inherited;
|
---|
11 | import java.lang.annotation.Retention;
|
---|
12 | import java.lang.annotation.RetentionPolicy;
|
---|
13 | import java.lang.annotation.Target;
|
---|
14 | import java.lang.reflect.Constructor;
|
---|
15 | import java.lang.reflect.Field;
|
---|
16 | import java.net.MalformedURLException;
|
---|
17 | import java.net.URL;
|
---|
18 | import java.util.ArrayList;
|
---|
19 | import java.util.Arrays;
|
---|
20 | import java.util.List;
|
---|
21 | import java.util.stream.Collectors;
|
---|
22 |
|
---|
23 | import org.junit.jupiter.api.extension.AfterAllCallback;
|
---|
24 | import org.junit.jupiter.api.extension.AfterEachCallback;
|
---|
25 | import org.junit.jupiter.api.extension.BeforeAllCallback;
|
---|
26 | import org.junit.jupiter.api.extension.BeforeEachCallback;
|
---|
27 | import org.junit.jupiter.api.extension.ExtendWith;
|
---|
28 | import org.junit.jupiter.api.extension.ExtensionContext;
|
---|
29 | import org.junit.jupiter.api.extension.ParameterContext;
|
---|
30 | import org.junit.jupiter.api.extension.ParameterResolutionException;
|
---|
31 | import org.junit.jupiter.api.extension.ParameterResolver;
|
---|
32 | import org.junit.platform.commons.support.AnnotationSupport;
|
---|
33 | import org.openstreetmap.josm.TestUtils;
|
---|
34 | import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
|
---|
35 | import org.openstreetmap.josm.gui.util.GuiHelper;
|
---|
36 | import org.openstreetmap.josm.io.OsmApi;
|
---|
37 | import org.openstreetmap.josm.spi.preferences.Config;
|
---|
38 | import org.openstreetmap.josm.tools.Logging;
|
---|
39 | import org.openstreetmap.josm.tools.Pair;
|
---|
40 | import org.openstreetmap.josm.tools.Utils;
|
---|
41 |
|
---|
42 | import com.github.tomakehurst.wiremock.WireMockServer;
|
---|
43 | import com.github.tomakehurst.wiremock.client.WireMock;
|
---|
44 | import com.github.tomakehurst.wiremock.extension.ResponseTransformerV2;
|
---|
45 | import com.github.tomakehurst.wiremock.verification.LoggedRequest;
|
---|
46 |
|
---|
47 | /**
|
---|
48 | * Create a basic wiremock environment. If you need the actual WireMockServer, annotate a field or parameter
|
---|
49 | * with {@code @BasicWiremock}.
|
---|
50 | *
|
---|
51 | * @author Taylor Smock
|
---|
52 | * @see OsmApiExtension (this sets the Osm Api to the wiremock URL)
|
---|
53 | * @since 18106
|
---|
54 | */
|
---|
55 | @Inherited
|
---|
56 | @Documented
|
---|
57 | @Retention(RetentionPolicy.RUNTIME)
|
---|
58 | @Target({ ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD})
|
---|
59 | @ExtendWith(BasicWiremock.WireMockExtension.class)
|
---|
60 | public @interface BasicWiremock {
|
---|
61 | /**
|
---|
62 | * Set the path for the data. Default is {@link TestUtils#getTestDataRoot()}.
|
---|
63 | * @return The path ({@code ""} for the default)
|
---|
64 | */
|
---|
65 | String value() default "";
|
---|
66 |
|
---|
67 | /**
|
---|
68 | * {@link ResponseTransformerV2} for use with the WireMock server.
|
---|
69 | * Current constructors supported:
|
---|
70 | * <ul>
|
---|
71 | * <li>{@code new ResponseTransformer()}</li>
|
---|
72 | * <li>{@code new ResponseTransformer(ExtensionContext context)}</li>
|
---|
73 | * </ul>
|
---|
74 | * @return The transformers to instantiate
|
---|
75 | */
|
---|
76 | Class<? extends ResponseTransformerV2>[] responseTransformers() default {};
|
---|
77 |
|
---|
78 | /**
|
---|
79 | * Start/stop WireMock automatically, and check for missed calls.
|
---|
80 | * @author Taylor Smock
|
---|
81 | *
|
---|
82 | */
|
---|
83 | class WireMockExtension
|
---|
84 | implements AfterAllCallback, AfterEachCallback, BeforeAllCallback, BeforeEachCallback, ParameterResolver {
|
---|
85 | /**
|
---|
86 | * Get the default wiremock server
|
---|
87 | * @param context The context to search
|
---|
88 | * @return The wiremock server
|
---|
89 | */
|
---|
90 | static WireMockServer getWiremock(ExtensionContext context) {
|
---|
91 | ExtensionContext.Namespace namespace = ExtensionContext.Namespace.create(BasicWiremock.class);
|
---|
92 | BasicWiremock annotation = AnnotationUtils.findFirstParentAnnotation(context, BasicWiremock.class)
|
---|
93 | .orElseThrow(() -> new IllegalArgumentException("There must be a @BasicWiremock annotation"));
|
---|
94 | return context.getStore(namespace).getOrComputeIfAbsent(WireMockServer.class, clazz -> {
|
---|
95 | final List<ResponseTransformerV2> transformers = new ArrayList<>(annotation.responseTransformers().length);
|
---|
96 | for (Class<? extends ResponseTransformerV2> responseTransformer : annotation.responseTransformers()) {
|
---|
97 | for (Pair<Class<?>[], Object[]> parameterMapping : Arrays.asList(
|
---|
98 | new Pair<>(new Class<?>[] {ExtensionContext.class }, new Object[] {context }),
|
---|
99 | new Pair<>(new Class<?>[0], new Object[0]))) {
|
---|
100 | try {
|
---|
101 | Constructor<? extends ResponseTransformerV2> constructor = responseTransformer
|
---|
102 | .getConstructor(parameterMapping.a);
|
---|
103 | transformers.add(constructor.newInstance(parameterMapping.b));
|
---|
104 | break;
|
---|
105 | } catch (ReflectiveOperationException e) {
|
---|
106 | fail(e);
|
---|
107 | }
|
---|
108 | }
|
---|
109 | }
|
---|
110 | return new WireMockServer(
|
---|
111 | options().usingFilesUnderDirectory(Utils.isStripEmpty(annotation.value()) ? TestUtils.getTestDataRoot() :
|
---|
112 | annotation.value()).extensions(transformers.toArray(new ResponseTransformerV2[0])).dynamicPort());
|
---|
113 | }, WireMockServer.class);
|
---|
114 | }
|
---|
115 |
|
---|
116 | /**
|
---|
117 | * Replace URL servers with wiremock
|
---|
118 | *
|
---|
119 | * @param wireMockServer The wiremock to point to
|
---|
120 | * @param url The URL to fix
|
---|
121 | * @return A url that points at the wiremock server
|
---|
122 | */
|
---|
123 | public static String replaceUrl(WireMockServer wireMockServer, String url) {
|
---|
124 | try {
|
---|
125 | URL temp = new URL(url);
|
---|
126 | return wireMockServer.baseUrl() + temp.getFile();
|
---|
127 | } catch (MalformedURLException error) {
|
---|
128 | Logging.error(error);
|
---|
129 | }
|
---|
130 | return null;
|
---|
131 | }
|
---|
132 |
|
---|
133 | @Override
|
---|
134 | public void afterAll(ExtensionContext context) throws Exception {
|
---|
135 | // Run in EDT to avoid stopping wiremock server before wiremock requests finish.
|
---|
136 | GuiHelper.runInEDTAndWait(getWiremock(context)::stop);
|
---|
137 | }
|
---|
138 |
|
---|
139 | @Override
|
---|
140 | public void afterEach(ExtensionContext context) throws Exception {
|
---|
141 | List<LoggedRequest> missed = getWiremock(context).findUnmatchedRequests().getRequests();
|
---|
142 | missed.forEach(r -> Logging.error(r.getAbsoluteUrl()));
|
---|
143 | try {
|
---|
144 | assertTrue(missed.isEmpty(), missed.stream().map(LoggedRequest::getUrl).collect(Collectors.joining("\n\n")));
|
---|
145 | } finally {
|
---|
146 | getWiremock(context).resetRequests();
|
---|
147 | getWiremock(context).resetToDefaultMappings();
|
---|
148 | getWiremock(context).resetScenarios();
|
---|
149 | if (AnnotationUtils.elementIsAnnotated(context.getElement(), BasicWiremock.class)
|
---|
150 | || getWiremock(context) == null) {
|
---|
151 | this.afterAll(context);
|
---|
152 | }
|
---|
153 | }
|
---|
154 | }
|
---|
155 |
|
---|
156 | @Override
|
---|
157 | public void beforeAll(ExtensionContext context) throws Exception {
|
---|
158 | getWiremock(context).start();
|
---|
159 | }
|
---|
160 |
|
---|
161 | @Override
|
---|
162 | public void beforeEach(ExtensionContext context) throws Exception {
|
---|
163 | if (AnnotationUtils.elementIsAnnotated(context.getElement(), BasicWiremock.class) || getWiremock(context) == null) {
|
---|
164 | this.beforeAll(context);
|
---|
165 | }
|
---|
166 | if (context.getTestClass().isPresent()) {
|
---|
167 | List<Field> wireMockFields = AnnotationSupport.findAnnotatedFields(context.getRequiredTestClass(), BasicWiremock.class);
|
---|
168 | for (Field field : wireMockFields) {
|
---|
169 | if (WireMockServer.class.isAssignableFrom(field.getType())) {
|
---|
170 | final boolean isAccessible = field.canAccess(context.getRequiredTestInstance());
|
---|
171 | field.setAccessible(true);
|
---|
172 | try {
|
---|
173 | field.set(context.getTestInstance().orElse(null), getWiremock(context));
|
---|
174 | } finally {
|
---|
175 | field.setAccessible(isAccessible);
|
---|
176 | }
|
---|
177 | } else {
|
---|
178 | throw new IllegalArgumentException("@BasicWiremock: cannot set field of type " + field.getType().getName());
|
---|
179 | }
|
---|
180 | }
|
---|
181 | }
|
---|
182 | }
|
---|
183 |
|
---|
184 | @Override
|
---|
185 | public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
|
---|
186 | throws ParameterResolutionException {
|
---|
187 | return parameterContext.getParameter().getAnnotation(BasicWiremock.class) != null
|
---|
188 | && parameterContext.getParameter().getType() == WireMockServer.class;
|
---|
189 | }
|
---|
190 |
|
---|
191 | @Override
|
---|
192 | public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
|
---|
193 | throws ParameterResolutionException {
|
---|
194 | return getWiremock(extensionContext);
|
---|
195 | }
|
---|
196 | }
|
---|
197 |
|
---|
198 | /**
|
---|
199 | * A class specifically to mock OSM API calls
|
---|
200 | */
|
---|
201 | class OsmApiExtension extends WireMockExtension {
|
---|
202 | @Override
|
---|
203 | public void afterAll(ExtensionContext context) throws Exception {
|
---|
204 | try {
|
---|
205 | super.afterAll(context);
|
---|
206 | } finally {
|
---|
207 | Config.getPref().put("osm-server.url", "https://invalid.url");
|
---|
208 | }
|
---|
209 | }
|
---|
210 |
|
---|
211 | @Override
|
---|
212 | public void beforeAll(ExtensionContext context) throws Exception {
|
---|
213 | if (!AnnotationSupport.isAnnotated(context.getElement(), BasicPreferences.class)) {
|
---|
214 | fail("OsmApiExtension requires @BasicPreferences");
|
---|
215 | }
|
---|
216 | super.beforeAll(context);
|
---|
217 | Config.getPref().put("osm-server.url", getWiremock(context).baseUrl() + "/api");
|
---|
218 | getWiremock(context).stubFor(WireMock.get("/api/0.6/capabilities")
|
---|
219 | .willReturn(WireMock.aResponse().withBodyFile("api/0.6/capabilities")));
|
---|
220 | getWiremock(context).stubFor(WireMock.get("/api/capabilities")
|
---|
221 | .willReturn(WireMock.aResponse().withBodyFile("api/capabilities")));
|
---|
222 | OsmApi.getOsmApi().initialize(NullProgressMonitor.INSTANCE);
|
---|
223 | }
|
---|
224 | }
|
---|
225 | }
|
---|