Skip to content

Instantly share code, notes, and snippets.

@Riduidel
Created April 1, 2020 14:00
Show Gist options
  • Save Riduidel/29457acc818dea53264651dfd6c7835d to your computer and use it in GitHub Desktop.
Save Riduidel/29457acc818dea53264651dfd6c7835d to your computer and use it in GitHub Desktop.
package com.adeo.costing.computer.swagger;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DynamicTest;
import org.junit.jupiter.api.TestFactory;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.util.TestPropertyValues;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Profile;
import org.springframework.data.mongodb.repository.config.EnableMongoRepositories;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.shaded.org.apache.commons.io.FileUtils;
import com.adeo.costing.computer.configuration.MongoDBContainer;
import com.adeo.costing.computer.configuration.SecurityConfiguration;
import com.adeo.costing.computer.repository.ParameterRepository;
import com.deepoove.swagger.diff.SwaggerDiff;
import com.deepoove.swagger.diff.model.ChangedEndpoint;
import com.deepoove.swagger.diff.model.ChangedOperation;
import com.deepoove.swagger.diff.model.ChangedParameter;
import com.deepoove.swagger.diff.model.ElProperty;
import com.deepoove.swagger.diff.model.Endpoint;
import com.deepoove.swagger.diff.output.HtmlRender;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
import io.swagger.models.HttpMethod;
@SpringBootTest(classes = ExporterForApiValidator.TestConfiguration.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ImportAutoConfiguration
@ContextConfiguration(initializers = { ExporterForApiValidator.Initializer.class })
@Testcontainers
@Import(value = {SecurityConfiguration.class})
@Profile(value = "validate-api")
public class ExporterForApiValidator {
private static final String API_VALIDATOR_OUTPUT_FILE = "api.validator.output.file";
private static final String API_VALIDATOR_CONTRACT_FILE = "api.validator.contract.file";
private static final String API_VALIDATOR_DIFF_FILE = "api.validator.diff.file";
static class Initializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
@Override
public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
}
}
@EnableAutoConfiguration
@EnableWebMvc
@ComponentScan(basePackages = "TODO")
@Configuration
static class TestConfiguration {
}
@LocalServerPort
private int port;
HttpHeaders headers = new HttpHeaders();
TestRestTemplate restTemplate = new TestRestTemplate();
private String createURLWithPort(String uri) {
return "http://localhost:" + port + uri;
}
private static class DynamicTestRender {
public Collection<DynamicTest> render(SwaggerDiff diff) {
Collection<DynamicTest> returned = new ArrayList<>();
for(Endpoint newEndpoint: diff.getNewEndpoints()) {
// A new endpoint is not a problem
returned.add(DynamicTest.dynamicTest("ADDED "+newEndpoint.getPathUrl(),
() -> Assertions.assertThat(true).isTrue().as("Endpoint %s seems to have been added", newEndpoint.getPathUrl())));
}
for(Endpoint missingEndpoint: diff.getMissingEndpoints()) {
// A missing endpoint IS problem
returned.add(dynamicTest("REMOVED "+missingEndpoint.getPathUrl(),
() -> assertThat(false).isTrue().as("Endpoint %s seems to have been removed", missingEndpoint.getPathUrl())));
}
for(ChangedEndpoint changedEndpoint: diff.getChangedEndpoints()) {
// For a changed endpoint, we can have added elements, but not removed ones
changedEndpoint.getNewOperations().entrySet().stream().forEach(entry -> {
returned.add(dynamicTest("ADDED "+entry.getKey().name()+" "+changedEndpoint.getPathUrl(),
() -> assertThat(true).isTrue().as("Endpoint %s seems to have been added", entry.getKey().name()+" "+changedEndpoint.getPathUrl())));
});
changedEndpoint.getMissingOperations().entrySet().stream().forEach(entry -> {
returned.add(dynamicTest("REMOVED "+entry.getKey().name()+" "+changedEndpoint.getPathUrl(),
() -> assertThat(false).isTrue().as("Endpoint %s seems to have been removed", entry.getKey().name()+" "+changedEndpoint.getPathUrl())));
});
for(HttpMethod method : changedEndpoint.getChangedOperations().keySet()) {
ChangedOperation changedOperation = changedEndpoint.getChangedOperations().get(method);
returned.add(dynamicTest("CHANGED "+method.name()+" "+changedEndpoint.getPathUrl(),
() -> assertThat(changedOperation)
.satisfies(changed -> assertThat(changed.getMissingParameters())
.as("Removed parameters in \"%s\", %s are marked as missing",
changed.getSummary(),
changed.getMissingParameters().stream().map(param -> param.getName()).collect(Collectors.toList())
)
.isEmpty())
.satisfies(changed -> assertThat(changed.getMissingProps())
.as("Removed properties in \"%s\", %s are marked as missing",
changed.getSummary(),
changed.getMissingProps().stream().map(prop -> prop.getProperty().getName()).collect(Collectors.toList())
)
.isEmpty())
.satisfies(changed -> assertThat(changed.getChangedParameter())
.as("Changed parameter failure in \"%s\"", changed.getSummary())
.allSatisfy(changedParamater -> assertThat(changedParamater)
.satisfies(this::noMissingParameterInChangedParameter)
// .satisfies(this::noChangedParameterInChangedParameter)
)
)));
}
}
return returned;
}
private void noMissingParameterInChangedParameter(ChangedParameter changed) {
List<ElProperty> missing = changed.getMissing();
assertThat(missing)
.as("There should be no removed parameter, but %s are marked as removed",
missing.stream().map(m -> m.getEl()).collect(Collectors.joining(", ", "[", "]")))
.isEmpty();
}
private void noChangedParameterInChangedParameter(ChangedParameter changed) {
List<ElProperty> modified = changed.getChanged();
assertThat(modified)
.as("There should be no changed parameter, but %s are marked as changed",
modified.stream().map(m -> m.getProperty().getName()==null ? m.getProperty().getDescription() : m.getProperty().getName()).collect(Collectors.toList()))
.isEmpty();
}
}
@TestFactory
Collection<DynamicTest> testSwaggerAnnotationsMatchesApiYaml() throws IOException {
File effectiveSwagger = getSwaggerAsFile();
File contractSwagger = systemPropertyAsFile(API_VALIDATOR_CONTRACT_FILE);
File outputDiff = systemPropertyAsFile(API_VALIDATOR_DIFF_FILE);
SwaggerDiff diff = SwaggerDiff.compareV2(contractSwagger.getAbsolutePath(), effectiveSwagger.getAbsolutePath());
// Let's suppose for now that the CSS will be in the same folder
String html = new HtmlRender("Changelog", "demo.css").render(diff);
FileUtils.write(outputDiff, html, "UTF-8");
// Beside rendering, we also generate dynamic tests
return new DynamicTestRender().render(diff);
}
private File getSwaggerAsFile() throws JsonProcessingException, JsonMappingException, IOException {
File output = systemPropertyAsFile(API_VALIDATOR_OUTPUT_FILE);
ResponseEntity<String> docs = restTemplate.getForEntity(createURLWithPort("/api-docs"), String.class);
assertThat(docs.getStatusCodeValue()).isLessThan(400);
String api = docs.getBody();
assertThat(api).isNotBlank();
writeYaml(output, api);
return output;
}
private void writeYaml(File output, String api) throws JsonProcessingException, JsonMappingException, IOException {
JsonNode json = new ObjectMapper().readTree(api);
String yaml = new YAMLMapper().writeValueAsString(json).replace("---\n", "");
FileUtils.write(output, yaml, "UTF-8");
}
private File systemPropertyAsFile(String systemProperty) {
String outputFileName = System.getProperty(systemProperty);
assertThat(outputFileName).as("Property %s must be defined", systemProperty).isNotBlank();
File output = new File(outputFileName);
// assertThat(output).as("Property %s must be a resolvable file path", systemProperty).isFile();
return output;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment