diff --git a/gradle.properties b/gradle.properties index c4f21c1..1478313 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,6 +25,8 @@ jsoupVersion=1.18.1 htmlunitVersion=4.4.0 flywayVersion=10.17.1 classgraphVersion=4.8.175 +seleniumVersion=4.24.0 +webdrivermanagerVersion=5.9.2 # For deploy to maven central SONATYPE_HOST=CENTRAL_PORTAL SONATYPE_CONNECT_TIMEOUT_SECONDS=300 diff --git a/springdog-project/springdog-agent/src/main/resources/templates/content/error-tracing/configuration.html b/springdog-project/springdog-agent/src/main/resources/templates/content/error-tracing/configuration.html index 72d8e56..e737ad4 100644 --- a/springdog-project/springdog-agent/src/main/resources/templates/content/error-tracing/configuration.html +++ b/springdog-project/springdog-agent/src/main/resources/templates/content/error-tracing/configuration.html @@ -18,7 +18,7 @@ + xmlns:th="http://www.thymeleaf.org"> configuration @@ -97,7 +97,7 @@

Error Trace

- +
diff --git a/springdog-project/springdog-tests/springdog-e2e-agent-tests/build.gradle b/springdog-project/springdog-tests/springdog-e2e-agent-tests/build.gradle index b9b7cf3..b4ce326 100644 --- a/springdog-project/springdog-tests/springdog-e2e-agent-tests/build.gradle +++ b/springdog-project/springdog-tests/springdog-e2e-agent-tests/build.gradle @@ -19,8 +19,10 @@ dependencies { testImplementation "org.springframework.security:spring-security-test:${springVersion}" testImplementation "org.springframework.boot:spring-boot-starter-test:${springbootVersion}" - testImplementation group: 'org.htmlunit', name: 'htmlunit', version: '4.4.0' - testImplementation "org.seleniumhq.selenium:htmlunit3-driver:4.23.0" + + implementation "org.seleniumhq.selenium:selenium-java:${seleniumVersion}" + implementation "org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}" + implementation "io.github.bonigarcia:webdrivermanager:${webdrivermanagerVersion}" } test { 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 2048bf4..e221f94 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 @@ -16,157 +16,136 @@ package org.easypeelsecurity.agenttests; - import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.By.cssSelector; +import static org.openqa.selenium.By.xpath; + +import java.util.List; -import java.io.IOException; - -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; - -import org.htmlunit.WebClient; -import org.htmlunit.html.HtmlButton; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlPasswordInput; -import org.htmlunit.html.HtmlTable; -import org.htmlunit.html.HtmlTableRow; -import org.htmlunit.html.HtmlTextInput; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junitpioneer.jupiter.RetryingTest; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedConditions; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class AgentTests { - final String AGENT_USERNAME = "username"; - final String AGENT_PASSWORD = "password"; - - @LocalServerPort - private int port; - - private WebClient webClient; - - @BeforeEach - public void setUp() { - webClient = new WebClient(); - webClient.getOptions().setThrowExceptionOnScriptError(false); - webClient.getOptions().setCssEnabled(false); - webClient.getOptions().setJavaScriptEnabled(false); - } +import support.SeleniumTestSupport; - @AfterEach - public void tearDown() { - if (webClient != null) { - webClient.close(); - } - } +class AgentTests extends SeleniumTestSupport { @Test @DisplayName("Redirect to the login page when accessing the home PATH") - void should_redirect_to_loginPage() throws IOException { - HtmlPage page = webClient.getPage("http://localhost:" + port + "/springdog/"); - assertThat(page.getUrl()).hasToString(getAgentPath("/login")); + void should_redirect_to_loginPage() { + accessPage("/"); + assertThat(currentUrl()).isEqualTo(getAgentPath("/login")); } - @Test @DisplayName("Redirect to home page after successful login") - void login_success() throws IOException { - AgentTestHelper agent = new AgentTestHelper(); - HtmlPage resultPage = agent.login(AGENT_USERNAME, AGENT_PASSWORD); - - assertThat(resultPage.getUrl()).hasToString(getAgentPath("")); - assertThat(resultPage.asXml()).contains("Dashboard", "Total Endpoints"); + void login_success() throws InterruptedException { + withLogin(); + assertThat(currentUrl()).isEqualTo(getAgentPath("")); + assertThat(pageSource()).contains("Dashboard", "Total Endpoints"); } @Test @DisplayName("Display error message when login fails") - void login_fail() throws IOException { - AgentTestHelper agent = new AgentTestHelper(); - HtmlPage resultPage = agent.login("wrong_username", "wrong_password"); + void login_fail() throws InterruptedException { + withWrongLogin(); - assertThat(resultPage.getUrl()).hasToString(getAgentPath("/login?error")); - assertThat(resultPage.asXml()).contains("Login Failed."); + assertThat(currentUrl()).isEqualTo(getAgentPath("/login?error")); + assertThat(pageSource()).contains("Login Failed."); } @Test - void homePage() throws IOException { - AgentTestHelper agent = new AgentTestHelper(); - agent.withLogin(); + void homePage() throws InterruptedException { + withLogin(); - HtmlPage page = webClient.getPage(getAgentPath("/")); - assertThat(page.asXml()).contains("Dashboard", "Total Endpoints"); + accessPage("/"); + assertThat(pageSource()).contains("Dashboard", "Total Endpoints"); } @Test - void ratelimit_detail() throws IOException { - AgentTestHelper agent = new AgentTestHelper(); - agent.withLogin(); - - HtmlPage page = webClient.getPage(getAgentPath("/rate-limit")); - HtmlTable table = page.getFirstByXPath("//table[@id='table']"); - HtmlTableRow row = table.getRow(1); // Skip the header row - HtmlButton analyzeBtn = row.getCell(4).getFirstByXPath(".//button[@title='Analyze']"); - HtmlPage detailPage = analyzeBtn.click(); - - // then - assertThat(detailPage.asNormalizedText()).contains(""" - Path Method Signature HTTP Method Status Actions - /api/hello java.lang.String org.easypeelsecurity.agenttests.ExampleController.hello() GET NOT_CONFIGURED"""); + @RetryingTest(5) + void ratelimit_detail() throws InterruptedException { + withLogin(); + + accessPage("/rate-limit"); + WebElement analyzeBtn = accessPageUntilXPath("/rate-limit", "(//button[@title='Analyze'])[1]"); + analyzeBtn.click(); + + String pageText = getDriver().findElement(By.tagName("body")).getText(); + assertThat(pageText).contains( + "Endpoint Information\nPath: /api/hello\nEndpoint Method Signature: java.lang.String org.easypeelsecurity.agenttests.ExampleController.hello()\nHTTP Method: GET\nPattern Path: false\nRule Status: NOT_CONFIGURED\nIP Based Rule: false\nPermanent Ban Rule: false\nRequest Limit Count: 0\nTime Limit (seconds): 0\nBan Time (seconds): 0\n"); } @Test - void ratelimit_read() throws IOException { - AgentTestHelper agent = new AgentTestHelper(); - agent.withLogin(); + @RetryingTest(5) + void ratelimit_read() throws InterruptedException { + withLogin(); - HtmlPage page = webClient.getPage(getAgentPath("/rate-limit")); - HtmlTable table = page.getFirstByXPath("//table[@id='table']"); - HtmlTableRow row = table.getRow(1); // Skip the header row + WebElement table = + accessPageUntil("/rate-limit", ExpectedConditions.presenceOfElementLocated(By.id("table"))); + WebElement row = table.findElements(By.tagName("tr")).get(1); - // check row - assertThat(row.getCell(0).getTextContent()).isEqualTo("/api/hello"); - assertThat(row.getCell(1).getTextContent()).isEqualTo( + List cells = row.findElements(By.tagName("td")); + assertThat(cells.get(0).getText()).isEqualTo("/api/hello"); + assertThat(cells.get(1).getText()).isEqualTo( "java.lang.String org.easypeelsecurity.agenttests.ExampleController.hello()"); - assertThat(row.getCell(2).getTextContent()).isEqualTo("GET"); - assertThat(row.getCell(3).getTextContent()).contains("NOT_CONFIGURED"); - HtmlButton configureButton = row.getCell(4).getFirstByXPath(".//button[@title='Configure']"); - assertThat(configureButton).isNotNull(); - HtmlButton analyzeButton = row.getCell(4).getFirstByXPath(".//button[@title='Analyze']"); - assertThat(analyzeButton).isNotNull(); + assertThat(cells.get(2).getText()).isEqualTo("GET"); + assertThat(cells.get(3).getText()).contains("NOT_CONFIGURED"); + assertThat(cells.get(4).findElement(xpath(".//button[@title='Configure']"))).isNotNull(); + assertThat(cells.get(4).findElement(xpath(".//button[@title='Analyze']"))).isNotNull(); } - class AgentTestHelper { - HtmlPage login(String username, String password) throws IOException { - HtmlPage loginPage = webClient.getPage(getAgentPath("login")); - - // Get the form - HtmlForm loginForm = loginPage.getForms().get(0); - assertThat(loginForm).isNotNull(); - - // Fill in the form - HtmlTextInput usernameInput = loginForm.getInputByName("username"); - HtmlPasswordInput passwordInput = loginForm.getInputByName("password"); - HtmlButton submitButton = loginForm.getFirstByXPath("//button[@type='submit']"); - usernameInput.type(username); - passwordInput.type(password); - - // Submit - return submitButton.click(); - } - - void withLogin() throws IOException { - login(AGENT_USERNAME, AGENT_PASSWORD); - } + @Test + @RetryingTest(5) + void errorTraceConfigurationSearchTest() throws InterruptedException { + withLogin(); + + WebElement searchInput = accessPageUntil("/error-tracing/configuration", + ExpectedConditions.visibilityOfElementLocated(By.id("searchInput"))); + assertThat(searchInput).isNotNull(); + + performSearch("java.io.ObjectStrea", "java.io.ObjectStreamException", + "java.lang.MatchException"); + performSearch("MatchException", "java.lang.MatchException", "java.io.ObjectStreamException"); + performSearch("nullpointer", "java.lang.NullPointerException", + "java.io.ObjectStreamException"); } + private void performSearch(String searchTerm, String expectedResult, String notExpectedResult) { + WebElement searchInput = getDriver().findElement(By.id("searchInput")); + searchInput.clear(); + searchInput.sendKeys(searchTerm); + + getWait().until(ExpectedConditions.attributeToBe(searchInput, "value", searchTerm)); + List visibleItems = getDriver().findElements( + cssSelector("li.list-group-item.exception-item:not([style*='display: none']) div label")) + .stream() + .map(element -> element.getAttribute("innerHTML")) + .toList(); + + assertThat(visibleItems).isNotEmpty(); + assertThat(visibleItems).contains(expectedResult); + assertThat(visibleItems).doesNotContain(notExpectedResult); + } - private String getAgentPath(String path) { - if (path.startsWith("/")) { - path = path.substring(1); - } - return "http://localhost:" + port + "/springdog/" + path; + @Test + @RetryingTest(5) + void errorTraceConfigurationEnableTest() throws InterruptedException { + withLogin(); + + WebElement accordionButton = + accessPageUntilCssSelector("/error-tracing/configuration", "h2[id^='heading'] button[type='button']"); + accordionButton.click(); + + getWait().until( + ExpectedConditions.visibilityOfElementLocated(cssSelector("li.list-group-item.exception-item"))); + WebElement exceptionClasses = + getDriver().findElements(cssSelector("li.list-group-item.exception-item")).getFirst(); + WebElement checkbox = exceptionClasses.findElement(xpath(".//input[@type='checkbox']")); + checkbox.click(); + assertThat(checkbox.isSelected()).isFalse(); } } - diff --git a/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/ExampleControllerTest.java b/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/ExampleControllerTest.java index df73c9d..06fdb04 100644 --- a/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/ExampleControllerTest.java +++ b/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/org/easypeelsecurity/agenttests/ExampleControllerTest.java @@ -18,35 +18,18 @@ import static org.assertj.core.api.Assertions.assertThat; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.server.LocalServerPort; - -import org.htmlunit.WebClient; -import org.htmlunit.html.HtmlPage; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -class ExampleControllerTest { - - @LocalServerPort - private int port; +import support.SeleniumTestSupport; - private WebClient webClient; - - @BeforeEach - public void setUp() { - webClient = new WebClient(); - webClient.getOptions().setThrowExceptionOnScriptError(false); - webClient.getOptions().setCssEnabled(false); - webClient.getOptions().setJavaScriptEnabled(false); - } +class ExampleControllerTest extends SeleniumTestSupport { @Test @DisplayName("Should return hello world") - void shouldReturnHelloWorld() throws Exception { - HtmlPage page = webClient.getPage("http://localhost:" + port + "/api/hello"); - assertThat(page.getBody().asNormalizedText()).isEqualTo("Hello, World!"); + void shouldReturnHelloWorld() { + accessNotSpringdogPage("/api/hello"); + + assertThat(pageSource()).contains("Hello, World!"); } } diff --git a/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/support/SeleniumTestSupport.java b/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/support/SeleniumTestSupport.java new file mode 100644 index 0000000..a9296d3 --- /dev/null +++ b/springdog-project/springdog-tests/springdog-e2e-agent-tests/src/test/java/support/SeleniumTestSupport.java @@ -0,0 +1,144 @@ +/* + * 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 support; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.openqa.selenium.By.name; +import static org.openqa.selenium.By.xpath; + +import java.time.Duration; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.openqa.selenium.By; +import org.openqa.selenium.By.ByCssSelector; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.chrome.ChromeOptions; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.ExpectedConditions; +import org.openqa.selenium.support.ui.WebDriverWait; + +import io.github.bonigarcia.wdm.WebDriverManager; + +/** + * SeleniumTestSupport. + * For functionality not provided here, access it directly via getWait and getDriver. + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +public abstract class SeleniumTestSupport { + final String AGENT_USERNAME = "username"; + final String AGENT_PASSWORD = "password"; + + @LocalServerPort + private int port; + + private WebDriver driver; + private WebDriverWait wait; + + @BeforeEach + public void setUp() { + ChromeOptions options = new ChromeOptions(); + options.addArguments("--headless"); + options.addArguments("--disable-gpu"); + options.addArguments("--no-sandbox"); + options.setExperimentalOption("excludeSwitches", new String[] {"enable-automation"}); + this.driver = WebDriverManager.chromedriver().capabilities(options).create(); + this.wait = new WebDriverWait(driver, Duration.ofSeconds(30)); + } + + @AfterEach + public void tearDown() { + this.driver.quit(); + } + + public void accessPage(String path) { + this.driver.get(getAgentPath(path)); + } + + public WebElement accessPageUntil(String path, ExpectedCondition condition) { + this.driver.get(getAgentPath(path)); + return this.wait.until(condition); + } + + public WebElement accessPageUntilCssSelector(String path, String cssSelector) { + this.driver.get(getAgentPath(path)); + return this.wait.until(ExpectedConditions.presenceOfElementLocated(ByCssSelector.cssSelector(cssSelector))); + } + + public WebElement accessPageUntilXPath(String path, String xpathElement) { + this.driver.get(getAgentPath(path)); + return this.wait.until(ExpectedConditions.presenceOfElementLocated(By.xpath(xpathElement))); + } + + public void accessNotSpringdogPage(String path) { + assertThat(path).startsWith("/"); + this.driver.get("http://localhost:" + port + path); + } + + public void withLogin() throws InterruptedException { + this.login(AGENT_USERNAME, AGENT_PASSWORD); + } + + public void withWrongLogin() throws InterruptedException { + this.login("wrong_username", "wrong_password"); + } + + public String pageSource() { + return this.driver.getPageSource(); + } + + public String currentUrl() { + return this.driver.getCurrentUrl(); + } + + private void login(String username, String password) throws InterruptedException { + this.driver.get(getAgentPath("login")); + + WebElement loginForm = this.wait.until(ExpectedConditions.presenceOfElementLocated(By.tagName("form"))); + WebElement usernameInput = loginForm.findElement(name("username")); + WebElement passwordInput = loginForm.findElement(name("password")); + WebElement submitButton = loginForm.findElement(xpath("//button[@type='submit']")); + + usernameInput.sendKeys(username); + passwordInput.sendKeys(password); + submitButton.click(); + + if (username.equals(AGENT_USERNAME) && password.equals(AGENT_PASSWORD)) { + this.wait.until( + ExpectedConditions.visibilityOfElementLocated(xpath("//h1[normalize-space()='Dashboard']"))); + } + } + + public String getAgentPath(String path) { + if (path.startsWith("/")) { + path = path.substring(1); + } + return "http://localhost:" + port + "/springdog/" + path; + } + + public WebDriver getDriver() { + return this.driver; + } + + public WebDriverWait getWait() { + return this.wait; + } +}