diff --git a/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java b/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java index 6bcff8a76fc..ce1ea359572 100644 --- a/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java +++ b/src/main/java/tech/jhipster/lite/generator/project/domain/GeneratorAction.java @@ -67,6 +67,8 @@ private GeneratorAction() {} public static final String SPRINGDOC_OPENAPI = "springdoc-openapi"; public static final String SPRINGDOC_OPENAPI_WITH_SECURIITY_JWT = "springdoc-openapi-with-security-jwt"; + public static final String SPRINGBOOT_CUCUMBER = "springboot-cucumber"; + public static final String REACT = "react"; public static final String REACT_STYLED = "react-styled"; diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/application/CucumberApplicationService.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/application/CucumberApplicationService.java new file mode 100644 index 00000000000..e920848a9bb --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/application/CucumberApplicationService.java @@ -0,0 +1,26 @@ +package tech.jhipster.lite.generator.server.springboot.cucumber.application; + +import org.springframework.stereotype.Service; +import tech.jhipster.lite.generator.module.JHipsterModules; +import tech.jhipster.lite.generator.module.domain.JHipsterModule; +import tech.jhipster.lite.generator.server.springboot.cucumber.domain.CucumberModuleFactory; +import tech.jhipster.lite.generator.server.springboot.cucumber.domain.CucumberModuleProperties; + +@Service +public class CucumberApplicationService { + + private final JHipsterModules modules; + private final CucumberModuleFactory factory; + + public CucumberApplicationService(JHipsterModules modules) { + this.modules = modules; + + factory = new CucumberModuleFactory(); + } + + public void add(CucumberModuleProperties properties) { + JHipsterModule module = factory.buildModule(properties); + + modules.apply(properties.indentation(), module); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleFactory.java new file mode 100644 index 00000000000..e18178cfd7e --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleFactory.java @@ -0,0 +1,105 @@ +package tech.jhipster.lite.generator.server.springboot.cucumber.domain; + +import static tech.jhipster.lite.generator.module.domain.JHipsterModule.*; + +import tech.jhipster.lite.error.domain.Assert; +import tech.jhipster.lite.generator.module.domain.JHipsterModule; +import tech.jhipster.lite.generator.module.domain.JHipsterSource; +import tech.jhipster.lite.generator.module.domain.javadependency.JavaDependency; +import tech.jhipster.lite.generator.module.domain.javadependency.JavaDependencyScope; + +public class CucumberModuleFactory { + + public JHipsterModule buildModule(CucumberModuleProperties properties) { + Assert.notNull("properties", properties); + + String packagePath = properties.basePackage().path(); + String applicationName = properties.projectBaseName().capitalized(); + JHipsterSource source = from("server/springboot/cucumber"); + + //@formatter:off + return moduleForProject(properties.project()) + .context() + .packageName(properties.basePackage()) + .put("applicationName", applicationName) + .and() + .files() + .batch(source, toSrcTestJava().append(packagePath).append("cucumber")) + .add("AsyncElementAsserter.java") + .add("AsyncHeaderAsserter.java") + .add("AsyncResponseAsserter.java") + .add("Awaiter.java") + .add("CucumberAssertions.java") + .add("CucumberConfiguration.java") + .add("CucumberJson.java") + .add("CucumberTest.java") + .add("CucumberTestContext.java") + .add("CucumberTestContextUnitTest.java") + .add("ElementAsserter.java") + .add("ElementAssertions.java") + .add("HeaderAsserter.java") + .add("HeaderAssertions.java") + .add("ResponseAsserter.java") + .add("SyncElementAsserter.java") + .add("SyncHeaderAsserter.java") + .add("SyncResponseAsserter.java") + .and() + .add(source.template("cucumber.md"), to("documentation/cucumber.md")) + .add(source.file("gitkeep"), to("src/test/features/.gitkeep")) + .and() + .javaDependencies() + .add(CucumberJunitDependency()) + .add(cucumberJavaDependency()) + .add(cucumberSpringDependency()) + .add(junitVintageDependency()) + .add(testNgDependency()) + .add(awaitilityDepencency()) + .and() + .build(); + //@formatter:on + } + + private JavaDependency CucumberJunitDependency() { + return javaDependency() + .groupId("io.cucumber") + .artifactId("cucumber-junit") + .versionSlug("cucumber.version") + .scope(JavaDependencyScope.TEST) + .build(); + } + + private JavaDependency cucumberJavaDependency() { + return javaDependency() + .groupId("io.cucumber") + .artifactId("cucumber-java") + .versionSlug("cucumber.version") + .scope(JavaDependencyScope.TEST) + .build(); + } + + private JavaDependency cucumberSpringDependency() { + return javaDependency() + .groupId("io.cucumber") + .artifactId("cucumber-spring") + .versionSlug("cucumber.version") + .scope(JavaDependencyScope.TEST) + .build(); + } + + private JavaDependency junitVintageDependency() { + return javaDependency().groupId("org.junit.vintage").artifactId("junit-vintage-engine").scope(JavaDependencyScope.TEST).build(); + } + + private JavaDependency testNgDependency() { + return javaDependency() + .groupId("org.testng") + .artifactId("testng") + .versionSlug("testng.version") + .scope(JavaDependencyScope.TEST) + .build(); + } + + private JavaDependency awaitilityDepencency() { + return javaDependency().groupId("org.awaitility").artifactId("awaitility").scope(JavaDependencyScope.TEST).build(); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleProperties.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleProperties.java new file mode 100644 index 00000000000..cf2dbe70734 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleProperties.java @@ -0,0 +1,90 @@ +package tech.jhipster.lite.generator.server.springboot.cucumber.domain; + +import tech.jhipster.lite.error.domain.Assert; +import tech.jhipster.lite.generator.module.domain.Indentation; +import tech.jhipster.lite.generator.module.domain.JHipsterBasePackage; +import tech.jhipster.lite.generator.module.domain.JHipsterProjectBaseName; +import tech.jhipster.lite.generator.module.domain.JHipsterProjectFolder; +import tech.jhipster.lite.generator.project.domain.Project; + +public class CucumberModuleProperties { + + private final JHipsterProjectFolder project; + private final Indentation indentation; + private final JHipsterBasePackage basePackage; + private final JHipsterProjectBaseName projectBaseName; + + private CucumberModuleProperties(CucumberModulePropertiesBuilder builder) { + project = new JHipsterProjectFolder(builder.project); + indentation = Indentation.from(builder.indentation); + basePackage = new JHipsterBasePackage(builder.basePackage); + projectBaseName = new JHipsterProjectBaseName(builder.projectBaseName); + } + + public static CucumberModuleProperties from(Project project) { + Assert.notNull("project", project); + + return builder() + .project(project.getFolder()) + .indentation(project.getIntegerConfig("prettierDefaultIndent").orElse(null)) + .basePackage(project.getPackageNamePath().orElse(null)) + .projectBaseName(project.getBaseName().orElse(null)) + .build(); + } + + public static CucumberModulePropertiesBuilder builder() { + return new CucumberModulePropertiesBuilder(); + } + + public JHipsterProjectFolder project() { + return project; + } + + public Indentation indentation() { + return indentation; + } + + public JHipsterBasePackage basePackage() { + return basePackage; + } + + public JHipsterProjectBaseName projectBaseName() { + return projectBaseName; + } + + public static class CucumberModulePropertiesBuilder { + + private String project; + private Integer indentation; + private String basePackage; + private String projectBaseName; + + public CucumberModulePropertiesBuilder project(String project) { + this.project = project; + + return this; + } + + public CucumberModulePropertiesBuilder indentation(Integer indentation) { + this.indentation = indentation; + + return this; + } + + public CucumberModulePropertiesBuilder basePackage(String basePackage) { + this.basePackage = basePackage; + + return this; + } + + public CucumberModulePropertiesBuilder projectBaseName(String projectBaseName) { + this.projectBaseName = projectBaseName; + + return this; + } + + public CucumberModuleProperties build() { + return new CucumberModuleProperties(this); + } + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/infrastructure/primary/CucumberResource.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/infrastructure/primary/CucumberResource.java new file mode 100644 index 00000000000..6341cc3bc48 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/infrastructure/primary/CucumberResource.java @@ -0,0 +1,37 @@ +package tech.jhipster.lite.generator.server.springboot.cucumber.infrastructure.primary; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import tech.jhipster.lite.generator.project.domain.GeneratorAction; +import tech.jhipster.lite.generator.project.domain.Project; +import tech.jhipster.lite.generator.project.infrastructure.primary.dto.ProjectDTO; +import tech.jhipster.lite.generator.server.springboot.cucumber.application.CucumberApplicationService; +import tech.jhipster.lite.generator.server.springboot.cucumber.domain.CucumberModuleProperties; +import tech.jhipster.lite.technical.infrastructure.primary.annotation.GeneratorStep; + +@RestController +@Tag(name = "Spring Boot") +@RequestMapping("/api/servers/spring-boot/cucumber") +class CucumberResource { + + private final CucumberApplicationService cucumber; + + public CucumberResource(CucumberApplicationService cucumber) { + this.cucumber = cucumber; + } + + @PostMapping + @GeneratorStep(id = GeneratorAction.SPRINGBOOT_CUCUMBER) + @Operation(summary = "Add cucumber integration to project") + @ApiResponse(responseCode = "500", description = "An error occurred while adding cucumber elements") + public void addJavaBase(@RequestBody ProjectDTO projectDTO) { + Project project = ProjectDTO.toProject(projectDTO); + + cucumber.add(CucumberModuleProperties.from(project)); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/package-info.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/package-info.java new file mode 100644 index 00000000000..98229f8fc33 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/cucumber/package-info.java @@ -0,0 +1,2 @@ +@tech.jhipster.lite.BusinessContext +package tech.jhipster.lite.generator.server.springboot.cucumber; diff --git a/src/main/resources/generator/dependencies/pom.xml b/src/main/resources/generator/dependencies/pom.xml index 154c80fc876..106cebb1386 100644 --- a/src/main/resources/generator/dependencies/pom.xml +++ b/src/main/resources/generator/dependencies/pom.xml @@ -32,6 +32,8 @@ 8.5.11 5.0.41 0.23.1 + 7.3.4 + 7.6.0 @@ -117,6 +119,40 @@ ${archunit-junit5.version} test + + io.cucumber + cucumber-junit + ${cucumber.version} + test + + + io.cucumber + cucumber-java + ${cucumber.version} + test + + + io.cucumber + cucumber-spring + ${cucumber.version} + test + + + org.junit.vintage + junit-vintage-engine + test + + + org.testng + testng + ${testng.version} + test + + + org.awaitility + awaitility + test + diff --git a/src/main/resources/generator/server/springboot/cucumber/AsyncElementAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/AsyncElementAsserter.java.mustache new file mode 100644 index 00000000000..c4527f0ba38 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/AsyncElementAsserter.java.mustache @@ -0,0 +1,73 @@ +package {{packageName}}.cucumber; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +class AsyncElementAsserter implements ElementAsserter { + + private final Duration maxTime; + private final AsyncResponseAsserter responseAsserter; + private final ElementAssertions assertions; + + AsyncElementAsserter(AsyncResponseAsserter responseAsserter, String jsonPath, Duration maxTime) { + this.responseAsserter = responseAsserter; + this.maxTime = maxTime; + assertions = new ElementAssertions(jsonPath); + } + + @Override + public AsyncElementAsserter withValues(Collection values) { + Awaiter.await(maxTime, () -> assertions.withValues(values)); + + return this; + } + + @Override + public AsyncElementAsserter withElementsCount(int count) { + Awaiter.await(maxTime, () -> assertions.withElementsCount(count)); + + return this; + } + + @Override + public AsyncElementAsserter withMoreThanElementsCount(int count) { + Awaiter.await(maxTime, () -> assertions.withMoreThanElementsCount(count)); + + return this; + } + + @Override + public AsyncElementAsserter withValue(Object value) { + Awaiter.await(maxTime, () -> assertions.withValue(value)); + + return this; + } + + @Override + public AsyncElementAsserter containingExactly(List> responses) { + Awaiter.await(maxTime, () -> assertions.containingExactly(responses)); + + return this; + } + + @Override + public AsyncElementAsserter containing(Map response) { + Awaiter.await(maxTime, () -> assertions.containing(response)); + + return this; + } + + @Override + public AsyncElementAsserter containing(List> responses) { + Awaiter.await(maxTime, () -> assertions.containing(responses)); + + return this; + } + + @Override + public AsyncResponseAsserter and() { + return responseAsserter; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/AsyncHeaderAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/AsyncHeaderAsserter.java.mustache new file mode 100644 index 00000000000..478084e2d60 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/AsyncHeaderAsserter.java.mustache @@ -0,0 +1,35 @@ +package {{packageName}}.cucumber; + +import java.time.Duration; + +public class AsyncHeaderAsserter implements HeaderAsserter { + + private final Duration maxTime; + private final AsyncResponseAsserter responseAsserter; + private final HeaderAssertions assertions; + + AsyncHeaderAsserter(AsyncResponseAsserter responseAsserter, String header, Duration maxTime) { + this.responseAsserter = responseAsserter; + this.maxTime = maxTime; + assertions = new HeaderAssertions(header); + } + + @Override + public AsyncHeaderAsserter containing(String value) { + Awaiter.await(maxTime, () -> assertions.containing(value)); + + return this; + } + + @Override + public AsyncHeaderAsserter startingWith(String prefix) { + Awaiter.await(maxTime, () -> assertions.startingWith(prefix)); + + return this; + } + + @Override + public AsyncResponseAsserter and() { + return responseAsserter; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/AsyncResponseAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/AsyncResponseAsserter.java.mustache new file mode 100644 index 00000000000..b74cc0dd4fd --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/AsyncResponseAsserter.java.mustache @@ -0,0 +1,46 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Duration; +import org.springframework.http.HttpStatus; + +class AsyncResponseAsserter implements ResponseAsserter { + + private final Duration maxTime; + + AsyncResponseAsserter(Duration maxTime) { + this.maxTime = maxTime; + } + + @Override + public AsyncResponseAsserter hasHttpStatus(HttpStatus status) { + Awaiter.await(maxTime, () -> CucumberAssertions.assertHttpStatus(status)); + + return this; + } + + @Override + public ResponseAsserter hasHttpStatusIn(HttpStatus... statuses) { + Awaiter.await(maxTime, () -> CucumberAssertions.assertHttpStatusIn(statuses)); + + return this; + } + + @Override + public AsyncElementAsserter hasElement(String jsonPath) { + return new AsyncElementAsserter(this, jsonPath, maxTime); + } + + @Override + public AsyncHeaderAsserter hasHeader(String header) { + return new AsyncHeaderAsserter(this, header, maxTime); + } + + @Override + public AsyncResponseAsserter hasRawBody(String info) { + Awaiter.await(maxTime, () -> assertThat(CucumberAssertions.responseBody()).isEqualTo(info)); + + return this; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/Awaiter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/Awaiter.java.mustache new file mode 100644 index 00000000000..bb0db1d6710 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/Awaiter.java.mustache @@ -0,0 +1,28 @@ +package {{packageName}}.cucumber; + +import java.time.Duration; +import org.assertj.core.api.SoftAssertionsProvider.ThrowingRunnable; +import org.awaitility.Awaitility; +import org.awaitility.core.ConditionFactory; + +class Awaiter { + + private Awaiter() {} + + static void await(Duration maxTime, ThrowingRunnable assertion) { + awaiter(maxTime) + .untilAsserted(() -> { + try { + assertion.run(); + } catch (AssertionError e) { + CucumberTestContext.retry(); + + assertion.run(); + } + }); + } + + private static ConditionFactory awaiter(Duration maxTime) { + return Awaitility.await().pollDelay(Duration.ofMillis(0)).pollInterval(Duration.ofMillis(20)).atMost(maxTime); + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/CucumberAssertions.java.mustache b/src/main/resources/generator/server/springboot/cucumber/CucumberAssertions.java.mustache new file mode 100644 index 00000000000..b9d2edf64ea --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/CucumberAssertions.java.mustache @@ -0,0 +1,72 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; + +import java.time.Duration; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.assertj.core.description.Description; +import org.springframework.http.HttpStatus; + +public final class CucumberAssertions { + + private static final CallDescription JSON_DESCRIPTION = new CallDescription(); + + private CucumberAssertions() {} + + public static ResponseAsserter assertThatLastResponse() { + return new SyncResponseAsserter(); + } + + public static ResponseAsserter assertThatLastAsyncResponse() { + return assertThatLastAsyncResponse(Duration.ofSeconds(4)); + } + + public static ResponseAsserter assertThatLastAsyncResponse(Duration maxDelay) { + assertThat(maxDelay).as("Can't check async responses without maxDelay").isNotNull(); + + return new AsyncResponseAsserter(maxDelay); + } + + static void assertHttpStatus(HttpStatus status) { + assertThat(CucumberTestContext.getStatus()) + .as(() -> "Expecting request to result in " + status + " but got " + CucumberTestContext.getStatus() + callContext()) + .isEqualTo(status); + } + + static void assertHttpStatusIn(HttpStatus[] statuses) { + assertThat(statuses).as("Can't check statuses without statuses").isNotEmpty(); + + assertThat(CucumberTestContext.getStatus()) + .as(() -> + "Expecting request to result in any of " + + Stream.of(statuses).map(HttpStatus::toString).collect(Collectors.joining(", ")) + + " but got " + + CucumberTestContext.getStatus() + + callContext() + ) + .isIn((Object[]) statuses); + } + + static Description callContext() { + return JSON_DESCRIPTION; + } + + static String responseBody() { + return CucumberTestContext.getResponse().get(); + } + + private static class CallDescription extends Description { + + @Override + public String value() { + return ( + " from " + + CucumberTestContext.getUri() + + " in " + + System.lineSeparator() + + CucumberTestContext.getResponse().map(CucumberJson::pretty).orElse("empty") + ); + } + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/CucumberConfiguration.java.mustache b/src/main/resources/generator/server/springboot/cucumber/CucumberConfiguration.java.mustache new file mode 100644 index 00000000000..71fb329202b --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/CucumberConfiguration.java.mustache @@ -0,0 +1,59 @@ +package {{packageName}}.cucumber; + +import io.cucumber.java.Before; +import io.cucumber.spring.CucumberContextConfiguration; +import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.http.client.SimpleClientHttpRequestFactory; +import org.springframework.http.converter.StringHttpMessageConverter; +import org.springframework.web.client.RestTemplate; +import {{packageName}}.{{applicationName}}App; + +@CucumberContextConfiguration +@SpringBootTest(classes = { {{applicationName}}App.class }, webEnvironment = WebEnvironment.RANDOM_PORT) +public class CucumberConfiguration { + + @Autowired + private TestRestTemplate rest; + + @Before + public void resetTestContext() { + CucumberTestContext.reset(); + } + + @Before + public void loadInterceptors() { + ClientHttpRequestFactory requestFactory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()); + + RestTemplate template = rest.getRestTemplate(); + template.setRequestFactory(requestFactory); + template.setInterceptors(Arrays.asList(mockedCsrfTokenInterceptor(), saveLastResultInterceptor())); + template.getMessageConverters().add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8)); + } + + private ClientHttpRequestInterceptor mockedCsrfTokenInterceptor() { + return (request, body, execution) -> { + request.getHeaders().add("mocked-csrf-token", "MockedToken"); + + return execution.execute(request, body); + }; + } + + private ClientHttpRequestInterceptor saveLastResultInterceptor() { + return (request, body, execution) -> { + ClientHttpResponse response = execution.execute(request, body); + + CucumberTestContext.addResponse(request, response, execution, body); + + return response; + }; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/CucumberJson.java.mustache b/src/main/resources/generator/server/springboot/cucumber/CucumberJson.java.mustache new file mode 100644 index 00000000000..016f72fc220 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/CucumberJson.java.mustache @@ -0,0 +1,56 @@ +package {{packageName}}.cucumber; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.json.JsonMapper; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.io.IOException; +import java.util.Arrays; +import java.util.stream.Collectors; +import org.apache.commons.lang3.StringUtils; + +final class CucumberJson { + + private static final ObjectMapper jsonMapper = jsonMapper(); + + private CucumberJson() {} + + public static ObjectMapper jsonMapper() { + return JsonMapper + .builder() + .serializationInclusion(JsonInclude.Include.NON_NULL) + .addModule(new JavaTimeModule()) + .addModules(new Jdk8Module()) + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .disable(DeserializationFeature.FAIL_ON_MISSING_EXTERNAL_TYPE_ID_PROPERTY) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) + .build(); + } + + public static String pretty(String json) { + if (StringUtils.isBlank(json)) { + return json; + } + + try { + return jsonMapper.writerWithDefaultPrettyPrinter().writeValueAsString(jsonMapper.readValue(json, Object.class)); + } catch (IOException e) { + return json; + } + } + + public static String toCamelCase(String value) { + if (value == null) { + return null; + } + + return Arrays.stream(value.split("\\.")).map(CucumberJson::camelCaseField).collect(Collectors.joining(".")); + } + + private static String camelCaseField(String value) { + return StringUtils.uncapitalize(Arrays.stream(value.split(" ")).map(StringUtils::capitalize).collect(Collectors.joining())); + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/CucumberTest.java.mustache b/src/main/resources/generator/server/springboot/cucumber/CucumberTest.java.mustache new file mode 100644 index 00000000000..d81c01c1bf7 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/CucumberTest.java.mustache @@ -0,0 +1,17 @@ +package {{packageName}}.cucumber; + +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; +import org.junit.runner.RunWith; +import {{packageName}}.ComponentTest; + +@ComponentTest +@RunWith(Cucumber.class) +@CucumberOptions( + glue = "{{packageName}}", + plugin = { + "pretty", "json:target/cucumber/cucumber.json", "html:target/cucumber/cucumber.htm", "junit:target/cucumber/TEST-cucumber.xml", + }, + features = "src/test/features" +) +public class CucumberTest {} diff --git a/src/main/resources/generator/server/springboot/cucumber/CucumberTestContext.java.mustache b/src/main/resources/generator/server/springboot/cucumber/CucumberTestContext.java.mustache new file mode 100644 index 00000000000..6a844ca61de --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/CucumberTestContext.java.mustache @@ -0,0 +1,208 @@ +package {{packageName}}.cucumber; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.Option; +import com.jayway.jsonpath.PathNotFoundException; +import com.jayway.jsonpath.spi.json.JsonProvider; +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.Function; +import net.minidev.json.JSONArray; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Assertions; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StreamUtils; + +public final class CucumberTestContext { + + private static final Deque queries = new ConcurrentLinkedDeque<>(); + private static JsonProvider jsonReader = Configuration.defaultConfiguration().addOptions(Option.SUPPRESS_EXCEPTIONS).jsonProvider(); + + private CucumberTestContext() {} + + public static void addResponse(HttpRequest request, ClientHttpResponse response, ClientHttpRequestExecution execution, byte[] body) { + queries.addFirst(new RestQuery(request, response, execution, body)); + } + + public static Object getElement(String jsonPath) { + return lastQuery().response().map(toElement(jsonPath)).orElse(null); + } + + public static Object getElement(String uri, String jsonPath) { + return queries + .stream() + .filter(query -> query.response.isPresent() && StringUtils.isNotBlank(query.response.get())) + .filter(query -> query.forUri(uri)) + .findFirst() + .flatMap(response -> response.response.map(toElement(jsonPath))) + .orElse(null); + } + + public static List getResponseHeader(String header) { + return Collections.unmodifiableList(lastQuery().responseHeaders().get(header)); + } + + public static int countEntries(String jsonPath) { + return lastQuery().response().map(toEntriesCount(jsonPath)).orElse(0); + } + + private static Function toEntriesCount(String jsonPath) { + return response -> { + Object element = null; + try { + element = JsonPath.read(jsonReader.parse(response), jsonPath); + } catch (PathNotFoundException e) { + return 0; + } + + if (!(element instanceof JSONArray)) { + return 1; + } + + JSONArray array = (JSONArray) element; + if (array.isEmpty()) { + return 0; + } + + return array.size(); + }; + } + + private static Function toElement(String jsonPath) { + return response -> { + try { + Object element = JsonPath.read(jsonReader.parse(response), jsonPath); + + return element; + } catch (PathNotFoundException e) { + return null; + } + }; + } + + public static HttpStatus getStatus() { + return lastQuery().status(); + } + + public static String getUri() { + return lastQuery().uri(); + } + + public static Optional getResponse() { + return lastQuery().response(); + } + + public static void reset() { + queries.clear(); + } + + public static void retry() { + lastQuery().retry(); + } + + private static RestQuery lastQuery() { + try { + return queries.getFirst(); + } catch (NoSuchElementException e) { + throw new AssertionError("Can't get last query: empty queries"); + } + } + + private static class RestQuery { + + private static final String URI_MATCHER = ".*\\/%s(\\/[\\w-]*\\/?)?"; + + private final HttpRequest request; + private final String uri; + private final HttpStatus status; + private final Optional response; + private final HttpHeaders responseHeaders; + private final ClientHttpRequestExecution execution; + private final byte[] body; + + public RestQuery(HttpRequest request, ClientHttpResponse response, ClientHttpRequestExecution execution, byte[] body) { + this.request = request; + try { + uri = URLDecoder.decode(request.getURI().toString(), StandardCharsets.UTF_8.displayName()); + responseHeaders = response.getHeaders(); + status = response.getStatusCode(); + } catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + + this.response = readResponse(response); + this.execution = execution; + this.body = body; + } + + private Optional readResponse(ClientHttpResponse response) { + try { + return Optional.of(StreamUtils.copyToString(response.getBody(), Charset.defaultCharset())); + } catch (Exception e) { + return Optional.empty(); + } + } + + public String uri() { + return uri; + } + + private HttpStatus status() { + return status; + } + + private Optional response() { + return response; + } + + private HttpHeaders responseHeaders() { + return responseHeaders; + } + + /** + * Matches the supplied URI respecting REST principles. + * + *
+     * true  = "/api/working-folders".forUri("working-folders")
+     * true  = "/api/working-folders/".forUri("working-folders")
+     * true  = "/api/working-folders/e47df162-f397".forUri("working-folders")
+     * true  = "/api/working-folders/e47df162-f397/".forUri("working-folders")
+     * false = "/api/working-folders/e47df162-f397/commentaries".forUri("working-folders")
+     * false = "/api/working-folders/e47df162-f397/commentaries/".forUri("working-folders")
+     * false = "/api/working-folders/e47df162-f397/commentaries/955eea5e-9fbf".forUri("working-folders")
+     * false = "/api/working-folders/e47df162-f397/commentaries/955eea5e-9fbf/".forUri("working-folders")
+     * 
+ * + * @param uri + * name of a REST resource, such as "working-folders" + */ + private boolean forUri(String uri) { + if (!uri.matches("[\\w-]+")) Assertions.fail("URI should be the name of a REST resource"); + + return this.uri.matches(String.format(URI_MATCHER, uri)); + } + + private void retry() { + try { + ClientHttpResponse response = execution.execute(request, body); + + CucumberTestContext.addResponse(request, response, execution, body); + } catch (IOException e) { + throw new AssertionError("Error while retrying last call: " + e.getMessage(), e); + } + } + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/CucumberTestContextUnitTest.java.mustache b/src/main/resources/generator/server/springboot/cucumber/CucumberTestContextUnitTest.java.mustache new file mode 100644 index 00000000000..9252a00129e --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/CucumberTestContextUnitTest.java.mustache @@ -0,0 +1,166 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpResponse; +import {{packageName}}.UnitTest; + +@UnitTest +class CucumberTestContextUnitTest { + + @Test + void shouldGetQueryInformation() { + addQuery("{}"); + + assertThat(CucumberTestContext.getStatus()).isEqualTo(HttpStatus.OK); + assertThat(CucumberTestContext.getUri()).isEqualTo("http://localhost/"); + assertThat(CucumberTestContext.getResponse()).contains("{}"); + } + + @Test + void shouldResetTestContext() { + addQuery("{}"); + + CucumberTestContext.reset(); + + assertThatThrownBy(() -> CucumberTestContext.getResponse()).isExactlyInstanceOf(AssertionError.class).hasMessageContaining("empty"); + } + + @Test + void shouldGetZeroEntriesForUnknownPath() { + addQuery("{}"); + + assertThat(CucumberTestContext.countEntries("$.dummy")).isZero(); + } + + @Test + void shouldGetNulForUnknownElement() { + addQuery("{\"element\":\"value\"}"); + + assertThat(CucumberTestContext.getElement("dummy")).isNull(); + } + + @Test + void shouldGetElement() { + addQuery("{\"element\":\"value\"}"); + + assertThat(CucumberTestContext.getElement("element")).isEqualTo("value"); + } + + @Test + void shouldGetElementWithPath() { + addQuery("/dummies", "{\"element\":\"old\"}"); + addQuery("/dummies", "{\"element\":\"value\"}"); + addQuery("/dumm", "{\"element\":\"dd\"}"); + addQuery("/employees", "{\"element\":\"test\"}"); + + assertThat(CucumberTestContext.getElement("dummies", "element")).isEqualTo("value"); + } + + @Test + void shouldGetOneEntryForSingleElement() { + addQuery("{\"element\":\"value\"}"); + + assertThat(CucumberTestContext.countEntries("$.element")).isEqualTo(1); + } + + @Test + void shouldGetZeroEntryForEmptyArray() { + addQuery("{\"element\":[]}"); + + assertThat(CucumberTestContext.countEntries("$.element")).isZero(); + } + + @Test + void shouldGetArraySize() { + addQuery("{\"element\":[{\"key\":\"value\"},{\"key\":\"value\"}]}"); + + assertThat(CucumberTestContext.countEntries("$.element")).isEqualTo(2); + } + + @Test + void shouldNotReadStatusCodeForUreadableStatusCode() throws UnsupportedEncodingException { + ClientHttpResponse httpResponse = mock(ClientHttpResponse.class); + try { + when(httpResponse.getStatusCode()).thenThrow(IOException.class); + } catch (IOException e) { + fail(e.getMessage()); + } + + assertThatThrownBy(() -> + CucumberTestContext.addResponse(mockedRequest("/"), httpResponse, mock(ClientHttpRequestExecution.class), "body".getBytes()) + ) + .isExactlyInstanceOf(AssertionError.class); + } + + @Test + void shouldNotReadResponseCodeForUreadableResponse() throws UnsupportedEncodingException { + ClientHttpResponse httpResponse = mock(ClientHttpResponse.class); + try { + when(httpResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(httpResponse.getBody()).thenThrow(IOException.class); + } catch (IOException e) { + fail(e.getMessage()); + } + + CucumberTestContext.addResponse(mockedRequest("/"), httpResponse, mock(ClientHttpRequestExecution.class), "body".getBytes()); + + assertThat(CucumberTestContext.getResponse()).isEmpty(); + } + + @Test + void shouldGracefullyHandleRetryErrors() throws IOException { + byte[] body = "body".getBytes(); + + ClientHttpRequestExecution execution = mock(ClientHttpRequestExecution.class); + HttpRequest request = mockedRequest("/"); + when(execution.execute(request, body)).thenThrow(IOException.class); + + CucumberTestContext.addResponse(request, mockedResponse("response"), execution, body); + + assertThatThrownBy(() -> CucumberTestContext.retry()).isExactlyInstanceOf(AssertionError.class).hasMessageContaining("retrying"); + } + + private void addQuery(String response) { + addQuery("/", response); + } + + private void addQuery(String path, String response) { + ClientHttpResponse httpResponse = mockedResponse(response); + + CucumberTestContext.addResponse(mockedRequest(path), httpResponse, mock(ClientHttpRequestExecution.class), "body".getBytes()); + } + + private ClientHttpResponse mockedResponse(String response) { + ClientHttpResponse httpResponse = mock(ClientHttpResponse.class); + + try { + when(httpResponse.getStatusCode()).thenReturn(HttpStatus.OK); + when(httpResponse.getBody()).thenReturn(new ByteArrayInputStream(response.getBytes())); + } catch (IOException e) { + fail(e.getMessage()); + } + + return httpResponse; + } + + private HttpRequest mockedRequest(String path) { + HttpRequest request = mock(HttpRequest.class); + try { + when(request.getURI()).thenReturn(new URI("http://localhost" + path)); + } catch (URISyntaxException e) { + fail(e.getMessage()); + } + return request; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/ElementAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/ElementAsserter.java.mustache new file mode 100644 index 00000000000..0a74e226bf0 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/ElementAsserter.java.mustache @@ -0,0 +1,31 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +public interface ElementAsserter { + ElementAsserter withValue(Object value); + + ElementAsserter containingExactly(List> responses); + + ElementAsserter containing(Map response); + + ElementAsserter withElementsCount(int count); + + ElementAsserter withMoreThanElementsCount(int count); + + ElementAsserter containing(List> responses); + + ElementAsserter withValues(Collection values); + + T and(); + + default ElementAsserter withValues(String... values) { + assertThat(values).as("Can't check object agains null values").isNotNull(); + + return withValues(List.of(values)); + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/ElementAssertions.java.mustache b/src/main/resources/generator/server/springboot/cucumber/ElementAssertions.java.mustache new file mode 100644 index 00000000000..fedd47d525e --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/ElementAssertions.java.mustache @@ -0,0 +1,97 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import net.minidev.json.JSONArray; + +class ElementAssertions { + + private final String jsonPath; + + public ElementAssertions(String jsonPath) { + this.jsonPath = buildJsonPath(jsonPath); + + assertThat(CucumberTestContext.getElement(this.jsonPath)).as("Can't find " + this.jsonPath + " in response").isNotNull(); + } + + private String buildJsonPath(String jsonPath) { + if (jsonPath == null) { + return "$"; + } + + return jsonPath; + } + + void withValue(Object value) { + assertPathValue(jsonPath, value); + } + + void containingExactly(List> responses) { + withElementsCount(responses.size()); + containing(responses); + } + + void containing(Map response) { + assertThat(response).as("Can't check object agains a null response").isNotNull(); + + response.entrySet().forEach(entry -> assertPathValue(jsonPath + "." + CucumberJson.toCamelCase(entry.getKey()), entry.getValue())); + } + + void withElementsCount(int count) { + int elementsCount = CucumberTestContext.countEntries(jsonPath); + + assertThat(elementsCount) + .as("Expecting " + count + " element(s) in " + jsonPath + " but got " + elementsCount + CucumberAssertions.callContext()) + .isEqualTo(count); + } + + void withMoreThanElementsCount(int count) { + int elementsCount = CucumberTestContext.countEntries(jsonPath); + + assertThat(elementsCount) + .as("Expecting at least " + count + " element(s) in " + jsonPath + " but got " + elementsCount + CucumberAssertions.callContext()) + .isGreaterThanOrEqualTo(count); + } + + void containing(List> responses) { + assertThat(responses).as("Can't check object agains null responses").isNotNull(); + + for (int line = 0; line < responses.size(); line++) { + for (Map.Entry entry : responses.get(line).entrySet()) { + String path = jsonPath + "[" + line + "]." + CucumberJson.toCamelCase(entry.getKey()); + + assertPathValue(path, entry.getValue()); + } + } + } + + void withValues(Collection values) { + assertThat(values).as("Can't check object agains null values").isNotNull(); + + Object responseValue = CucumberTestContext.getElement(jsonPath); + + if (!(responseValue instanceof JSONArray)) { + fail(jsonPath + " is not an array"); + } + + JSONArray array = (JSONArray) responseValue; + assertThat(array) + .as("Expecting " + jsonPath + " to contain " + values + " but got " + responseValue + CucumberAssertions.callContext()) + .containsExactlyElementsOf(values); + } + + private void assertPathValue(String jsonPath, Object value) { + Object responseValue = CucumberTestContext.getElement(jsonPath); + + if (responseValue != null && value instanceof String && !(responseValue instanceof String)) { + responseValue = responseValue.toString(); + } + + assertThat(responseValue) + .as("Expecting " + jsonPath + " to contain " + value + " but got " + responseValue + CucumberAssertions.callContext()) + .isEqualTo(value); + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/HeaderAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/HeaderAsserter.java.mustache new file mode 100644 index 00000000000..222539ff6ef --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/HeaderAsserter.java.mustache @@ -0,0 +1,9 @@ +package {{packageName}}.cucumber; + +public interface HeaderAsserter { + HeaderAsserter containing(String value); + + HeaderAsserter startingWith(String prefix); + + T and(); +} diff --git a/src/main/resources/generator/server/springboot/cucumber/HeaderAssertions.java.mustache b/src/main/resources/generator/server/springboot/cucumber/HeaderAssertions.java.mustache new file mode 100644 index 00000000000..d978d610aef --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/HeaderAssertions.java.mustache @@ -0,0 +1,36 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; +import java.util.stream.Collectors; + +class HeaderAssertions { + + private final String header; + + HeaderAssertions(String header) { + this.header = header; + + assertThat(CucumberTestContext.getResponseHeader(header)).as("Can't find header " + header).isNotEmpty(); + } + + public void containing(String value) { + List values = CucumberTestContext.getResponseHeader(header); + assertThat(values) + .as("Expecting header " + header + " to contain " + value + " but can't find it, current values are " + displayValues(values)) + .contains(value); + } + + public void startingWith(String prefix) { + List values = CucumberTestContext.getResponseHeader(header); + + assertThat(values) + .as("Expecting header " + header + " to start with " + prefix + " but can't find it, current values are " + displayValues(values)) + .anyMatch(content -> content.startsWith(prefix)); + } + + private String displayValues(List values) { + return values.stream().collect(Collectors.joining(", ")); + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/ResponseAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/ResponseAsserter.java.mustache new file mode 100644 index 00000000000..acb14f0df87 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/ResponseAsserter.java.mustache @@ -0,0 +1,32 @@ +package {{packageName}}.cucumber; + +import java.util.stream.Stream; +import org.springframework.http.HttpStatus; + +public interface ResponseAsserter { + ResponseAsserter hasHttpStatus(HttpStatus status); + + ResponseAsserter hasHttpStatusIn(HttpStatus... statuses); + + ElementAsserter hasElement(String jsonPath); + + HeaderAsserter hasHeader(String header); + + public ResponseAsserter hasRawBody(String info); + + default ElementAsserter hasResponse() { + return hasElement(null); + } + + default ResponseAsserter hasOkStatus() { + return hasHttpStatus(200); + } + + default ResponseAsserter hasHttpStatus(int status) { + return hasHttpStatus(HttpStatus.valueOf(status)); + } + + default ResponseAsserter hasHttpStatusIn(Integer... statuses) { + return hasHttpStatusIn(Stream.of(statuses).map(HttpStatus::valueOf).toArray(HttpStatus[]::new)); + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/SyncElementAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/SyncElementAsserter.java.mustache new file mode 100644 index 00000000000..99b13df2204 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/SyncElementAsserter.java.mustache @@ -0,0 +1,69 @@ +package {{packageName}}.cucumber; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +class SyncElementAsserter implements ElementAsserter { + + private final SyncResponseAsserter responseAsserter; + private final ElementAssertions assertions; + + SyncElementAsserter(SyncResponseAsserter responseAsserter, String jsonPath) { + this.responseAsserter = responseAsserter; + assertions = new ElementAssertions(jsonPath); + } + + @Override + public SyncElementAsserter withValue(Object value) { + assertions.withValue(value); + + return this; + } + + @Override + public SyncElementAsserter containingExactly(List> responses) { + assertions.containingExactly(responses); + + return this; + } + + @Override + public SyncElementAsserter containing(Map response) { + assertions.containing(response); + + return this; + } + + @Override + public SyncElementAsserter withElementsCount(int count) { + assertions.withElementsCount(count); + + return this; + } + + @Override + public SyncElementAsserter withMoreThanElementsCount(int count) { + assertions.withMoreThanElementsCount(count); + + return this; + } + + @Override + public SyncElementAsserter containing(List> responses) { + assertions.containing(responses); + + return this; + } + + @Override + public SyncElementAsserter withValues(Collection values) { + assertions.withValues(values); + + return this; + } + + public SyncResponseAsserter and() { + return responseAsserter; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/SyncHeaderAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/SyncHeaderAsserter.java.mustache new file mode 100644 index 00000000000..abe4713986d --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/SyncHeaderAsserter.java.mustache @@ -0,0 +1,31 @@ +package {{packageName}}.cucumber; + +class SyncHeaderAsserter implements HeaderAsserter { + + private final SyncResponseAsserter responseAsserter; + private final HeaderAssertions assertions; + + SyncHeaderAsserter(SyncResponseAsserter responseAsserter, String header) { + this.responseAsserter = responseAsserter; + assertions = new HeaderAssertions(header); + } + + @Override + public SyncHeaderAsserter containing(String value) { + assertions.containing(value); + + return this; + } + + @Override + public SyncHeaderAsserter startingWith(String prefix) { + assertions.startingWith(prefix); + + return this; + } + + @Override + public SyncResponseAsserter and() { + return responseAsserter; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/SyncResponseAsserter.java.mustache b/src/main/resources/generator/server/springboot/cucumber/SyncResponseAsserter.java.mustache new file mode 100644 index 00000000000..b01770c16e2 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/SyncResponseAsserter.java.mustache @@ -0,0 +1,47 @@ +package {{packageName}}.cucumber; + +import static org.assertj.core.api.Assertions.*; + +import org.springframework.http.HttpStatus; + +class SyncResponseAsserter implements ResponseAsserter { + + @Override + public SyncResponseAsserter hasHttpStatus(HttpStatus status) { + CucumberAssertions.assertHttpStatus(status); + + return this; + } + + @Override + public ResponseAsserter hasHttpStatusIn(HttpStatus... statuses) { + CucumberAssertions.assertHttpStatusIn(statuses); + + return this; + } + + @Override + public SyncElementAsserter hasElement(String jsonPath) { + return new SyncElementAsserter(this, jsonPath); + } + + @Override + public SyncHeaderAsserter hasHeader(String header) { + return new SyncHeaderAsserter(this, header); + } + + public SyncResponseAsserter doNotHaveElement(String jsonPath) { + int elementsCount = CucumberTestContext.countEntries(jsonPath); + + assertThat(elementsCount).as("Expecting " + jsonPath + " to not exists " + CucumberAssertions.callContext()).isZero(); + + return this; + } + + @Override + public SyncResponseAsserter hasRawBody(String info) { + assertThat(CucumberAssertions.responseBody()).isEqualTo(info); + + return this; + } +} diff --git a/src/main/resources/generator/server/springboot/cucumber/cucumber.md.mustache b/src/main/resources/generator/server/springboot/cucumber/cucumber.md.mustache new file mode 100644 index 00000000000..6a10d40f286 --- /dev/null +++ b/src/main/resources/generator/server/springboot/cucumber/cucumber.md.mustache @@ -0,0 +1,148 @@ +# Usage + +In `src/test/features` create a feature file: + +``` +Feature: Simple WebService test + Background: + Given I am logged in as "user" + + Scenario: Calling WebService with static assertion + When I get simple bean "Bob" + Then I get simple response with name "Bob" and age 42 +``` + +You'll then have to define the glue code: + +```java +import static {{packageName}}.cucumber.CucumberAssertions.*; + +import io.cucumber.java.en.Then; +import io.cucumber.java.en.When; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.web.client.TestRestTemplate; + +public class SimpleSteps { + @Autowired + private TestRestTemplate rest; + + @When("I get simple bean {string}") + public void callSimpleWebService(String bean) { + rest.getForEntity("/test-resources/" + bean, Void.class); + } + + @Then("I get simple response with name {string} and age {int}") + public void shouldGetResponse(String name, int age) { + assertThatLastResponse() + .hasOkStatus() + .hasElement("$.name") + .withValue(name) + .and() + .hasElement("$.age") + .withValue(age); + } +} +``` + +Use a `TestRestTemplate` to make your rest calls so you'll have the `context` management: the stuff allowing easier assertions in the `Then` steps. + +The `assertThatLastResponse()` is a fluent API to assert your WebServices results. + +As an example, you can use results arrays in your features: + +``` + Scenario: Calling WebService with line presentation table + Given I am logged in as "user" + When I get simple bean "Bill" + Then I should get simple bean + | Name | Bill | + | Age | 42 | + + Scenario: Calling WebServices with all users + When I get all simple beans + Then I should get simple beans + | Name | Age | + | Bob | 42 | + | Bill | 50 | +``` + +And validate them easily with those assertions: + +```java +@Then("I should get simple bean") +public void shouldGetResponseContent(Map response) { + assertThatLastResponse().hasResponse().containing(response); +} + +@Then("I should get simple beans") +public void shouldGetResponseContent(List> responses) { + assertThatLastResponse().hasElement("$.users").containingExactly(responses); +} +``` + +## Reading responses content + +Sometimes you may want to access the last response content without asserting it, you can do: + +```java +CucumberTestContext.getElement("$.path"); +CucumberTestContext.getElement("uri-path", "$.path"); +CucumberTestContext.countEntries("$.path"); +``` + +## Async services + +Sometimes you have to validate behavior of async operations. You can do: + +```java +assertThatLastAsyncResponse().hasOkStatus(); +``` + +To have a default waiting time of 5 second or you can get a custom max with: + +```java +assertThatLastAsyncResponse(Duration.ofSeconds(30)).hasOkStatus(); +``` + +Behind the scene, your last service will be recalled until the assertions are OK or you reach the timeout. + +## Mocking beans + +You may need to mock beans for your component tests but you won't be able to do it in a "classic" way (using `@MockBean`) since the application context will be already loaded. A way to achieve that is to overload beans to have mocks: + +```java +@RunWith(Cucumber.class) +@CucumberContextConfiguration +@CucumberOptions( + glue = "{{packageName}}", + plugin = { + "pretty", + "json:target/cucumber/cucumber.json", + "html:target/cucumber/cucumber.htm", + "junit:target/cucumber/cucumber.xml" + }, + features = "src/test/features" +) +@SpringBootTest( + classes = { + {{applicationName}}App.class, + CucumberConfiguration.class, + CucumberMocksConfiguration.class + }, + webEnvironment = WebEnvironment.RANDOM_PORT +) +public class CucumberTest { + + @TestConfiguration + public static class CucumberMocksConfiguration { + + @Bean + @Primary + public AccountsRepository cucumberAccountsRepository() { + return Mockito.mock(AccountsRepository.class); + } + } +} +``` + +**Careful: the mock bean names (by default the method name) must be different from the real one or else they may just be ignored** diff --git a/src/main/resources/generator/server/springboot/cucumber/gitkeep b/src/main/resources/generator/server/springboot/cucumber/gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/test/features/cucumber.feature b/src/test/features/cucumber.feature new file mode 100644 index 00000000000..1f59323eee8 --- /dev/null +++ b/src/test/features/cucumber.feature @@ -0,0 +1,6 @@ +Feature: Cucumber module + + Scenario: Should add cucumber elements + When I add cucumber to default project with maven file + Then I should have files in "src/test/java/tech/jhipster/chips/cucumber" + | CucumberConfiguration.java | \ No newline at end of file diff --git a/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java b/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java index f1577104a56..9085dd08b82 100644 --- a/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java +++ b/src/test/java/tech/jhipster/lite/generator/ModulesSteps.java @@ -2,6 +2,10 @@ import static tech.jhipster.lite.generator.ProjectsSteps.*; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.web.client.TestRestTemplate; @@ -23,6 +27,30 @@ protected void applyModuleForDefaultProject(String moduleUrl) { post(moduleUrl, JsonHelper.writeAsString(project)); } + protected void applyModuleForDefaultProjectWithMavenFile(String moduleUrl) { + ProjectDTO project = newDefaultProjectDto(); + + addPomToproject(project.getFolder()); + + post(moduleUrl, JsonHelper.writeAsString(project)); + } + + private static void addPomToproject(String folder) { + Path folderPath = Paths.get(folder); + try { + Files.createDirectories(folderPath); + } catch (IOException e) { + throw new AssertionError(e); + } + + Path pomPath = folderPath.resolve("pom.xml"); + try { + Files.copy(Paths.get("src/test/resources/projects/maven/pom.xml"), pomPath); + } catch (IOException e) { + throw new AssertionError(e); + } + } + private void post(String uri, String content) { rest.exchange(uri, HttpMethod.POST, new HttpEntity<>(content, jsonHeaders()), Void.class); } diff --git a/src/test/java/tech/jhipster/lite/generator/ProjectsSteps.java b/src/test/java/tech/jhipster/lite/generator/ProjectsSteps.java index 30d06dc4a1f..6f132d57a5d 100644 --- a/src/test/java/tech/jhipster/lite/generator/ProjectsSteps.java +++ b/src/test/java/tech/jhipster/lite/generator/ProjectsSteps.java @@ -2,6 +2,7 @@ import static tech.jhipster.lite.common.domain.FileUtils.*; +import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import java.io.IOException; import java.nio.file.Files; @@ -20,6 +21,11 @@ public class ProjectsSteps { private static String lastProjectFolder; + @Given("I have a default project with a maven file") + public void createDefaultProjectWithMavenFile() { + // TODO + } + public static ProjectDTO newDefaultProjectDto() { lastProjectFolder = tmpDirForTest(); diff --git a/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/FileSystemModulesRepositoryTest.java b/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/FileSystemModulesRepositoryTest.java index 717789e3cb1..0f62c3b8fb4 100644 --- a/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/FileSystemModulesRepositoryTest.java +++ b/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/FileSystemModulesRepositoryTest.java @@ -4,16 +4,11 @@ import static tech.jhipster.lite.generator.module.infrastructure.secondary.JHipsterModulesAssertions.*; import ch.qos.logback.classic.Level; -import java.io.IOException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import tech.jhipster.lite.LogSpy; import tech.jhipster.lite.UnitTest; import tech.jhipster.lite.generator.module.domain.JHipsterModule; -import tech.jhipster.lite.generator.module.domain.JHipsterProjectFolder; @UnitTest @ExtendWith(LogSpy.class) @@ -28,10 +23,9 @@ public FileSystemModulesRepositoryTest(LogSpy logs) { @Test void shouldApplyModule() { JHipsterModule module = module(); - addPomToproject(module.projectFolder()); // @formatter:off - assertThatModule(module) + assertThatModuleOnProjectWithDefaultPom(module) .createFiles( "src/main/java/com/company/myapp/MyApp.java", "src/main/java/com/company/myapp/errors/Assert.java", @@ -73,20 +67,4 @@ void shouldApplyModule() { logs.assertLogged(Level.DEBUG, "Applying fixture module"); logs.assertLogged(Level.DEBUG, "You shouldn't add this by default in your modules :D"); } - - private static void addPomToproject(JHipsterProjectFolder project) { - Path folder = Paths.get(project.folder()); - try { - Files.createDirectories(folder); - } catch (IOException e) { - throw new AssertionError(e); - } - - Path pomPath = folder.resolve("pom.xml"); - try { - Files.copy(Paths.get("src/test/resources/projects/maven/pom.xml"), pomPath); - } catch (IOException e) { - throw new AssertionError(e); - } - } } diff --git a/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/JHipsterModulesAssertions.java b/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/JHipsterModulesAssertions.java index dea62bc99a5..b35ab382025 100644 --- a/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/JHipsterModulesAssertions.java +++ b/src/test/java/tech/jhipster/lite/generator/module/infrastructure/secondary/JHipsterModulesAssertions.java @@ -25,6 +25,28 @@ public static ModuleAsserter assertThatModule(JHipsterModule module) { return new ModuleAsserter(module); } + public static ModuleAsserter assertThatModuleOnProjectWithDefaultPom(JHipsterModule module) { + addPomToproject(module.projectFolder()); + + return new ModuleAsserter(module); + } + + private static void addPomToproject(JHipsterProjectFolder project) { + Path folder = Paths.get(project.folder()); + try { + Files.createDirectories(folder); + } catch (IOException e) { + throw new AssertionError(e); + } + + Path pomPath = folder.resolve("pom.xml"); + try { + Files.copy(Paths.get("src/test/resources/projects/maven/pom.xml"), pomPath); + } catch (IOException e) { + throw new AssertionError(e); + } + } + public static class ModuleAsserter { private static final String SLASH = "/"; diff --git a/src/test/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleFactoryTest.java new file mode 100644 index 00000000000..b7fbcacee82 --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/server/springboot/cucumber/domain/CucumberModuleFactoryTest.java @@ -0,0 +1,60 @@ +package tech.jhipster.lite.generator.server.springboot.cucumber.domain; + +import static tech.jhipster.lite.generator.module.infrastructure.secondary.JHipsterModulesAssertions.*; + +import org.junit.jupiter.api.Test; +import tech.jhipster.lite.UnitTest; +import tech.jhipster.lite.common.domain.FileUtils; +import tech.jhipster.lite.generator.module.domain.JHipsterModule; + +@UnitTest +class CucumberModuleFactoryTest { + + private static final CucumberModuleFactory factory = new CucumberModuleFactory(); + + @Test + void shouldCreateModule() { + CucumberModuleProperties properties = CucumberModuleProperties + .builder() + .project(FileUtils.tmpDirForTest()) + .basePackage("com.jhipster.test") + .projectBaseName("myapp") + .build(); + + JHipsterModule module = factory.buildModule(properties); + + assertThatModuleOnProjectWithDefaultPom(module) + .createPrefixedFiles( + "src/test/java/com/jhipster/test/cucumber", + "AsyncElementAsserter.java", + "AsyncHeaderAsserter.java", + "AsyncResponseAsserter.java", + "Awaiter.java", + "CucumberAssertions.java", + "CucumberConfiguration.java", + "CucumberJson.java", + "CucumberTest.java", + "CucumberTestContext.java", + "CucumberTestContextUnitTest.java", + "ElementAsserter.java", + "ElementAssertions.java", + "HeaderAsserter.java", + "HeaderAssertions.java", + "ResponseAsserter.java", + "SyncElementAsserter.java", + "SyncHeaderAsserter.java", + "SyncResponseAsserter.java" + ) + .createFile("documentation/cucumber.md") + .and() + .createFile("src/test/features/.gitkeep") + .and() + .createFile("pom.xml") + .containing("cucumber-junit") + .containing("cucumber-java") + .containing("cucumber-spring") + .containing("junit-vintage-engine") + .containing("testng") + .containing("awaitility"); + } +} diff --git a/src/test/java/tech/jhipster/lite/generator/server/springboot/cucumber/infrastructure/primary/CucumberSteps.java b/src/test/java/tech/jhipster/lite/generator/server/springboot/cucumber/infrastructure/primary/CucumberSteps.java new file mode 100644 index 00000000000..e876da1df8e --- /dev/null +++ b/src/test/java/tech/jhipster/lite/generator/server/springboot/cucumber/infrastructure/primary/CucumberSteps.java @@ -0,0 +1,12 @@ +package tech.jhipster.lite.generator.server.springboot.cucumber.infrastructure.primary; + +import io.cucumber.java.en.When; +import tech.jhipster.lite.generator.ModulesSteps; + +public class CucumberSteps extends ModulesSteps { + + @When("I add cucumber to default project with maven file") + public void addCucumberModule() { + applyModuleForDefaultProjectWithMavenFile("/api/servers/spring-boot/cucumber"); + } +} diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh index d4af70e635c..58c7dd12b50 100755 --- a/tests-ci/generate.sh +++ b/tests-ci/generate.sh @@ -104,6 +104,7 @@ elif [[ $application == 'fullapp' ]]; then callApi "/api/servers/spring-boot/databases/postgresql" callApi "/api/servers/spring-boot/features/user/postgresql" + callApi "/api/servers/spring-boot/cucumber" callApi "/api/servers/spring-boot/database-migration-tools/liquibase" callApi "/api/servers/spring-boot/database-migration-tools/liquibase/user"