Skip to content

Commit

Permalink
feat: HTTP and HTTPS in same mock (refs #62)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasbjerre committed Nov 2, 2024
1 parent 9a1684b commit 6806588
Show file tree
Hide file tree
Showing 7 changed files with 407 additions and 155 deletions.
29 changes: 25 additions & 4 deletions src/main/java/org/wiremock/spring/ConfigureWireMock.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,24 @@
public @interface ConfigureWireMock {

/**
* Port on which WireMock server is going to listen. {@code 0} means WireMock will pick random
* port.
* Port on which WireMock server is going to listen.
*
* <p>{@code -1} means disabled.
*
* <p>{@code 0} means WireMock will pick random available port.
*
* <p>{@code >0} means that static port will be used.
*
* @return WireMock server port
*/
int port() default 0;

/**
* @return true for HTTPS, else false.
* Same as {@link #port()} but for HTTPS.
*
* @return HTTPS port to use.
*/
boolean useHttps() default false;
int httpsPort() default -1;

/**
* The name of WireMock server.
Expand All @@ -49,13 +56,27 @@
*/
String[] portProperties() default {"wiremock.server.port"};

/**
* Names of Spring properties to inject the {@link WireMockServer#httpsPort()}
*
* @return names of Spring properties to inject the {@link WireMockServer#httpsPort()}
*/
String[] httpsPortProperties() default {"wiremock.server.httpsPort"};

/**
* Names of Spring properties to inject the {@link WireMockServer#baseUrl()}.
*
* @return names of Spring properties to inject the {@link WireMockServer#baseUrl()}.
*/
String[] baseUrlProperties() default {"wiremock.server.baseUrl"};

/**
* Names of Spring properties to inject the {@link WireMockServer#baseUrl()}.
*
* @return names of Spring properties to inject the {@link WireMockServer#baseUrl()}.
*/
String[] httpsBaseUrlProperties() default {"wiremock.server.httpsBaseUrl"};

/**
* Classpaths to pass to {@link WireMockConfiguration#usingFilesUnderClasspath(String)}. First one
* that is found will be used. If a {@link #name()} is supplied, it will first look for {@link
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
Expand All @@ -15,80 +16,110 @@
import org.wiremock.spring.EnableWireMock;

/**
* Creates {@link WireMockContextCustomizer} for test classes annotated with {@link EnableWireMock}.
* Creates {@link WireMockContextCustomizer} for test classes annotated with
* {@link EnableWireMock}.
*
* @author Maciej Walkowiak
*/
public class WireMockContextCustomizerFactory implements ContextCustomizerFactory {
static final ConfigureWireMock DEFAULT_CONFIGURE_WIREMOCK =
DefaultConfigureWireMock.class.getAnnotation(ConfigureWireMock.class);

@ConfigureWireMock(name = "wiremock")
private static class DefaultConfigureWireMock {}

static ConfigureWireMock[] getConfigureWireMocksOrDefault(
final ConfigureWireMock... configureWireMock) {
if (configureWireMock == null || configureWireMock.length == 0) {
return new ConfigureWireMock[] {WireMockContextCustomizerFactory.DEFAULT_CONFIGURE_WIREMOCK};
}
return configureWireMock;
}

@Override
public ContextCustomizer createContextCustomizer(
final Class<?> testClass, final List<ContextConfigurationAttributes> configAttributes) {
// scan class and all enclosing classes if the test class is @Nested
final ConfigureWiremockHolder holder = new ConfigureWiremockHolder();
this.parseDefinitions(testClass, holder);

if (holder.isEmpty()) {
return null;
} else {
return new WireMockContextCustomizer(holder.asArray());
}
}

private void parseDefinitions(final Class<?> testClass, final ConfigureWiremockHolder parser) {
parser.parse(testClass);
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
this.parseDefinitions(testClass.getEnclosingClass(), parser);
}
}

private static class ConfigureWiremockHolder {
private final List<ConfigureWireMock> annotations = new ArrayList<>();

void add(final ConfigureWireMock... annotations) {
this.annotations.addAll(Arrays.asList(annotations));
this.sanityCheckDuplicateNames(this.annotations);
}

void parse(final Class<?> clazz) {
final EnableWireMock annotation = AnnotationUtils.findAnnotation(clazz, EnableWireMock.class);
if (annotation != null) {
this.add(getConfigureWireMocksOrDefault(annotation.value()));
}
}

private void sanityCheckDuplicateNames(final List<ConfigureWireMock> check) {
final List<String> names = check.stream().map(it -> it.name()).toList();
final Set<String> dublicateNames =
names.stream()
.filter(it -> Collections.frequency(names, it) > 1)
.collect(Collectors.toSet());
if (!dublicateNames.isEmpty()) {
throw new IllegalStateException(
"Names of mocks must be unique, found duplicates of: "
+ dublicateNames.stream().sorted().collect(Collectors.joining(",")));
}
}

boolean isEmpty() {
return this.annotations.isEmpty();
}

ConfigureWireMock[] asArray() {
return this.annotations.toArray(new ConfigureWireMock[] {});
}
}
static final ConfigureWireMock DEFAULT_CONFIGURE_WIREMOCK = DefaultConfigureWireMock.class
.getAnnotation(ConfigureWireMock.class);

@ConfigureWireMock(name = "wiremock")
private static class DefaultConfigureWireMock {
}

static ConfigureWireMock[] getConfigureWireMocksOrDefault(final ConfigureWireMock... configureWireMock) {
if (configureWireMock == null || configureWireMock.length == 0) {
return new ConfigureWireMock[] { WireMockContextCustomizerFactory.DEFAULT_CONFIGURE_WIREMOCK };
}
return configureWireMock;
}

@Override
public ContextCustomizer createContextCustomizer(final Class<?> testClass,
final List<ContextConfigurationAttributes> configAttributes) {
// scan class and all enclosing classes if the test class is @Nested
final ConfigureWiremockHolder holder = new ConfigureWiremockHolder();
this.parseDefinitions(testClass, holder);

if (holder.isEmpty()) {
return null;
} else {
return new WireMockContextCustomizer(holder.asArray());
}
}

private void parseDefinitions(final Class<?> testClass, final ConfigureWiremockHolder parser) {
parser.parse(testClass);
if (TestContextAnnotationUtils.searchEnclosingClass(testClass)) {
this.parseDefinitions(testClass.getEnclosingClass(), parser);
}
}

private static class ConfigureWiremockHolder {
private final List<ConfigureWireMock> annotations = new ArrayList<>();

void add(final ConfigureWireMock... annotations) {
this.annotations.addAll(Arrays.asList(annotations));
this.sanityCheckDuplicateNames(this.annotations);
this.sanityCheckHttpOrHttpsMustBeEnabled(this.annotations);
this.sanityCheckHttpAndHttpsMustUseDifferentPorts(this.annotations);
this.sanityCheckUniquePorts(this.annotations);
}

void parse(final Class<?> clazz) {
final EnableWireMock annotation = AnnotationUtils.findAnnotation(clazz, EnableWireMock.class);
if (annotation != null) {
this.add(getConfigureWireMocksOrDefault(annotation.value()));
}
}

private void sanityCheckDuplicateNames(final List<ConfigureWireMock> check) {
final List<String> names = check.stream().map(it -> it.name()).toList();
final Set<String> dublicateNames = names.stream().filter(it -> Collections.frequency(names, it) > 1)
.collect(Collectors.toSet());
if (!dublicateNames.isEmpty()) {
throw new IllegalStateException("Names of mocks must be unique, found duplicates of: "
+ dublicateNames.stream().sorted().collect(Collectors.joining(",")));
}
}

private void sanityCheckHttpOrHttpsMustBeEnabled(final List<ConfigureWireMock> check) {
for (final ConfigureWireMock configureWireMock : check) {
if (configureWireMock.port() == -1 && configureWireMock.httpsPort() == -1) {
throw new IllegalStateException("ConfigureWireMock " + configureWireMock.name()
+ " has both HTTP and HTTPS disabled. It is an invalid configuration.");
}
}
}

private void sanityCheckUniquePorts(final List<ConfigureWireMock> check) {
final List<Integer> ports = check.stream().map(it -> List.of(it.port(), it.httpsPort())).toList().stream()
.collect(ArrayList::new, List::addAll, List::addAll);
final Set<Integer> dublicatePors = ports.stream().filter(it -> it > 0)
.filter(it -> Collections.frequency(ports, it) > 1).collect(Collectors.toSet());
if (!dublicatePors.isEmpty()) {
throw new IllegalStateException("Some statically configured ports are being used mor than once: "
+ dublicatePors.stream().sorted().map(it -> it.toString()).collect(Collectors.joining(",")));
}
}

private void sanityCheckHttpAndHttpsMustUseDifferentPorts(final List<ConfigureWireMock> check) {
for (final ConfigureWireMock configureWireMock : check) {
if (configureWireMock.port() > 0 && configureWireMock.port() == configureWireMock.httpsPort()) {
throw new IllegalStateException("ConfigureWireMock " + configureWireMock.name() + " uses same port "
+ configureWireMock.port() + " for HTTP and HTTPS.");
}
}
}

boolean isEmpty() {
return this.annotations.isEmpty();
}

ConfigureWireMock[] asArray() {
return this.annotations.toArray(new ConfigureWireMock[] {});
}
}
}
Loading

0 comments on commit 6806588

Please sign in to comment.