Created
April 1, 2020 14:00
-
-
Save Riduidel/29457acc818dea53264651dfd6c7835d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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