diff --git a/.gitlab-common-ci.yml b/.gitlab-common-ci.yml index 83a16b5f02426ff136b4d2bf673a3da65dcc5daa..f5b1aa5c9254d0921c438e047364e4e3e7c8ca29 100644 --- a/.gitlab-common-ci.yml +++ b/.gitlab-common-ci.yml @@ -35,10 +35,10 @@ stages: - cd $PROJECT_FOLDER script: - ./mvnw -B -Pprod,swagger verify - - awk -F"," '{ branches += $6 + $7; covered += $7 } END { print covered, "/", branches, "branches covered"; print 100*covered/branches, "%covered" }' target/jacoco-aggregate/index.csv + - awk -F"," '{ branches += $6 + $7; covered += $7 } END { print covered, "/", branches, "branches covered"; print 100*covered/branches, "%covered" }' target/jacoco-aggregate/jacoco.csv artifacts: reports: - junit: $PROJECT_FOLDER/target/test-results/TEST-*.xml + junit: $PROJECT_FOLDER/target/test-results/**/TEST-*.xml paths: - $PROJECT_FOLDER/target/jacoco-aggregate expire_in: 1 day diff --git a/borestop/pom.xml b/borestop/pom.xml index 304e1901b7137ad0735b4b0e70dc288f001ec9e2..e984eed49a3d26ad914397b329e2f126ea9277aa 100644 --- a/borestop/pom.xml +++ b/borestop/pom.xml @@ -55,6 +55,7 @@ <validation-api.version>2.0.1.Final</validation-api.version> <jaxb-runtime.version>2.3.3</jaxb-runtime.version> <mapstruct.version>1.3.1.Final</mapstruct.version> + <cucumber.version>6.4.0</cucumber.version> <!-- Plugin versions --> <maven-clean-plugin.version>3.1.0</maven-clean-plugin.version> <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version> @@ -280,6 +281,30 @@ <artifactId>metrics-core</artifactId> </dependency> <!-- jhipster-needle-maven-add-dependency --> + <!-- Cucumber --> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-java</artifactId> + <version>${cucumber.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-junit</artifactId> + <version>${cucumber.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>io.cucumber</groupId> + <artifactId>cucumber-spring</artifactId> + <version>${cucumber.version}</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.vintage</groupId> + <artifactId>junit-vintage-engine</artifactId> + <scope>test</scope> + </dependency> </dependencies> <build> @@ -778,6 +803,7 @@ <excludes> <exclude>**/*IT*</exclude> <exclude>**/*IntTest*</exclude> + <exclude>**/*CucumberTest*</exclude> </excludes> </configuration> </plugin> @@ -795,6 +821,7 @@ <includes> <include>**/*IT*</include> <include>**/*IntTest*</include> + <include>**/*CucumberTest*</include> </includes> </configuration> <executions> diff --git a/borestop/src/main/java/com/ippon/borestop/config/SecurityConfiguration.java b/borestop/src/main/java/com/ippon/borestop/config/SecurityConfiguration.java index 48042231fe4163b056f386e8bf9aa80c8dcd293c..156106b65ffecb84db26038f3767bd453c8fd560 100644 --- a/borestop/src/main/java/com/ippon/borestop/config/SecurityConfiguration.java +++ b/borestop/src/main/java/com/ippon/borestop/config/SecurityConfiguration.java @@ -1,8 +1,9 @@ package com.ippon.borestop.config; import com.ippon.borestop.common.infrastructure.Generated; -import com.ippon.borestop.security.*; -import com.ippon.borestop.security.jwt.*; +import com.ippon.borestop.security.AuthoritiesConstants; +import com.ippon.borestop.security.jwt.JWTConfigurer; +import com.ippon.borestop.security.jwt.TokenProvider; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; import org.springframework.http.HttpMethod; @@ -15,6 +16,10 @@ import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.context.HttpSessionSecurityContextRepository; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.csrf.CsrfTokenRepository; import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; import org.springframework.web.filter.CorsFilter; import org.zalando.problem.spring.web.advice.security.SecurityProblemSupport; @@ -58,6 +63,7 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { // @formatter:off http .csrf() + .csrfTokenRepository(csrfTokenRepository()) .disable() .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) .exceptionHandling() @@ -91,10 +97,23 @@ public class SecurityConfiguration extends WebSecurityConfigurerAdapter { .and() .httpBasic() .and() - .apply(securityConfigurerAdapter()); + .apply(securityConfigurerAdapter()) + .and() + .securityContext() + .securityContextRepository(securityContextRepository()); // @formatter:on } + @Bean + public SecurityContextRepository securityContextRepository() { + return new HttpSessionSecurityContextRepository(); + } + + @Bean + public CsrfTokenRepository csrfTokenRepository() { + return CookieCsrfTokenRepository.withHttpOnlyFalse(); + } + private JWTConfigurer securityConfigurerAdapter() { return new JWTConfigurer(tokenProvider); } diff --git a/borestop/src/test/features/jhipster/account.feature b/borestop/src/test/features/jhipster/account.feature new file mode 100644 index 0000000000000000000000000000000000000000..5abae8a05fe8d2e9a822eaf1fe9a29ad1a466069 --- /dev/null +++ b/borestop/src/test/features/jhipster/account.feature @@ -0,0 +1,12 @@ +Feature: Accounts management + + Scenario: Can't get authentication for not authenticated user + Given I am not logged in + When I get my account information + Then I should not be authorized + + Scenario: Get account informations + Given I am logged in as "admin" + When I get my account information + Then My login should be "admin" + And My email should be "admin@localhost" \ No newline at end of file diff --git a/borestop/src/test/java/com/ippon/borestop/cucumber/CucumberConfiguration.java b/borestop/src/test/java/com/ippon/borestop/cucumber/CucumberConfiguration.java new file mode 100644 index 0000000000000000000000000000000000000000..093a704514541ef933d5043c97517ee7b60c6c14 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/cucumber/CucumberConfiguration.java @@ -0,0 +1,79 @@ +package com.ippon.borestop.cucumber; + +import com.ippon.borestop.BorestopApp; +import com.ippon.borestop.cucumber.CucumberConfiguration.CucumberSecurityContextConfiguration; +import com.ippon.borestop.infrastructure.primay.CucumberTestContext; +import io.cucumber.java.Before; +import io.cucumber.spring.CucumberContextConfiguration; +import io.github.jhipster.config.JHipsterConstants; +import java.nio.charset.StandardCharsets; +import java.util.List; +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.context.TestConfiguration; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Primary; +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.security.web.context.SecurityContextRepository; +import org.springframework.security.web.csrf.CsrfTokenRepository; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.client.RestTemplate; + +@CucumberContextConfiguration +@ActiveProfiles(JHipsterConstants.SPRING_PROFILE_TEST) +@SpringBootTest(classes = { BorestopApp.class, CucumberSecurityContextConfiguration.class }, webEnvironment = WebEnvironment.RANDOM_PORT) +public class CucumberConfiguration { + @Autowired + private TestRestTemplate rest; + + @Before + public void loadInterceptors() { + ClientHttpRequestFactory requestFactory = new BufferingClientHttpRequestFactory(new SimpleClientHttpRequestFactory()); + + RestTemplate template = rest.getRestTemplate(); + template.setRequestFactory(requestFactory); + template.setInterceptors(List.of(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); + + return response; + }; + } + + @TestConfiguration + public static class CucumberSecurityContextConfiguration { + + @Bean + @Primary + public SecurityContextRepository securityContextRepository() { + return new MockedSecurityContextRepository(); + } + + @Bean + @Primary + public CsrfTokenRepository csrfTokenRepository() { + return new MockedCsrfTokenRepository(); + } + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/cucumber/CucumberTest.java b/borestop/src/test/java/com/ippon/borestop/cucumber/CucumberTest.java new file mode 100644 index 0000000000000000000000000000000000000000..71d535be7bf3e2f37cb618f43df9447673397e49 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/cucumber/CucumberTest.java @@ -0,0 +1,13 @@ +package com.ippon.borestop.cucumber; + +import io.cucumber.junit.Cucumber; +import io.cucumber.junit.CucumberOptions; +import org.junit.runner.RunWith; + +@RunWith(Cucumber.class) +@CucumberOptions( + glue = "com.ippon.borestop", + plugin = { "pretty", "json:target/cucumber/cucumber.json", "html:target/cucumber/cucumber.htm" }, + features = "src/test/features" +) +public class CucumberTest {} diff --git a/borestop/src/test/java/com/ippon/borestop/cucumber/HttpSteps.java b/borestop/src/test/java/com/ippon/borestop/cucumber/HttpSteps.java new file mode 100644 index 0000000000000000000000000000000000000000..20dd152e4282f3718009751b95f6acb798a6a1c7 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/cucumber/HttpSteps.java @@ -0,0 +1,20 @@ +package com.ippon.borestop.cucumber; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ippon.borestop.infrastructure.primay.CucumberTestContext; +import io.cucumber.java.en.Then; +import org.springframework.http.HttpStatus; + +public class HttpSteps { + + @Then("I can't find document") + public void shouldGetNotFoundResult() { + assertThat(CucumberTestContext.getStatus()).isEqualTo(HttpStatus.NOT_FOUND); + } + + @Then("I should not be authorized") + public void shouldNotBeAuthorized() { + assertThat(CucumberTestContext.getStatus()).isIn(HttpStatus.UNAUTHORIZED, HttpStatus.FORBIDDEN); + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/cucumber/MockedCsrfTokenRepository.java b/borestop/src/test/java/com/ippon/borestop/cucumber/MockedCsrfTokenRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..12de4b68bbecf035030b0b2d78114a454f4d9d2c --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/cucumber/MockedCsrfTokenRepository.java @@ -0,0 +1,35 @@ +package com.ippon.borestop.cucumber; + +import static org.mockito.Mockito.*; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRepository; + +public class MockedCsrfTokenRepository implements CsrfTokenRepository { + private static final CsrfToken TOKEN = buildCsrfToken(); + + private static CsrfToken buildCsrfToken() { + CsrfToken token = mock(CsrfToken.class); + + when(token.getHeaderName()).thenReturn("mocked-csrf-token"); + when(token.getParameterName()).thenReturn("mocked-csrf-token"); + when(token.getToken()).thenReturn("MockedToken"); + + return token; + } + + @Override + public CsrfToken generateToken(HttpServletRequest request) { + return TOKEN; + } + + @Override + public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {} + + @Override + public CsrfToken loadToken(HttpServletRequest request) { + return TOKEN; + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/cucumber/MockedSecurityContextRepository.java b/borestop/src/test/java/com/ippon/borestop/cucumber/MockedSecurityContextRepository.java new file mode 100644 index 0000000000000000000000000000000000000000..6454a5613dd68ea7163c26f9729a4194059ebe75 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/cucumber/MockedSecurityContextRepository.java @@ -0,0 +1,30 @@ +package com.ippon.borestop.cucumber; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextImpl; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; + +public class MockedSecurityContextRepository implements SecurityContextRepository { + private Authentication authentication; + + public void authentication(Authentication authentication) { + this.authentication = authentication; + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return authentication != null; + } + + @Override + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + return new SecurityContextImpl(authentication); + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) {} +} diff --git a/borestop/src/test/java/com/ippon/borestop/infrastructure/primay/AuthenticationSteps.java b/borestop/src/test/java/com/ippon/borestop/infrastructure/primay/AuthenticationSteps.java new file mode 100644 index 0000000000000000000000000000000000000000..cce5c375771d687e979e97e73c61744ffa56b786 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/infrastructure/primay/AuthenticationSteps.java @@ -0,0 +1,44 @@ +package com.ippon.borestop.infrastructure.primay; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ippon.borestop.cucumber.MockedSecurityContextRepository; +import com.ippon.borestop.security.AuthoritiesConstants; +import io.cucumber.java.en.Given; +import io.cucumber.java.en.Then; +import java.util.Map; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.security.authentication.TestingAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.authority.AuthorityUtils; +import org.springframework.security.core.context.SecurityContextHolder; + +public class AuthenticationSteps { + @Autowired + private MockedSecurityContextRepository contexts; + + private static final Map<String, Authentication> USERS = Map.of( + "admin", + new TestingAuthenticationToken("admin", "N/A", AuthorityUtils.createAuthorityList(AuthoritiesConstants.ADMIN)) + ); + + @Given("I am logged in as {string}") + public void authenticateUser(String username) { + Authentication authentication = USERS.get(username); + SecurityContextHolder.getContext().setAuthentication(authentication); + + contexts.authentication(authentication); + } + + @Given("I logout") + @Given("I am not logged in") + public void logout() { + contexts.authentication(null); + } + + @Then("I should get an authorization error") + public void authorizationError() { + assertThat(CucumberTestContext.getStatus()).isEqualTo(HttpStatus.FORBIDDEN); + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/infrastructure/primay/CucumberTestContext.java b/borestop/src/test/java/com/ippon/borestop/infrastructure/primay/CucumberTestContext.java new file mode 100644 index 0000000000000000000000000000000000000000..0b015bc72a0a822c3da743a9b5a08c9307702e32 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/infrastructure/primay/CucumberTestContext.java @@ -0,0 +1,111 @@ +package com.ippon.borestop.infrastructure.primay; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.JsonPath; +import com.jayway.jsonpath.spi.json.JsonProvider; +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Deque; +import java.util.Optional; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.function.Function; +import java.util.stream.Collectors; +import net.minidev.json.JSONArray; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.StreamUtils; + +public final class CucumberTestContext { + private static final Deque<RestQuery> queries = new ConcurrentLinkedDeque<>(); + private static JsonProvider jsonReader = Configuration.defaultConfiguration().jsonProvider(); + + private CucumberTestContext() {} + + public static void addResponse(HttpRequest request, ClientHttpResponse response) { + queries.addFirst(new RestQuery(request, response)); + } + + public static HttpStatus getStatus() { + return queries.getFirst().getStatus(); + } + + public static <T> T getResponse(Class<T> responseClass) { + return queries.getFirst().getResponse().map(response -> TestJson.readFromJson(response, responseClass)).orElse(null); + } + + public static Object getElement(String jsonPath) { + return queries.getFirst().getResponse().map(toElement(jsonPath)).orElse(null); + } + + public static Object getElement(String uri, String jsonPath) { + return queries + .stream() + .filter(query -> query.forUri(uri)) + .findFirst() + .flatMap(response -> response.response.map(toElement(jsonPath))) + .orElse(null); + } + + private static Function<String, Object> toElement(String jsonPath) { + return response -> { + Object element = JsonPath.read(jsonReader.parse(response), jsonPath); + + if (element instanceof JSONArray) { + JSONArray elements = (JSONArray) element; + + if (elements.size() == 0) { + return null; + } + + return elements.stream().map(Object::toString).collect(Collectors.joining(", ")); + } + + return element; + }; + } + + public static String getCreatedWorkingFolderId() { + return (String) CucumberTestContext.getElement("working-folders", "$.id"); + } + + public static void reset() { + queries.clear(); + } + + private static class RestQuery { + private final String uri; + private final HttpStatus status; + private final Optional<String> response; + + public RestQuery(HttpRequest request, ClientHttpResponse response) { + uri = request.getURI().toString(); + try { + status = response.getStatusCode(); + this.response = readResponse(response); + } catch (IOException e) { + throw new AssertionError(e.getMessage(), e); + } + } + + private Optional<String> readResponse(ClientHttpResponse response) throws IOException { + try { + return Optional.of(StreamUtils.copyToString(response.getBody(), Charset.defaultCharset())); + } catch (Exception e) { + return Optional.empty(); + } + } + + private boolean forUri(String uri) { + return this.uri.contains(uri); + } + + private HttpStatus getStatus() { + return status; + } + + private Optional<String> getResponse() { + return response; + } + } +} diff --git a/borestop/src/test/java/com/ippon/borestop/web/rest/AccountsSteps.java b/borestop/src/test/java/com/ippon/borestop/web/rest/AccountsSteps.java new file mode 100644 index 0000000000000000000000000000000000000000..ea9274f83b21ab17fcc8b3cd40cfb6c1234bacc4 --- /dev/null +++ b/borestop/src/test/java/com/ippon/borestop/web/rest/AccountsSteps.java @@ -0,0 +1,29 @@ +package com.ippon.borestop.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; + +import com.ippon.borestop.infrastructure.primay.CucumberTestContext; +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 AccountsSteps { + @Autowired + private TestRestTemplate rest; + + @When("I get my account information") + public void getAccountInforamtion() { + rest.getForEntity("/api/account", Void.class); + } + + @Then("My login should be {string}") + public void shouldHaveLogin(String login) { + assertThat(CucumberTestContext.getElement("$.login")).isEqualTo(login); + } + + @Then("My email should be {string}") + public void shouldHaveEmail(String email) { + assertThat(CucumberTestContext.getElement("$.email")).isEqualTo(email); + } +}