From e94180cc762ce7ad8c49df483ee5f9436df17616 Mon Sep 17 00:00:00 2001 From: Gaurav Mishra Date: Mon, 2 Dec 2024 16:48:50 +0530 Subject: [PATCH] test(config): add test docs for /config actuator Signed-off-by: Gaurav Mishra --- .../src/docs/asciidoc/config.adoc | 46 +++++++++++ .../resourceserver/Sw360ResourceServer.java | 37 ++++++++- .../actuator/SW360ConfigActuator.java | 3 +- .../security/ResourceServerConfiguration.java | 4 +- .../src/main/resources/application.yml | 3 +- .../actuator/SW360ConfigActuatorTest.java | 64 +++++++++++++++ ...Sw360AuthorizationServerConfiguration.java | 2 + .../restdocs/ConfigSpecTest.java | 79 +++++++++++++++++++ .../src/test/resources/application.yml | 3 +- 9 files changed, 236 insertions(+), 5 deletions(-) create mode 100644 rest/resource-server/src/docs/asciidoc/config.adoc create mode 100644 rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuatorTest.java create mode 100644 rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ConfigSpecTest.java diff --git a/rest/resource-server/src/docs/asciidoc/config.adoc b/rest/resource-server/src/docs/asciidoc/config.adoc new file mode 100644 index 0000000000..a61e636606 --- /dev/null +++ b/rest/resource-server/src/docs/asciidoc/config.adoc @@ -0,0 +1,46 @@ +// +// Copyright Siemens AG, 2024. Part of the SW360 Portal Project. +// +// This program and the accompanying materials are made +// available under the terms of the Eclipse Public License 2.0 +// which is available at https://www.eclipse.org/legal/epl-2.0/ +// +// SPDX-License-Identifier: EPL-2.0 +// +[[resources-config]] +=== Config + +The config resource is used to give information from the `sw360.properties` file. +The configurations exposed are useful for the frontend UI. The backend only configs are not exposed. + +[[resources-config-get]] +==== Getting config + +A `GET` request will get the service's configurations. + +===== Example request +include::{snippets}/should_document_get_config/curl-request.adoc[] + +The response structure is the config object of the config actuator. + +===== Response structure +include::{snippets}/should_document_get_config/response-fields.adoc[] + +===== Example response +include::{snippets}/should_document_get_config/http-response.adoc[] + +The response structure is not complete as it will grow overtime. But it is a key-value pair of strings. + +[[resources-config-get-single]] +==== Getting single config value + +A `GET` request will get the service's single configuration. + +===== Request Parameters +include::{snippets}/should_document_get_single_config/path-parameters.adoc[] + +===== Example request +include::{snippets}/should_document_get_single_config/curl-request.adoc[] + +===== Example response +include::{snippets}/should_document_get_single_config/http-response.adoc[] diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java index 715e72aacf..8f1db1f514 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/Sw360ResourceServer.java @@ -19,6 +19,7 @@ import io.swagger.v3.oas.models.media.Content; import io.swagger.v3.oas.models.media.MediaType; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.PathParameter; import io.swagger.v3.oas.models.responses.ApiResponse; import io.swagger.v3.oas.models.responses.ApiResponses; import io.swagger.v3.oas.models.security.SecurityScheme; @@ -151,7 +152,7 @@ public OpenAPI customOpenAPI() { .responses(new ApiResponses().addApiResponse("200", new ApiResponse().description("OK") .content(new Content() - .addMediaType("application/json", new MediaType() + .addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, new MediaType() .example(""" { "status": "UP", @@ -174,6 +175,40 @@ public OpenAPI customOpenAPI() { .schema(new Schema()) )) )) + )) + .path("/config", new PathItem().get( + new Operation().tags(Collections.singletonList("Health")) + .summary("Configuration properties").operationId("get-config") + .responses(new ApiResponses().addApiResponse("200", + new ApiResponse().description("OK") + .content(new Content() + .addMediaType(org.springframework.http.MediaType.APPLICATION_JSON_VALUE, new MediaType() + .example(""" + { + "property.1": "value1", + "property.2": "false", + "property.3": "value3,value4" + } + """) + .schema(new Schema>()) + )) + )) + )) + .path("/config/{key}", new PathItem().get( + new Operation().tags(Collections.singletonList("Health")) + .summary("Get single configuration property").operationId("get-single-config") + .addParametersItem(new PathParameter() + .required(true).in("path").description("Property key").example("operating.systems") + .name("key") + ) + .responses(new ApiResponses().addApiResponse("200", + new ApiResponse().description("OK") + .content(new Content() + .addMediaType(org.springframework.http.MediaType.TEXT_PLAIN_VALUE, new MediaType() + .example("Android,BSD,iOS,Linux,OS X,QNX,Microsoft Windows,Windows Phone,IBM z/OS") + .schema(new Schema>()) + )) + )) )); } } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuator.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuator.java index 03429cff1f..056265228b 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuator.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuator.java @@ -15,6 +15,7 @@ import org.springframework.boot.actuate.endpoint.annotation.Endpoint; import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import java.util.Map; @@ -78,7 +79,7 @@ public Map config() { return properties; } - @ReadOperation + @ReadOperation(produces = MediaType.TEXT_PLAIN_VALUE) public String config(@Selector String name) { return properties.get(name); } diff --git a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java index b9ac930ad0..dcec42a1fd 100644 --- a/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java +++ b/rest/resource-server/src/main/java/org/eclipse/sw360/rest/resourceserver/security/ResourceServerConfiguration.java @@ -62,7 +62,7 @@ public class ResourceServerConfiguration { public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring().requestMatchers("/", "/*/*.html", "/*/*.css", "/*/*.js", "/*.js", "/*.json", "/*/*.json", "/*/*.png", "/*/*.gif", "/*/*.ico", "/*/*.woff/*", "/*/*.ttf", "/*/*.html", "/*/*/*.html", - "/*/*.yaml", "/v3/api-docs/**", "/api/health", "/api/info"); + "/*/*.yaml", "/v3/api-docs/**", "/api/health", "/api/info", "/api/config", "/api/config/*"); } @Bean @@ -84,6 +84,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.addFilterBefore(filter, BasicAuthenticationFilter.class).authorizeHttpRequests(auth -> { auth.requestMatchers(HttpMethod.GET, "/api/health").permitAll(); auth.requestMatchers(HttpMethod.GET, "/api/info").hasAuthority("WRITE"); + auth.requestMatchers(HttpMethod.GET, "/api/config").permitAll(); + auth.requestMatchers(HttpMethod.GET, "/api/config/*").permitAll(); auth.requestMatchers(HttpMethod.GET, "/api").permitAll(); auth.requestMatchers(HttpMethod.GET, "/api/reports/download").permitAll(); auth.requestMatchers(HttpMethod.GET, "/api/**").hasAuthority("READ"); diff --git a/rest/resource-server/src/main/resources/application.yml b/rest/resource-server/src/main/resources/application.yml index 02d3c90f62..8ebfb080db 100644 --- a/rest/resource-server/src/main/resources/application.yml +++ b/rest/resource-server/src/main/resources/application.yml @@ -19,10 +19,11 @@ management: web: base-path: / exposure: - include: health,info + include: health,info,config path-mapping: health: /api/health info: /api/info + config: /api/config endpoint: health: show-details: always diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuatorTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuatorTest.java new file mode 100644 index 0000000000..1fbb952b52 --- /dev/null +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/actuator/SW360ConfigActuatorTest.java @@ -0,0 +1,64 @@ +/* + * Copyright Siemens AG, 2024. Part of the SW360 Portal Project. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.sw360.rest.resourceserver.actuator; + +import org.eclipse.sw360.rest.resourceserver.Sw360ResourceServer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringRunner; + +import java.util.Map; + +import static org.assertj.core.api.BDDAssertions.then; + +/* @DirtiesContext is necessary because the context needs to be reloaded inbetween the tests + otherwise the responses of previous tests are taken. NoOpCacheManager through @AutoConfigureCache + was not enough to avoid this bug. + */ +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +@RunWith(SpringRunner.class) +@SpringBootTest(classes = Sw360ResourceServer.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public class SW360ConfigActuatorTest { + + @LocalServerPort + private int port; + + @SpyBean + private SW360ConfigActuator restConfigActuatorMock; + + @Autowired + private TestRestTemplate testRestTemplate; + + /** + * Makes a request to localhost with the default server port and returns + * the response as a response entity with type Map + * @param endpoint endpoint that will be called + * @return response of request + */ + private ResponseEntity getMapResponseEntityForHealthEndpointRequest(String endpoint) { + return this.testRestTemplate.getForEntity( + "http://localhost:" + this.port + Sw360ResourceServer.REST_BASE_PATH + endpoint, Map.class); + } + + @Test + public void config_should_return_200() { + ResponseEntity entity = getMapResponseEntityForHealthEndpointRequest("/config"); + + then(entity.getStatusCode()).isEqualTo(HttpStatus.OK); + } +} diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/configuration/security/Sw360AuthorizationServerConfiguration.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/configuration/security/Sw360AuthorizationServerConfiguration.java index 21b51000f7..81deb7543f 100644 --- a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/configuration/security/Sw360AuthorizationServerConfiguration.java +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/configuration/security/Sw360AuthorizationServerConfiguration.java @@ -42,6 +42,8 @@ public SecurityFilterChain appSecurtiy(HttpSecurity httpSecurity) throws Excepti authz -> authz .requestMatchers(HttpMethod.GET, "/api/health").permitAll() .requestMatchers(HttpMethod.GET, "/api/info").permitAll() + .requestMatchers(HttpMethod.GET, "/api/config").permitAll() + .requestMatchers(HttpMethod.GET, "/api/config/*").permitAll() .anyRequest().authenticated() ).httpBasic(Customizer.withDefaults()).formLogin(Customizer.withDefaults()) .exceptionHandling(x -> x.authenticationEntryPoint(saep)); diff --git a/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ConfigSpecTest.java b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ConfigSpecTest.java new file mode 100644 index 0000000000..12d815bcd6 --- /dev/null +++ b/rest/resource-server/src/test/java/org/eclipse/sw360/rest/resourceserver/restdocs/ConfigSpecTest.java @@ -0,0 +1,79 @@ +/* + * Copyright Siemens AG, 2024. Part of the SW360 Portal Project. + * + * This program and the accompanying materials are made + * available under the terms of the Eclipse Public License 2.0 + * which is available at https://www.eclipse.org/legal/epl-2.0/ + * + * SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.sw360.rest.resourceserver.restdocs; + +import org.eclipse.sw360.rest.resourceserver.actuator.SW360ConfigActuator; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.BDDMockito.given; +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.pathParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@RunWith(SpringJUnit4ClassRunner.class) +@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD) +public class ConfigSpecTest extends TestRestDocsSpecBase{ + + @MockBean + private SW360ConfigActuator restConfigActuatorMock; + + Map properties; + + { + properties = new HashMap<>(); + properties.put("admin.private.project.access.enabled", "true"); + properties.put("clearing.teams", "org1,org2,org3"); + properties.put("rest.apitoken.read.validity.days", "90"); + properties.put("rest.write.access.usergroup", "ADMIN"); + } + + @Test + public void should_document_get_config() throws Exception { + given(this.restConfigActuatorMock.config()).willReturn(properties); + + mockMvc.perform(get("/api/config") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andDo(this.documentationHandler.document( + responseFields( + fieldWithPath("['admin.private.project.access.enabled']").description("Sample boolean property."), + fieldWithPath("['clearing.teams']").description("Sample set property (separated by comma)."), + fieldWithPath("['rest.apitoken.read.validity.days']").description("Sample integer property."), + fieldWithPath("['rest.write.access.usergroup']").description("Sample string property.") + ) + )); + } + + @Test + public void should_document_get_single_config() throws Exception { + given(this.restConfigActuatorMock.config("rest.apitoken.read.validity.days")).willReturn(properties.get("rest.apitoken.read.validity.days")); + + mockMvc.perform(RestDocumentationRequestBuilders.get("/api/config/{key}", "rest.apitoken.read.validity.days") + .accept(MediaType.TEXT_PLAIN)) + .andExpect(status().isOk()) + .andExpect(header().string("Content-Type", "text/plain;charset=UTF-8")) + .andDo(this.documentationHandler.document( + pathParameters(parameterWithName("key").description("Property key")) + )); + } +} diff --git a/rest/resource-server/src/test/resources/application.yml b/rest/resource-server/src/test/resources/application.yml index 3db9ecabe9..4dd86e03c4 100644 --- a/rest/resource-server/src/test/resources/application.yml +++ b/rest/resource-server/src/test/resources/application.yml @@ -10,10 +10,11 @@ management: web: base-path: / exposure: - include: health,info + include: health,info,config path-mapping: health: /api/health info: /api/info + config: /api/config endpoint: health: show-details: always