From 395b23925afaa04bfa2b2ff821becf9eb078d64b Mon Sep 17 00:00:00 2001 From: Brutus5000 Date: Fri, 20 Oct 2017 22:45:36 +0200 Subject: [PATCH] integration test base setup --- .idea/compiler.xml | 6 +- .idea/modules.xml | 1 + build.gradle | 32 +++++ .../api/config/MockConfiguration.java | 19 +++ .../api/user/UserControllerTest.java | 111 ++++++++++++++++++ src/inttest/resources/config/application.yml | 31 +++++ src/inttest/resources/sql/createUsers.sql | 14 +++ .../GlobalControllerExceptionHandler.java | 17 +++ .../faforever/api/user/UserController.java | 4 +- .../api/data/JsonApiNameIntegrationTest.java | 92 --------------- .../faforever/integration/OAuthHelper.java | 45 +++++++ 11 files changed, 277 insertions(+), 95 deletions(-) create mode 100644 src/inttest/java/com/faforever/api/config/MockConfiguration.java create mode 100644 src/inttest/java/com/faforever/api/user/UserControllerTest.java create mode 100644 src/inttest/resources/config/application.yml create mode 100644 src/inttest/resources/sql/createUsers.sql delete mode 100644 src/test/java/com/faforever/api/data/JsonApiNameIntegrationTest.java create mode 100644 src/test/java/com/faforever/integration/OAuthHelper.java diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 91fbefd52..b17a32e91 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -5,8 +5,12 @@ + + + + - + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index fb4d22020..85562b843 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + diff --git a/build.gradle b/build.gradle index 0c77822c3..bd327bd0b 100644 --- a/build.gradle +++ b/build.gradle @@ -107,6 +107,7 @@ tasks.withType(Test) { apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'propdeps' +apply plugin: 'idea' group = 'faforever' version = '0.9.1' @@ -132,6 +133,12 @@ configurations { compile.exclude module: "assertj-core" } +idea { + module { + testSourceDirs += file('src/inttest/java') + } +} + processResources { filesMatching('**/application.yml') { filter { @@ -234,6 +241,31 @@ configurations.all { resolutionStrategy.cacheChangingModulesFor 60, 'seconds' } +sourceSets { + inttest { + java.srcDir 'src/inttest/java' + resources.srcDir 'src/inttest/resources' + compileClasspath += main.output + test.output + runtimeClasspath += main.output + test.output + } +} + +configurations { + inttestCompile.extendsFrom testCompile + inttestRuntime.extendsFrom testRuntime +} + + +task inttest(type: Test) { + group = LifecycleBasePlugin.VERIFICATION_GROUP + description = "Runs the integration tests." + + testClassesDir = sourceSets.inttest.output.classesDir + classpath = sourceSets.inttest.runtimeClasspath + mustRunAfter test +} +build.dependsOn inttest + dependencyManagement { dependencies { dependency("org.hibernate:hibernate-core:${hibernateVersion}") diff --git a/src/inttest/java/com/faforever/api/config/MockConfiguration.java b/src/inttest/java/com/faforever/api/config/MockConfiguration.java new file mode 100644 index 000000000..e6386e0a5 --- /dev/null +++ b/src/inttest/java/com/faforever/api/config/MockConfiguration.java @@ -0,0 +1,19 @@ +package com.faforever.api.config; + +import com.faforever.api.user.AnopeUserRepository; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.context.annotation.Profile; + +import static org.mockito.Mockito.mock; + +@Profile("integration-test") +@Configuration +public class MockConfiguration { + @Bean + @Primary + public AnopeUserRepository anopeUserRepository() { + return mock(AnopeUserRepository.class); + } +} diff --git a/src/inttest/java/com/faforever/api/user/UserControllerTest.java b/src/inttest/java/com/faforever/api/user/UserControllerTest.java new file mode 100644 index 000000000..03957b0ac --- /dev/null +++ b/src/inttest/java/com/faforever/api/user/UserControllerTest.java @@ -0,0 +1,111 @@ +package com.faforever.api.user; + +import com.faforever.api.data.domain.User; +import com.faforever.api.error.ErrorCode; +import com.faforever.api.security.OAuthScope; +import com.faforever.integration.OAuthHelper; +import com.google.common.collect.Sets; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.skyscreamer.jsonassert.JSONAssert; +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.context.annotation.Import; +import org.springframework.http.HttpHeaders; +import org.springframework.security.test.context.support.WithUserDetails; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.test.context.jdbc.Sql.ExecutionPhase; +import org.springframework.test.context.junit4.SpringRunner; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import org.springframework.test.web.servlet.request.RequestPostProcessor; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.util.MultiValueMap; +import org.springframework.web.context.WebApplicationContext; + +import javax.transaction.Transactional; +import java.util.Collections; + +import static junitx.framework.Assert.assertEquals; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringRunner.class) +@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) +@ActiveProfiles("integration-test") +@Import(OAuthHelper.class) +@Transactional +@Sql(executionPhase = ExecutionPhase.BEFORE_TEST_METHOD, scripts = "classpath:sql/createUsers.sql") +public class UserControllerTest { + protected final static String AUTH_USER = "USER"; + protected final static String AUTH_MODERATOR = "MODERATOR"; + protected final static String AUTH_ADMIN = "ADMIN"; + MockMvc mockMvc; + @Autowired + private WebApplicationContext context; + @Autowired + private OAuthHelper oAuthHelper; + + @Autowired + private UserRepository userRepository; + + @Before + public void setUp() { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(this.context) + .apply(springSecurity()) + .build(); + } + + void assertApiError(MvcResult mvcResult, ErrorCode errorCode) throws Exception { + JSONObject resonseJson = new JSONObject(mvcResult.getResponse().getContentAsString()); + JSONAssert.assertEquals(String.format("{\"errors\":[{\"code\":\"%s\"}]}", errorCode.getCode()), resonseJson, false); + } + + @Test + @WithUserDetails(AUTH_USER) + public void changePassword_success() throws Exception { + MultiValueMap params = new HttpHeaders(); + params.add("currentPassword", AUTH_USER); + params.add("newPassword", "newPassword"); + + RequestPostProcessor oauthToken = oAuthHelper.addBearerToken(Sets.newHashSet(OAuthScope._WRITE_ACCOUNT_DATA)); + mockMvc.perform(post("/users/changePassword").with(oauthToken).params(params)) + .andExpect(status().isOk()); + + User user = userRepository.findOneByLoginIgnoreCase(AUTH_USER).get(); + assertEquals(user.getPassword(), "5c29a959abce4eda5f0e7a4e7ea53dce4fa0f0abbe8eaa63717e2fed5f193d31"); + } + + @Test + @WithUserDetails(AUTH_USER) + public void changePassword_wrongScope() throws Exception { + MultiValueMap params = new HttpHeaders(); + params.add("currentPassword", AUTH_USER); + params.add("newPassword", "newPassword"); + + RequestPostProcessor oauthToken = oAuthHelper.addBearerToken(Collections.emptySet()); + mockMvc.perform(post("/users/changePassword").with(oauthToken).params(params)) + .andExpect(status().isForbidden()); + } + + @Test + @WithUserDetails(AUTH_USER) + public void changePassword_wrongPassword() throws Exception { + MultiValueMap params = new HttpHeaders(); + params.add("currentPassword", "wrongPassword"); + params.add("newPassword", "newPassword"); + + RequestPostProcessor oauthToken = oAuthHelper.addBearerToken(Sets.newHashSet(OAuthScope._WRITE_ACCOUNT_DATA)); + MvcResult mvcResult = mockMvc.perform(post("/users/changePassword").with(oauthToken).params(params)) + .andExpect(status().is4xxClientError()) + .andReturn(); + + assertApiError(mvcResult, ErrorCode.PASSWORD_CHANGE_FAILED_WRONG_PASSWORD); + } +} diff --git a/src/inttest/resources/config/application.yml b/src/inttest/resources/config/application.yml new file mode 100644 index 000000000..a1f8e07b9 --- /dev/null +++ b/src/inttest/resources/config/application.yml @@ -0,0 +1,31 @@ +spring: + profiles: + include: int + datasource: + url: jdbc:h2:mem:faf + username: root + password: banana + driverClassName: org.h2.Driver + jpa: + show-sql: true + hibernate: + ddl-auto: create + properties: + hibernate: + current_session_context_class: org.springframework.orm.hibernate5.SpringSessionContext + h2: + console: + enabled: true + +security: + oauth2: + resource: + filter-order: 3 + +faf-api: + jwt: + secret: banana + clan: + website-url-format: "http://example.com/%s" + user: + minimum-days-between-username-change: 30 diff --git a/src/inttest/resources/sql/createUsers.sql b/src/inttest/resources/sql/createUsers.sql new file mode 100644 index 000000000..77ecc872c --- /dev/null +++ b/src/inttest/resources/sql/createUsers.sql @@ -0,0 +1,14 @@ +DELETE FROM oauth_clients; +DELETE FROM login; + +INSERT INTO oauth_clients (id, name, client_secret, client_type, redirect_uris, default_redirect_uri, default_scope) +VALUES ('test', 'test', 'test', 'public', 'http://localhost', 'http://localhost', + 'read_events read_achievements upload_map upload_mod write_account_data'); + +INSERT INTO login (id, login, email, password) +VALUES (1, 'USER', 'user@faforever.com', '92b7b421992ef490f3b75898ec0e511f1a5c02422819d89719b20362b023ee4f'); +INSERT INTO login (id, login, email, password) +VALUES (2, 'MODERATOR', 'moderator@faforever.com', '778ac5b81fa251b450f827846378739caee510c31b01cfa9d31822b88bed8441'); +INSERT INTO login (id, login, email, password) +VALUES (3, 'ADMIN', 'admin@faforever.com', '835d6dc88b708bc646d6db82c853ef4182fabbd4a8de59c213f2b5ab3ae7d9be'); + diff --git a/src/main/java/com/faforever/api/error/GlobalControllerExceptionHandler.java b/src/main/java/com/faforever/api/error/GlobalControllerExceptionHandler.java index ba9dc43dc..15189850b 100644 --- a/src/main/java/com/faforever/api/error/GlobalControllerExceptionHandler.java +++ b/src/main/java/com/faforever/api/error/GlobalControllerExceptionHandler.java @@ -2,6 +2,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -91,6 +92,22 @@ public ErrorResponse processProgrammingError(ProgrammingError ex) { return response; } + + @ExceptionHandler(AccessDeniedException.class) + @ResponseStatus(HttpStatus.FORBIDDEN) + @ResponseBody + public ErrorResponse processAccessDeniedException(Throwable ex) throws MissingServletRequestPartException { + log.debug("Access denied", ex); + + ErrorResponse response = new ErrorResponse(); + response.addError(new ErrorResult( + String.valueOf(HttpStatus.FORBIDDEN.value()), + ex.getClass().getName(), + ex.getMessage() + )); + return response; + } + @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) @ResponseBody diff --git a/src/main/java/com/faforever/api/user/UserController.java b/src/main/java/com/faforever/api/user/UserController.java index 168e7b272..194e5278e 100644 --- a/src/main/java/com/faforever/api/user/UserController.java +++ b/src/main/java/com/faforever/api/user/UserController.java @@ -33,14 +33,14 @@ public void activate(@RequestParam("token") String token) { userService.activate(token); } - @PreAuthorize("#oauth2.hasScope('change_password') and hasRole('ROLE_USER')") + @PreAuthorize("#oauth2.hasScope('write_account_data') and hasRole('ROLE_USER')") @ApiOperation("Changes the password of a previously registered account.") @RequestMapping(path = "/changePassword", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public void changePassword(@RequestParam("currentPassword") String currentPassword, @RequestParam("newPassword") String newPassword, Authentication authentication) { userService.changePassword(currentPassword, newPassword, userService.getUser(authentication)); } - @PreAuthorize("#oauth2.hasScope('change_login') and hasRole('ROLE_USER')") + @PreAuthorize("#oauth2.hasScope('write_account_data') and hasRole('ROLE_USER')") @ApiOperation("Changes the login of a previously registered account.") @RequestMapping(path = "/changeUsername", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) public void changeLogin(@RequestParam("newUsername") String newUsername, Authentication authentication) { diff --git a/src/test/java/com/faforever/api/data/JsonApiNameIntegrationTest.java b/src/test/java/com/faforever/api/data/JsonApiNameIntegrationTest.java deleted file mode 100644 index d8c9db64a..000000000 --- a/src/test/java/com/faforever/api/data/JsonApiNameIntegrationTest.java +++ /dev/null @@ -1,92 +0,0 @@ -package com.faforever.api.data; - -import com.faforever.api.data.domain.NameRecord; -import com.faforever.api.data.domain.Player; -import com.faforever.integration.TestDatabase; -import com.faforever.integration.factories.NameRecordFactory; -import com.faforever.integration.factories.PlayerFactory; -import com.faforever.integration.utils.MockMvcHelper; -import lombok.SneakyThrows; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.context.annotation.Import; -import org.springframework.test.context.junit4.SpringRunner; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -import javax.inject.Inject; -import javax.servlet.Filter; - -import static org.hamcrest.Matchers.hasSize; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertEquals; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -@RunWith(SpringRunner.class) -@SpringBootTest -@Import(TestDatabase.class) -public class JsonApiNameIntegrationTest { - - private MockMvc mvc; - private WebApplicationContext context; - private Filter springSecurityFilterChain; - - private TestDatabase database; - - - @Inject - public void init(TestDatabase database, WebApplicationContext context, Filter springSecurityFilterChain) { - this.context = context; - this.springSecurityFilterChain = springSecurityFilterChain; - this.database = database; - } - - @Before - public void setUp() { - mvc = MockMvcBuilders - .webAppContextSetup(context) - .addFilter(springSecurityFilterChain) - .build(); - database.assertEmptyDatabase(); - } - - @After - public void tearDown() { - // TODO: This is needed, because Elide has some problems with @Transactional annotation #71 - database.tearDown(); - } - - - @Test - @SneakyThrows - public void getNames() { - Player player = PlayerFactory.builder() - .login("UseFactory") - .database(database) - .build(); - NameRecord entity = NameRecordFactory.builder().name("MyFirstName") - .id(999) - .player(player) - .database(database) - .build(); - - assertEquals(1, database.getNameRepository().count()); - MockMvcHelper.of(this.mvc).perform(get("/data/nameRecord")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.data", hasSize(1))) - .andExpect(jsonPath("$.data[0].id", is(String.valueOf(999)))) - .andExpect(jsonPath("$.data[0].type", is("nameRecord"))) - .andExpect(jsonPath("$.data[0].attributes.name", is(entity.getName()))) - .andExpect(jsonPath("$.data[0].relationships.player.data.id", - is(String.valueOf(player.getId())))) - .andExpect(jsonPath("$.data[0].relationships.player.data.type", is("player"))); - assertEquals(1, database.getNameRepository().count()); - } - -} diff --git a/src/test/java/com/faforever/integration/OAuthHelper.java b/src/test/java/com/faforever/integration/OAuthHelper.java new file mode 100644 index 000000000..bb4d021dc --- /dev/null +++ b/src/test/java/com/faforever/integration/OAuthHelper.java @@ -0,0 +1,45 @@ +package com.faforever.integration; + +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.common.OAuth2AccessToken; +import org.springframework.security.oauth2.provider.OAuth2Authentication; +import org.springframework.security.oauth2.provider.OAuth2Request; +import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.stereotype.Component; +import org.springframework.test.web.servlet.request.RequestPostProcessor; + +import java.util.Set; + +@Component +public class OAuthHelper { + + @Autowired + AuthorizationServerTokenServices tokenservice; + + @Autowired + JwtAccessTokenConverter jwtAccessTokenConverter; + + public RequestPostProcessor addBearerToken(Set scope) { + return mockRequest -> { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + OAuth2Request oauth2Request = createOAuth2Request(scope); + OAuth2Authentication oauth2auth = new OAuth2Authentication(oauth2Request, authentication); + OAuth2AccessToken token = tokenservice.createAccessToken(oauth2auth); + token = jwtAccessTokenConverter.enhance(token, oauth2auth); + + // Set Authorization header to use Bearer + mockRequest.addHeader("Authorization", "Bearer " + token.getValue()); + return mockRequest; + }; + } + + @NotNull + private OAuth2Request createOAuth2Request(Set scope) { + return new OAuth2Request(null, "test", null, true, scope, null, null, null, null); + } + +}