From ee686fdc678e4e59cd1376e1feada8791abd8f75 Mon Sep 17 00:00:00 2001 From: penekhun Date: Mon, 2 Sep 2024 18:13:15 +0900 Subject: [PATCH] Add WebMvc tests for agent --- README.md | 14 +- .../springdog-agent/build.gradle | 2 +- .../agent/SpringdogAgentController.java | 6 +- .../SpringdogDynamicUrlMappingConfig.java | 1 + .../security/SpringdogSecurityConfig.java | 5 +- .../fixture/DashBoardResponseFixture.java | 45 +++++ .../agent/SpringdogAgentViewTest.java | 170 ++++++++++++++++++ .../springdog/agent/TestConfig.java | 54 ++++++ .../agenttests/AgentTests.java | 13 +- 9 files changed, 289 insertions(+), 21 deletions(-) create mode 100644 springdog-project/springdog-agent/src/test/java/fixture/DashBoardResponseFixture.java create mode 100644 springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/SpringdogAgentViewTest.java create mode 100644 springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/TestConfig.java diff --git a/README.md b/README.md index ba662c8..845b605 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ Add security configuration to your Spring application effortlessly by simply add 3. Run your application and access the Springdog agent in your browser. ```http request - GET http://localhost:8080/springdog + GET http://localhost:8080/springdog/ // Host and port may vary depending on your application configuration. ``` @@ -111,12 +111,12 @@ springdog: > The Springdog agent configuration. -| Name | Required | Description | Default | Value Sets | -|----------------|----------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|-------------------| -| basePath | x | The base path for the agent. Used to access the springdog agent from a deployed server, such as `{{host}}/springdog`. The basePath used with this option should never be used as the controller mapping address for your application. | springdog | -| username | x | The username for the Springdog agent. Empty fields are not allowed. | admin | -| password | x | The password for the Springdog agent. Empty fields are not allowed. | admin | -| externalAccess | x | Whether to allow external access to the Springdog agent. If `false`, access from external IPs is not allowed. | false | `true` or `false` | +| Name | Required | Description | Default | Value Sets | +|----------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------|-------------------| +| basePath | x | The base path for the agent. Used to access the springdog agent from a deployed server, such as `{{host}}/springdog/`. The basePath used with this option should never be used as the controller mapping address for your application. | springdog | +| username | x | The username for the Springdog agent. Empty fields are not allowed. | admin | +| password | x | The password for the Springdog agent. Empty fields are not allowed. | admin | +| externalAccess | x | Whether to allow external access to the Springdog agent. If `false`, access from external IPs is not allowed. | false | `true` or `false` | ### springdog.notification.gmail diff --git a/springdog-project/springdog-agent/build.gradle b/springdog-project/springdog-agent/build.gradle index c63058f..436aafd 100644 --- a/springdog-project/springdog-agent/build.gradle +++ b/springdog-project/springdog-agent/build.gradle @@ -8,7 +8,6 @@ shadowJar { } dependencies { -// implementation "org.springframework.boot:spring-boot-starter-web:${springbootVersion}" compileOnly "org.springframework.boot:spring-boot-starter-web:${springbootVersion}" implementation "org.springframework.boot:spring-boot-starter-security:${springbootVersion}" implementation "org.thymeleaf.extras:thymeleaf-extras-springsecurity${thymeleafSecurityVersion}" @@ -17,6 +16,7 @@ dependencies { implementation("nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:${thymeleafLayoutDialectVersion}") { exclude group: 'nz.net.ultraq.thymeleaf', module: 'thymeleaf-layout-dialect-extensions' } + testImplementation "org.springframework.boot:spring-boot-starter-web:${springbootVersion}" testImplementation "org.springframework.boot:spring-boot-starter-test:${springbootVersion}" testImplementation "org.springframework.security:spring-security-test:${springVersion}" diff --git a/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogAgentController.java b/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogAgentController.java index fa5fee7..3dab7eb 100644 --- a/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogAgentController.java +++ b/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogAgentController.java @@ -16,12 +16,16 @@ package org.easypeelsecurity.springdog.agent; +import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; /** * Annotation for Springdog agent controller. + * related to {@link SpringdogDynamicUrlMappingConfig} */ +@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) -public @interface SpringdogAgentController { +@interface SpringdogAgentController { } diff --git a/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogDynamicUrlMappingConfig.java b/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogDynamicUrlMappingConfig.java index 1f875c1..de63045 100644 --- a/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogDynamicUrlMappingConfig.java +++ b/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/SpringdogDynamicUrlMappingConfig.java @@ -27,6 +27,7 @@ /** * Springdog dynamic URL mapping configuration. * Enable agent access to the springdog path user determine at runtime. + * related to {@link SpringdogAgentController} */ @Component public class SpringdogDynamicUrlMappingConfig implements WebMvcRegistrations { diff --git a/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/security/SpringdogSecurityConfig.java b/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/security/SpringdogSecurityConfig.java index a5e736b..d98d80b 100644 --- a/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/security/SpringdogSecurityConfig.java +++ b/springdog-project/springdog-agent/src/main/java/org/easypeelsecurity/springdog/agent/security/SpringdogSecurityConfig.java @@ -38,6 +38,7 @@ @Configuration public class SpringdogSecurityConfig { + public static final String SPRINGDOG_AGENT_ADMIN_ROLE = "SPRINGDOG_AGENT_ADMIN"; private final SpringdogProperties springdogProperties; /** @@ -56,7 +57,7 @@ public SecurityFilterChain springdogSecurityFilterChain(HttpSecurity http) throw String baseAbsolutePath = springdogProperties.computeAbsolutePath(""); http .securityMatcher(baseAbsolutePath + "**") - .authorizeHttpRequests(auth -> auth.anyRequest().hasRole("SPRINGDOG_AGENT_ADMIN")) + .authorizeHttpRequests(auth -> auth.anyRequest().hasRole(SPRINGDOG_AGENT_ADMIN_ROLE)) .formLogin(formLogin -> formLogin .loginPage(springdogProperties.computeAbsolutePath("/login")) .failureUrl(springdogProperties.computeAbsolutePath("/login?error")) @@ -90,7 +91,7 @@ public UserDetailsService springdogUserDetailsService() { String password = springdogProperties.getAgentPassword(); UserDetails user = User.withUsername(username) .password("{noop}" + password) - .roles("SPRINGDOG_AGENT_ADMIN") + .roles(SPRINGDOG_AGENT_ADMIN_ROLE) .build(); return new InMemoryUserDetailsManager(user); diff --git a/springdog-project/springdog-agent/src/test/java/fixture/DashBoardResponseFixture.java b/springdog-project/springdog-agent/src/test/java/fixture/DashBoardResponseFixture.java new file mode 100644 index 0000000..bc4c5f1 --- /dev/null +++ b/springdog-project/springdog-agent/src/test/java/fixture/DashBoardResponseFixture.java @@ -0,0 +1,45 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package fixture; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +import org.easypeelsecurity.springdog.shared.dto.SystemMetricDto; +import org.easypeelsecurity.springdog.shared.vo.DashboardResponse; +import org.easypeelsecurity.springdog.shared.vo.DashboardResponse.DailyEndpointMetric; +import org.easypeelsecurity.springdog.shared.vo.DashboardResponse.DailySlowestEndpoint; +import org.easypeelsecurity.springdog.shared.vo.DashboardResponse.DailyTopFailWithRatelimitEndpoint; +import org.easypeelsecurity.springdog.shared.vo.DashboardResponse.DailyTopTrafficEndpoint; + +@SuppressWarnings("checkstyle:HideUtilityClassConstructor") +public class DashBoardResponseFixture { + public static DashboardResponse get() { + SystemMetricDto systemMetric = new SystemMetricDto(10.0, 20.0, 30.0, LocalDateTime.now()); + var dailyEndpointMetric = new DailyEndpointMetric(1, 2, 3, LocalDate.now()); + var dailyTopTrafficEndpoint = new DailyTopTrafficEndpoint("/test", "GET", 1); + var dailySlowestEndpoint = new DailySlowestEndpoint("/test", "GET", 1); + var dailyTopFailWithRatelimitEndpoint = new DailyTopFailWithRatelimitEndpoint("/test", "GET", 1); + + return new DashboardResponse(1, 2, 3, + List.of(systemMetric), + List.of(dailyEndpointMetric), + List.of(dailyTopTrafficEndpoint), List.of(dailySlowestEndpoint), + List.of(dailyTopFailWithRatelimitEndpoint)); + } +} diff --git a/springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/SpringdogAgentViewTest.java b/springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/SpringdogAgentViewTest.java new file mode 100644 index 0000000..daf91e8 --- /dev/null +++ b/springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/SpringdogAgentViewTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.easypeelsecurity.springdog.agent; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrl; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; + +import java.util.Collections; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.context.annotation.Import; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +import org.easypeelsecurity.springdog.agent.security.SpringdogSecurityConfig; +import org.easypeelsecurity.springdog.domain.ratelimit.EndpointService; +import org.easypeelsecurity.springdog.domain.statistics.StatisticsService; +import org.easypeelsecurity.springdog.shared.dto.EndpointDto; +import org.easypeelsecurity.springdog.shared.enums.HttpMethod; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import fixture.DashBoardResponseFixture; + +@SpringBootTest(properties = "springdog.agent.base-path=springdog") +@Import({TestConfig.class, SpringdogSecurityConfig.class}) +class SpringdogAgentViewTest { + MockMvc mockMvc; + + @Autowired + WebApplicationContext context; + + @MockBean + StatisticsService statisticsService; + + @MockBean + EndpointService endpointService; + + @BeforeEach + void setUp() { + this.mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @ParameterizedTest + @CsvSource({ + "/", + "/aidwjaiwjdawid", + "/login", + }) + void invalidPage(String invalidPath) throws Exception { + mockMvc.perform(get("/login")) + .andExpect(status().isNotFound()); + } + + @Test + @DisplayName("Inaccessible without permission") + void withoutAuthentication() throws Exception { + mockMvc.perform(get("/springdog/")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/springdog/login")); + } + + @Test + @WithMockUser(username = "admin", roles = {SpringdogSecurityConfig.SPRINGDOG_AGENT_ADMIN_ROLE}) + void testHomePage() throws Exception { + when(endpointService.getAllChangeLogsNotResolved()).thenReturn(Collections.emptyList()); + when(statisticsService.getDashboardResponse(any())).thenReturn(DashBoardResponseFixture.get()); + + mockMvc.perform(get("/springdog/")) + .andExpect(status().isOk()) + .andExpect(view().name("/templates/content/main.html")) + .andExpect(model().attributeExists("endpointChangeLog", "totalEndpointCount")); + } + + @Test + @WithMockUser(username = "admin", roles = {SpringdogSecurityConfig.SPRINGDOG_AGENT_ADMIN_ROLE}) + void ratelimitList() throws Exception { + when(endpointService.findAllEndpoints()).thenReturn(List.of()); + + mockMvc.perform(get("/springdog/rate-limit")) + .andExpect(status().isOk()) + .andExpect(view().name("/templates/content/rate-limit/manage.html")) + .andExpect(model().attributeExists("endpoints")); + } + + @Test + @WithMockUser(username = "admin", roles = {SpringdogSecurityConfig.SPRINGDOG_AGENT_ADMIN_ROLE}) + void setRateLimitSpecific() throws Exception { + long endpointId = 1L; + when(endpointService.findEndpoint(endpointId)).thenReturn( + new EndpointDto.Builder() + .id(endpointId) + .path("/test") + .methodSignature("org.a.b.C.d()") + .httpMethod(HttpMethod.GET) + .build() + ); + + mockMvc.perform(get("/springdog/rate-limit/{endpointId}", endpointId)) + .andExpect(status().isOk()) + .andExpect(view().name("/templates/content/rate-limit/actions/setting.html")) + .andExpect(model().attributeExists("endpoint")); + } + + @Test + @WithMockUser(username = "admin", roles = {SpringdogSecurityConfig.SPRINGDOG_AGENT_ADMIN_ROLE}) + void viewRatelimitEndpointAnalytics() throws Exception { + long endpointId = 1L; + when(endpointService.findEndpoint(endpointId)).thenReturn( + new EndpointDto.Builder() + .id(endpointId) + .path("/test") + .methodSignature("org.a.b.C.d()") + .httpMethod(HttpMethod.GET) + .build() + ); + + mockMvc.perform(get("/springdog/rate-limit/{endpointId}/analytics", endpointId)) + .andExpect(status().isOk()) + .andExpect(view().name("/templates/content/rate-limit/actions/analytics.html")) + .andExpect(model().attributeExists("endpoint")); + } + + @Test + void testLogin() throws Exception { + mockMvc.perform(get("/springdog/rate-limit")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("http://localhost/springdog/login")); + } + + @Test + @WithMockUser(username = "admin", roles = {SpringdogSecurityConfig.SPRINGDOG_AGENT_ADMIN_ROLE}) + void testLogout() throws Exception { + mockMvc.perform(get("/springdog/logout")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/springdog/login?logout")); + } +} diff --git a/springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/TestConfig.java b/springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/TestConfig.java new file mode 100644 index 0000000..bedc18f --- /dev/null +++ b/springdog-project/springdog-agent/src/test/java/org/easypeelsecurity/springdog/agent/TestConfig.java @@ -0,0 +1,54 @@ +/* + * Copyright 2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.easypeelsecurity.springdog.agent; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.web.servlet.ViewResolver; + +import org.thymeleaf.spring6.SpringTemplateEngine; +import org.thymeleaf.spring6.view.ThymeleafViewResolver; +import org.thymeleaf.templatemode.TemplateMode; +import org.thymeleaf.templateresolver.ClassLoaderTemplateResolver; + +/** + * Test configuration for agent test. + */ +@SpringBootApplication +@ComponentScan(basePackages = {"org.easypeelsecurity.springdog.shared", "org.easypeelsecurity.springdog.agent"}) +public class TestConfig { + + @Bean + public ViewResolver viewResolver() { + ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver(); + templateResolver.setPrefix(""); + templateResolver.setSuffix(""); + templateResolver.setTemplateMode(TemplateMode.HTML); + + ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); + viewResolver.setTemplateEngine(templateEngine(templateResolver)); + return viewResolver; + } + + @Bean + public SpringTemplateEngine templateEngine(ClassLoaderTemplateResolver templateResolver) { + SpringTemplateEngine templateEngine = new SpringTemplateEngine(); + templateEngine.setTemplateResolver(templateResolver); + return templateEngine; + } +} diff --git a/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/AgentTests.java b/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/AgentTests.java index 61108e1..2048bf4 100644 --- a/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/AgentTests.java +++ b/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/AgentTests.java @@ -29,7 +29,6 @@ import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlPage; import org.htmlunit.html.HtmlPasswordInput; -import org.htmlunit.html.HtmlSubmitInput; import org.htmlunit.html.HtmlTable; import org.htmlunit.html.HtmlTableRow; import org.htmlunit.html.HtmlTextInput; @@ -37,12 +36,9 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) class AgentTests { - final String AGENT_ADMIN_ROLE = "SPRINGDOG_AGENT_ADMIN"; final String AGENT_USERNAME = "username"; final String AGENT_PASSWORD = "password"; @@ -66,13 +62,10 @@ public void tearDown() { } } - @ParameterizedTest - @CsvSource({ - "/springdog", "/springdog/" - }) + @Test @DisplayName("Redirect to the login page when accessing the home PATH") - void should_redirect_to_loginPage(String homePath) throws IOException { - HtmlPage page = webClient.getPage("http://localhost:" + port + homePath); + void should_redirect_to_loginPage() throws IOException { + HtmlPage page = webClient.getPage("http://localhost:" + port + "/springdog/"); assertThat(page.getUrl()).hasToString(getAgentPath("/login")); }