| 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 | }
|
|---|