viclovsky / swagger-coverage

Tool which generates full picture of coverage of API tests based on OAS (Swagger) v2 and v3
Apache License 2.0
198 stars 38 forks source link

Spring mockMvc integration #144

Open Osmyslitelny opened 1 year ago

Osmyslitelny commented 1 year ago

I'm submitting a ...

Do you need PR with spring mockMvc integration based on ResultHandler implementation? I'm not sure this case is very popular, so, before creating PR I decided create this issues.

tdeverdiere commented 10 months ago

I was wondering how to implement swagger coverage on spring mvc. I first thought to use an Mvc filter : mockMvc = standaloneSetup(new PersonController()).addFilters(new SwaggerCoverageFilter()).build();

Do you have a piece of code to show how it could work?

tdeverdiere commented 10 months ago

I made an integration by implementing a ResultHandler for Swagger V3 and using it into a TestConfiguration. The ResultHandler is largely inspired by https://github.com/viclovsky/swagger-coverage/blob/master/swagger-coverage-rest-assured/src/main/java/com/github/viclovsky/swagger/coverage/SwaggerCoverageV3RestAssured.java

The ResultHandler implementation :

import com.github.viclovsky.swagger.coverage.CoverageOutputWriter;
import com.github.viclovsky.swagger.coverage.FileSystemOutputWriter;
import io.swagger.v3.oas.models.*;
import io.swagger.v3.oas.models.media.*;
import io.swagger.v3.oas.models.parameters.*;
import io.swagger.v3.oas.models.responses.*;
import io.swagger.v3.oas.models.servers.Server;

import org.apache.commons.lang3.StringUtils;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
import org.springframework.web.servlet.HandlerMapping;

import java.net.URI;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.Map;

public class SwaggerCoverageResultHandler implements ResultHandler {

    private CoverageOutputWriter writer;

    public SwaggerCoverageResultHandler() {
        this.writer = new FileSystemOutputWriter(Paths.get("target", OUTPUT_DIRECTORY));
    }

    @Override
    public void handle(MvcResult result) throws Exception {
        var request = result.getRequest();
        var response = result.getResponse();

        Operation operation = new Operation();

        Map<String, String> pathAttributes = (Map<String, String>) request.getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
        pathAttributes.forEach((n, v) -> operation.addParametersItem(new PathParameter().name(n).example(v)));

        var queryString = request.getQueryString();
        request.getParameterMap().keySet().stream()
                .filter(name -> queryString.contains(name + "="))
                .forEach((n) -> operation.addParametersItem(new QueryParameter().name(n).example(request.getParameter(n))));

        for (Iterator<String> it = request.getHeaderNames().asIterator(); it.hasNext(); ) {
            String headerName = it.next();
            String headerValue = request.getHeader(headerName);
            operation.addParametersItem(new HeaderParameter().name(headerName)
                    .example(headerValue));
        }

        if ("POST".equalsIgnoreCase(request.getMethod()) && request.getContentLength() > 0) {
            MediaType mediaType = new MediaType();
            mediaType.setSchema(new Schema());

            request.getParameterMap().keySet().stream()
                    .filter(name -> !queryString.contains(name + "="))
                    .forEach((n) -> mediaType.getSchema().addProperties(n, new Schema().example(request.getParameter(n))));

            operation.requestBody(
                    new RequestBody().content(new Content().addMediaType(request.getContentType(), mediaType)));

        }

        var apiResponse = new ApiResponse();
        if (!StringUtils.isEmpty(response.getContentType())) {
            apiResponse.content((new Content()).addMediaType(response.getContentType(), new MediaType()));
        }
        operation.responses((new ApiResponses())
                .addApiResponse(String.valueOf(response.getStatus()), apiResponse));

        PathItem pathItem = new PathItem();
        pathItem.operation(PathItem.HttpMethod.valueOf(request.getMethod().toUpperCase()), operation);
        OpenAPI openAPI = new OpenAPI()
                .addServersItem(new Server().url(URI.create(request.getRequestURI()).getHost()))
                .path(request.getRequestURI(), pathItem);

        writer.write(openAPI);

    }
}

The configuration:

import org.springframework.boot.test.autoconfigure.web.servlet.MockMvcBuilderCustomizer;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder;

@TestConfiguration
public class SwaggerCoverageTestConfiguration {

    @Bean
    SwaggerCoverageMockMvcBuilderCustomizer swaggerCoverageMockMvcBuilderCustomizer() {
        return new SwaggerCoverageMockMvcBuilderCustomizer();
    }

    public static class SwaggerCoverageMockMvcBuilderCustomizer implements MockMvcBuilderCustomizer {

        @Override
        public void customize(ConfigurableMockMvcBuilder<?> builder) {
            builder.alwaysDo(new SwaggerCoverageResultHandler());
        }
    }
}

The configuration can then be imported into tests: @ImportAutoConfiguration({SwaggerCoverageTestConfiguration.class})

Osmyslitelny commented 3 months ago

pathAttributes could be null but other code is work as expected