|
| 1 | +package com.baeldung.keycloaksoap; |
| 2 | + |
| 3 | +import com.baeldung.keycloak.keycloaksoap.KeycloakSoapServicesApplication; |
| 4 | +import com.fasterxml.jackson.databind.ObjectMapper; |
| 5 | +import org.junit.jupiter.api.DisplayName; |
| 6 | +import org.junit.jupiter.api.Test; |
| 7 | +import org.slf4j.Logger; |
| 8 | +import org.slf4j.LoggerFactory; |
| 9 | +import org.springframework.beans.factory.annotation.Autowired; |
| 10 | +import org.springframework.beans.factory.annotation.Value; |
| 11 | +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; |
| 12 | +import org.springframework.boot.test.context.SpringBootTest; |
| 13 | +import org.springframework.boot.test.web.client.TestRestTemplate; |
| 14 | +import org.springframework.boot.test.web.server.LocalServerPort; |
| 15 | +import org.springframework.http.HttpEntity; |
| 16 | +import org.springframework.http.HttpHeaders; |
| 17 | +import org.springframework.http.HttpMethod; |
| 18 | +import org.springframework.http.HttpStatus; |
| 19 | +import org.springframework.http.MediaType; |
| 20 | +import org.springframework.http.ResponseEntity; |
| 21 | +import org.springframework.test.context.ActiveProfiles; |
| 22 | +import org.springframework.util.LinkedMultiValueMap; |
| 23 | +import org.springframework.util.MultiValueMap; |
| 24 | + |
| 25 | +import java.util.Objects; |
| 26 | + |
| 27 | +import static org.assertj.core.api.Assertions.assertThat; |
| 28 | + |
| 29 | +/** |
| 30 | + * The class contains Live tests. |
| 31 | + * These tests expect that the Keycloak server is up and running on port 8080. |
| 32 | + */ |
| 33 | +@DisplayName("Keycloak SOAP Webservice Live Tests") |
| 34 | +@SpringBootTest(classes = KeycloakSoapServicesApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) |
| 35 | +@ActiveProfiles("test") |
| 36 | +@AutoConfigureMockMvc |
| 37 | +class KeycloakSoapLiveTest { |
| 38 | + |
| 39 | + private static final Logger logger = LoggerFactory.getLogger(KeycloakSoapLiveTest.class); |
| 40 | + |
| 41 | + @LocalServerPort |
| 42 | + private int port; |
| 43 | + |
| 44 | + @Autowired |
| 45 | + private TestRestTemplate restTemplate; |
| 46 | + |
| 47 | + @Autowired |
| 48 | + private ObjectMapper objectMapper; |
| 49 | + |
| 50 | + @Value("${grant.type}") |
| 51 | + private String grantType; |
| 52 | + |
| 53 | + @Value("${client.id}") |
| 54 | + private String clientId; |
| 55 | + |
| 56 | + @Value("${client.secret}") |
| 57 | + private String clientSecret; |
| 58 | + |
| 59 | + @Value("${url}") |
| 60 | + private String keycloakUrl; |
| 61 | + |
| 62 | + /** |
| 63 | + * Test a happy flow. Test the <i>janedoe</i> user. |
| 64 | + * This user should be configured in Keycloak server with a role <i>user</i> |
| 65 | + */ |
| 66 | + @Test |
| 67 | + @DisplayName("Get Products With Access Token") |
| 68 | + void givenAccessToken_whenGetProducts_thenReturnProduct() { |
| 69 | + |
| 70 | + HttpHeaders headers = new HttpHeaders(); |
| 71 | + headers.set("content-type", "text/xml"); |
| 72 | + headers.set("Authorization", "Bearer " + generateToken("janedoe", "password")); |
| 73 | + HttpEntity<String> request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers); |
| 74 | + ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); |
| 75 | + |
| 76 | + assertThat(responseEntity).isNotNull(); |
| 77 | + assertThat(responseEntity.getStatusCode().value()).isEqualTo(HttpStatus.OK.value()); |
| 78 | + assertThat(responseEntity.getBody()).isNotBlank(); |
| 79 | + assertThat(responseEntity.getBody()).containsIgnoringCase(":id>1</"); |
| 80 | + } |
| 81 | + |
| 82 | + /** |
| 83 | + * A negative test. Deliberately pass wrong credentials to Keycloak. Test the invalid <i>janeadoe</i> user. |
| 84 | + * Keycloak returns Unauthorized. Assert 401 status and empty body. |
| 85 | + */ |
| 86 | + @Test |
| 87 | + @DisplayName("Get Products With Wrong Access Token") |
| 88 | + void givenWrongAccessToken_whenGetProducts_thenReturnError() { |
| 89 | + |
| 90 | + HttpHeaders headers = new HttpHeaders(); |
| 91 | + headers.set("content-type", "text/xml"); |
| 92 | + headers.set("Authorization", "Bearer " + generateToken("janeadoe", "password")); |
| 93 | + HttpEntity<String> request = new HttpEntity<>(Utility.getGetProductDetailsRequest(), headers); |
| 94 | + ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); |
| 95 | + assertThat(responseEntity).isNotNull(); |
| 96 | + assertThat(responseEntity.getStatusCode().value()).isEqualTo(HttpStatus.UNAUTHORIZED.value()); |
| 97 | + assertThat(responseEntity.getBody()).isBlank(); |
| 98 | + } |
| 99 | + |
| 100 | + /** |
| 101 | + * Happy flow to test <i>deleteProduct</i> operation. Test the <i>jhondoe</i> user. |
| 102 | + * This user should be configured in Keycloak server with a role <i>user</i> |
| 103 | + */ |
| 104 | + @Test |
| 105 | + @DisplayName("Delete Product With Access Token") |
| 106 | + void givenAccessToken_whenDeleteProduct_thenReturnSuccess() { |
| 107 | + HttpHeaders headers = new HttpHeaders(); |
| 108 | + headers.set("content-type", "text/xml"); |
| 109 | + headers.set("Authorization", "Bearer " + generateToken("jhondoe", "password")); |
| 110 | + HttpEntity<String> request = new HttpEntity<>(Utility.getDeleteProductsRequest(), headers); |
| 111 | + ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); |
| 112 | + |
| 113 | + assertThat(responseEntity).isNotNull(); |
| 114 | + assertThat(responseEntity.getStatusCode().value()).isEqualTo(HttpStatus.OK.value()); |
| 115 | + assertThat(responseEntity.getBody()).isNotBlank(); |
| 116 | + assertThat(responseEntity.getBody()).containsIgnoringCase("Deleted the product with the id"); |
| 117 | + } |
| 118 | + |
| 119 | + /** |
| 120 | + * Negative flow to test <i></i>. Test the <i>janedoe</i> user. |
| 121 | + * Obtain the access token of <i>janedoe</i> and access the admin operation <i>deleteProduct</i> |
| 122 | + * Assume <i>janedoe</i> has restricted access to <i>deleteProduct</i> operation |
| 123 | + */ |
| 124 | + @Test |
| 125 | + @DisplayName("Delete Products With Unauthorized Access Token") |
| 126 | + void givenUnauthorizedAccessToken_whenDeleteProduct_thenReturnUnauthorized() { |
| 127 | + HttpHeaders headers = new HttpHeaders(); |
| 128 | + headers.set("content-type", "text/xml"); |
| 129 | + headers.set("Authorization", "Bearer " + generateToken("johndoe", "password")); |
| 130 | + HttpEntity<String> request = new HttpEntity<>(Utility.getDeleteProductsRequest(), headers); |
| 131 | + ResponseEntity<String> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/ws/api/v1/", request, String.class); |
| 132 | + |
| 133 | + assertThat(responseEntity).isNotNull(); |
| 134 | + assertThat(responseEntity.getStatusCode().value()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR.value()); |
| 135 | + assertThat(responseEntity.getBody()).isNotBlank(); |
| 136 | + assertThat(responseEntity.getBody()).containsIgnoringCase("Access Denied"); |
| 137 | + } |
| 138 | + |
| 139 | + private String generateToken(String username, String password) { |
| 140 | + |
| 141 | + try { |
| 142 | + HttpHeaders headers = new HttpHeaders(); |
| 143 | + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); |
| 144 | + MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); |
| 145 | + map.add("grant_type", grantType); |
| 146 | + map.add("client_id", clientId); |
| 147 | + map.add("client_secret", clientSecret); |
| 148 | + map.add("username", username); |
| 149 | + map.add("password", password); |
| 150 | + HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(map, headers); |
| 151 | + ResponseEntity<String> response = restTemplate.exchange(keycloakUrl, HttpMethod.POST, entity, String.class); |
| 152 | + return Objects.requireNonNull(response.getBody()).contains("access_token") ? objectMapper.readTree(response.getBody()).get("access_token").asText() : ""; |
| 153 | + } catch (Exception ex) { |
| 154 | + logger.error("There is an internal server error. Returning an empty access token", ex); |
| 155 | + return ""; |
| 156 | + } |
| 157 | + |
| 158 | + } |
| 159 | + |
| 160 | +} |
0 commit comments