Start refactoring UI tests

This commit is contained in:
Théophile Diot 2024-02-07 15:06:24 +01:00
parent 1ee9846762
commit b8eaa1e5f5
No known key found for this signature in database
GPG key ID: 248FEA4BAE400D06
16 changed files with 1162 additions and 11 deletions

View file

@ -74,13 +74,32 @@ jobs:
security-events: write
# UI tests
prepare-tests-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- id: set-matrix
run: |
tests=$(find ./tests/ui/ -name "*_page.py" -type f -printf "%f\n" | jq -c --raw-input --slurp 'split("\n")| .[0:-1]')
echo "tests=$tests" >> $GITHUB_OUTPUT
outputs:
tests: ${{ steps.set-matrix.outputs.tests }}
tests-ui:
needs: [build-containers]
needs: [prepare-tests-ui, build-containers]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
uses: ./.github/workflows/tests-ui.yml
with:
RELEASE: dev
tests-ui-linux:
needs: [build-packages]
needs: [prepare-tests-ui, build-packages]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
uses: ./.github/workflows/tests-ui-linux.yml
with:
RELEASE: dev

View file

@ -97,18 +97,38 @@ jobs:
echo "tests=$tests" >> $GITHUB_OUTPUT
outputs:
tests: ${{ steps.set-matrix.outputs.tests }}
prepare-tests-ui:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- id: set-matrix
run: |
tests=$(find ./tests/ui/ -name "*_page.py" -type f -printf "%f\n" | jq -c --raw-input --slurp 'split("\n")| .[0:-1]')
echo "tests=$tests" >> $GITHUB_OUTPUT
outputs:
tests: ${{ steps.set-matrix.outputs.tests }}
# Perform tests
tests-ui:
needs: [codeql, build-containers]
needs: [prepare-tests-ui, build-containers]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
uses: ./.github/workflows/tests-ui.yml
with:
RELEASE: testing
RELEASE: dev
tests-ui-linux:
needs: [codeql, build-packages]
needs: [prepare-tests-ui, build-packages]
strategy:
fail-fast: false
matrix:
test: ${{ fromJson(needs.prepare-tests-core.outputs.tests) }}
uses: ./.github/workflows/tests-ui-linux.yml
with:
RELEASE: testing
RELEASE: dev
staging-tests:
needs: [create-infras]
strategy:
@ -130,6 +150,7 @@ jobs:
TYPE: ${{ matrix.type }}
RUNS_ON: ${{ matrix.runs_on }}
secrets: inherit
tests-core:
needs: [build-containers, prepare-tests-core]
strategy:

View file

@ -3,6 +3,9 @@ name: Core test Linux (REUSABLE)
on:
workflow_call:
inputs:
TEST:
required: true
type: string
RELEASE:
required: true
type: string
@ -113,6 +116,6 @@ jobs:
}' | tee plugin.json
zip discord.zip plugin.json
rm plugin.json
./tests.sh "linux"
./tests.sh "linux" ${{ inputs.TEST }}
env:
MODE: ${{ inputs.RELEASE }}

View file

@ -3,6 +3,9 @@ name: Perform tests for UI (REUSABLE)
on:
workflow_call:
inputs:
TEST:
required: true
type: string
RELEASE:
required: true
type: string
@ -29,6 +32,6 @@ jobs:
- name: Run tests
run: |
cd ./tests/ui
./tests.sh "docker"
./tests.sh "docker" ${{ inputs.TEST }}
env:
MODE: ${{ inputs.RELEASE }}

View file

@ -36,7 +36,12 @@ RUN echo '{ \
RUN apk del .build-deps && \
rm -rf /var/cache/apk/*
COPY main.py .
ARG TEST_FILE=main.py
COPY base.py .
COPY wizard.py .
COPY utils.py .
COPY $TEST_FILE main.py
ENV PYTHONUNBUFFERED=1

63
tests/ui/base.py Normal file
View file

@ -0,0 +1,63 @@
from contextlib import suppress
from functools import partial
from logging import DEBUG, INFO, _nameToLevel, basicConfig, info
from os import getenv, listdir, sep
from pathlib import Path
from time import sleep
from requests import RequestException, get
from selenium import webdriver
from selenium.webdriver.firefox.options import Options
from selenium.webdriver.firefox.service import Service
basicConfig(
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
datefmt="[%Y-%m-%d %H:%M:%S]",
level=DEBUG if getenv("ACTIONS_STEP_DEBUG", False) else _nameToLevel.get(getenv("LOG_LEVEL", "INFO").upper(), INFO),
)
os_release_path = Path(sep, "etc", "os-release")
DEFAULT_SERVER = "192.168.0.2" if os_release_path.is_file() and "Alpine" in os_release_path.read_text(encoding="utf-8") else "127.0.0.1"
TEST_TYPE = getenv("TEST_TYPE", "docker")
local_geckodriver = "geckodriver" in listdir(Path.cwd())
FIREFOX_OPTIONS = Options()
if not local_geckodriver:
FIREFOX_OPTIONS.add_argument("--headless")
FIREFOX_OPTIONS.log.level = "trace" # type: ignore
ready = False
retries = 0
while not ready:
with suppress(RequestException):
status_code = get(f"http://{DEFAULT_SERVER}/setup").status_code
if status_code > 500 and status_code != 502:
print("An error occurred with the server, exiting ...", flush=True)
exit(1)
ready = status_code < 400
if retries > 20:
print("UI took too long to be ready, exiting ...", flush=True)
exit(1)
elif not ready:
retries += 1
print("Waiting for UI to be ready, retrying in 5s ...", flush=True)
sleep(5)
driver_func = partial(webdriver.Firefox, service=Service(log_output="./geckodriver.log"), options=FIREFOX_OPTIONS)
if TEST_TYPE == "dev":
driver_func = partial(
webdriver.Firefox,
service=Service(executable_path="./geckodriver" if local_geckodriver else "/usr/local/bin/geckodriver", log_output="./geckodriver.log"),
options=FIREFOX_OPTIONS,
)
DRIVER = driver_func()
info("UI is ready, starting tests ...")
__all__ = ("DEFAULT_SERVER", "TEST_TYPE", "DRIVER")

208
tests/ui/configs_page.py Normal file
View file

@ -0,0 +1,208 @@
from contextlib import suppress
from logging import info as log_info, exception as log_exception, error as log_error
from time import sleep
from requests import get
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import TimeoutException
from wizard import DRIVER
from base import TEST_TYPE
from utils import access_page, assert_alert_message, assert_button_click, safe_get_element, wait_for_service
exit_code = 0
try:
log_info("Navigating to the services page to create a new service ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a", "services")
assert_button_click(DRIVER, "//button[@data-services-action='new']")
server_name_input = safe_get_element(DRIVER, By.ID, "SERVER_NAME")
assert isinstance(server_name_input, WebElement), "Input is not a WebElement"
server_name_input.clear()
server_name_input.send_keys("app1.example.com")
access_page(DRIVER, "//button[@data-services-modal-submit='']", "services", False)
if TEST_TYPE == "linux":
wait_for_service("app1.example.com")
log_info("Navigating to the configs page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[5]/a", "configs")
log_info("Trying to create a new config ...")
assert_button_click(DRIVER, "//div[@data-configs-element='server-http' and @data-_type='folder']")
assert_button_click(DRIVER, "//li[@data-configs-add-file='']/button")
configs_modal_path_input = safe_get_element(DRIVER, By.XPATH, "//div[@data-configs-modal-path='']/input")
assert isinstance(configs_modal_path_input, WebElement), "The path input is not an instance of WebElement"
configs_modal_path_input.send_keys("hello")
configs_modal_editor_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-configs-modal-editor='']/textarea")
assert isinstance(configs_modal_editor_elem, WebElement), "The editor element is not an instance of WebElement"
configs_modal_editor_elem.send_keys(
"""
location /hello {
default_type 'text/plain';
content_by_lua_block {
ngx.say('hello app1')
}
}"""
)
access_page(DRIVER, "//button[@data-configs-modal-submit='']", "configs", False)
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "was successfully created")
sleep(10)
DRIVER.execute_script("window.open('http://www.example.com/hello','_blank');")
DRIVER.switch_to.window(DRIVER.window_handles[1])
DRIVER.switch_to.default_content()
try:
pre_elem = safe_get_element(DRIVER, By.XPATH, "//pre", error=True)
assert isinstance(pre_elem, WebElement), "The pre element is not an instance of WebElement"
if pre_elem.text.strip() != "hello app1":
log_error("The config hasn't been created correctly, exiting ...")
exit(1)
except TimeoutException:
log_info("The config hasn't been created, exiting ...")
exit(1)
DRIVER.execute_script("window.open('http://app1.example.com/hello','_blank');")
DRIVER.switch_to.window(DRIVER.window_handles[1])
DRIVER.switch_to.default_content()
try:
pre_elem = safe_get_element(DRIVER, By.XPATH, "//pre", error=True)
assert isinstance(pre_elem, WebElement), "The pre element is not an instance of WebElement"
if pre_elem.text.strip() != "hello app1":
log_error("The config hasn't been created correctly, exiting ...")
exit(1)
except TimeoutException:
log_info("The config hasn't been created, exiting ...")
exit(1)
log_info("The config has been created and is working with both services, trying to delete it ...")
for _ in range(2):
DRIVER.close()
DRIVER.switch_to.window(DRIVER.window_handles[len(DRIVER.window_handles) - 1])
assert_button_click(DRIVER, "//div[@data-configs-element='server-http' and @data-_type='folder']")
assert_button_click(DRIVER, "//div[@data-configs-action-button='hello.conf']")
assert_button_click(DRIVER, "//div[@data-configs-action-dropdown='hello.conf']/button[@value='delete' and @data-configs-action-dropdown-btn='hello.conf']")
access_page(DRIVER, "//button[@data-configs-modal-submit='']", "configs", False)
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "was successfully deleted")
sleep(10)
resp = get("http://www.example.com/hello")
if resp.status_code != 404:
log_error("The config hasn't been deleted correctly, exiting ...")
exit(1)
log_info("The config has been deleted, trying the same for a specific service ...")
assert_button_click(DRIVER, "//div[@data-configs-element='server-http' and @data-_type='folder']")
assert_button_click(DRIVER, "//div[@data-path='/etc/bunkerweb/configs/server-http/app1.example.com' and @data-_type='folder']")
assert_button_click(DRIVER, "//li[@data-configs-add-file='']/button")
configs_modal_path_input = safe_get_element(DRIVER, By.XPATH, "//div[@data-configs-modal-path='']/input")
assert isinstance(configs_modal_path_input, WebElement), "The path input is not an instance of WebElement"
configs_modal_path_input.send_keys("hello")
configs_modal_editor_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-configs-modal-editor='']/textarea")
assert isinstance(configs_modal_editor_elem, WebElement), "The editor element is not an instance of WebElement"
configs_modal_editor_elem.send_keys(
"""
location /hello {
default_type 'text/plain';
content_by_lua_block {
ngx.say('hello app1')
}
}"""
)
access_page(DRIVER, "//button[@data-configs-modal-submit='']", "configs", False)
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "was successfully created")
sleep(10)
DRIVER.execute_script("window.open('http://app1.example.com/hello','_blank');")
DRIVER.switch_to.window(DRIVER.window_handles[1])
DRIVER.switch_to.default_content()
try:
pre_elem = safe_get_element(DRIVER, By.XPATH, "//pre", error=True)
assert isinstance(pre_elem, WebElement), "The pre element is not an instance of WebElement"
if pre_elem.text.strip() != "hello app1":
log_error("The config hasn't been created correctly, exiting ...")
exit(1)
except TimeoutException:
log_info("The config hasn't been created, exiting ...")
exit(1)
DRIVER.close()
DRIVER.switch_to.window(DRIVER.window_handles[0])
resp = get("http://www.example.com/hello")
if resp.status_code != 404:
log_error("The config didn't get created only for the app1.example.com service, exiting ...")
exit(1)
log_info("The config has been created only for the app1.example.com service, trying to delete the service to see if the config gets deleted ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a", "services")
assert_button_click(DRIVER, "//button[@data-services-action='delete' and @data-services-name='app1.example.com']")
access_page(DRIVER, "//form[@data-services-modal-form-delete='']//button[@type='submit']", "services", False)
if TEST_TYPE == "linux":
wait_for_service()
log_info("The service has been deleted, checking if the config has been deleted as well ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[5]/a", "configs")
assert_button_click(DRIVER, "//div[@data-configs-element='server-http' and @data-_type='folder']")
with suppress(TimeoutException):
safe_get_element(DRIVER, By.XPATH, "//div[@data-configs-element='app2.example.com' and @data-_type='folder']", error=True)
log_error("The config hasn't been deleted, exiting ...")
exit(1)
log_info("The config has been deleted")
log_info("✅ Configs page tests finished successfully")
except SystemExit as e:
exit_code = e.code
except KeyboardInterrupt:
exit_code = 1
except:
log_exception("Something went wrong, exiting ...")
DRIVER.save_screenshot("error.png")
exit_code = 1
finally:
DRIVER.quit()
exit(exit_code)

View file

@ -5,6 +5,8 @@ services:
build:
context: .
dockerfile: Dockerfile
args:
- TEST_FILE=${TEST_FILE}
environment:
- PYTHONUNBUFFERED=1
extra_hosts:

View file

@ -0,0 +1,112 @@
from logging import info as log_info, exception as log_exception, error as log_error, warning as log_warning
from random import shuffle
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from wizard import DRIVER
from base import TEST_TYPE
from utils import access_page, assert_alert_message, assert_button_click, safe_get_element, wait_for_service
exit_code = 0
try:
log_info("Navigating to the global config page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[3]/a", "global config")
no_errors = True
retries = 0
while no_errors:
try:
log_info("Trying to save the global config without changing anything ...")
access_page(DRIVER, "//form[@id='form-edit-global-configs']//button[@type='submit']", "global config", False)
log_info("The page reloaded successfully, checking the message ...")
assert_alert_message(DRIVER, "The global configuration was not edited because no values were changed.")
no_errors = False
except:
if retries >= 3:
exit(1)
retries += 1
log_warning("message list doesn't contain the expected message or is empty, retrying...")
log_info('Checking if the "DATASTORE_MEMORY_SIZE" input have the overridden value ...')
input_datastore = safe_get_element(DRIVER, By.ID, "DATASTORE_MEMORY_SIZE")
assert isinstance(input_datastore, WebElement), "Input is not a WebElement"
if not input_datastore.get_attribute("disabled"):
log_error('The input "DATASTORE_MEMORY_SIZE" is not disabled, even though it should be, exiting ...')
exit(1)
elif input_datastore.get_attribute("value") != "384m":
log_error(f"The value is not the expected one ({input_datastore.get_attribute('value')} instead of 384m), exiting ...")
exit(1)
log_info("The value is the expected one and the input is disabled, trying to edit the global config with wrong values ...")
input_worker = safe_get_element(DRIVER, By.ID, "WORKER_RLIMIT_NOFILE")
assert isinstance(input_worker, WebElement), "Input is not a WebElement"
input_worker.clear()
input_worker.send_keys("ZZZ")
assert_button_click(DRIVER, "//form[@id='form-edit-global-configs']//button[@type='submit']")
assert_alert_message(DRIVER, "The global configuration was not edited because no values were changed.")
log_info("The form was not submitted, trying to edit the global config with good values ...")
input_worker.clear()
input_worker.send_keys("4096")
access_page(DRIVER, "//form[@id='form-edit-global-configs']//button[@type='submit']", "global config", False)
if TEST_TYPE == "linux":
wait_for_service()
input_worker = safe_get_element(DRIVER, By.ID, "WORKER_RLIMIT_NOFILE")
assert isinstance(input_worker, WebElement), "Input is not a WebElement"
if input_worker.get_attribute("value") != "4096":
log_error(f"The value was not updated ({input_worker.get_attribute('value')} instead of 4096), exiting ...")
exit(1)
log_info("The value was updated successfully, trying to navigate through the global config tabs ...")
buttons = safe_get_element(DRIVER, By.XPATH, "//div[@data-global-config-tabs-desktop='']/button", multiple=True)
assert isinstance(buttons, list), "Buttons is not a list of WebElements"
shuffle(buttons)
for button in buttons:
assert_button_click(DRIVER, button)
log_info("Trying to filter the global config ...")
setting_filter_elem = safe_get_element(DRIVER, By.ID, "settings-filter")
assert isinstance(setting_filter_elem, WebElement), "Setting filter input is not a WebElement"
setting_filter_elem.send_keys("Datastore")
settings = safe_get_element(
DRIVER,
By.XPATH,
"//form[@id='form-edit-global-configs']//div[@data-setting-container='' and not(contains(@class, 'hidden'))]",
multiple=True,
)
assert isinstance(settings, list), "Hidden settings is not a list of WebElements"
if len(settings) != 1:
log_error(f"The filter didn't work (found {len(settings)} settings instead of 1), exiting ...")
exit(1)
log_info("✅ Global config page tests finished successfully")
except SystemExit as e:
exit_code = e.code
except KeyboardInterrupt:
exit_code = 1
except:
log_exception("Something went wrong, exiting ...")
DRIVER.save_screenshot("error.png")
exit_code = 1
finally:
DRIVER.quit()
exit(exit_code)

4
tests/ui/home_page.py Normal file
View file

@ -0,0 +1,4 @@
from wizard import UI_URL
print(f"done, UI_URL: {UI_URL}", flush=True)
# TODO

View file

@ -0,0 +1,54 @@
from logging import info as log_info, exception as log_exception, warning as log_warning
from selenium.webdriver.common.by import By
from selenium.common.exceptions import TimeoutException
from wizard import DRIVER
from base import TEST_TYPE
from utils import access_page, assert_alert_message, safe_get_element, wait_for_service
exit_code = 0
try:
log_info("Navigating to the instances page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[2]/a", "instances")
no_errors = True
retries = 0
action = "reload" if TEST_TYPE == "docker" else "restart"
while no_errors:
log_info(f"Trying to {action} BunkerWeb instance ...")
try:
form = safe_get_element(DRIVER, By.XPATH, "//form[starts-with(@id, 'form-instance-')]")
except TimeoutException:
log_exception("No instance form found, exiting ...")
exit(1)
try:
access_page(DRIVER, f"//form[starts-with(@id, 'form-instance-')]//button[@value='{action}']", "instances", False)
log_info(f"Instance was {action}ed successfully, checking the message ...")
assert_alert_message(DRIVER, f"has been {action}ed")
no_errors = False
except:
if retries >= 3:
exit(1)
retries += 1
log_warning("Message list doesn't contain the expected message or is empty, retrying...")
if TEST_TYPE == "linux":
wait_for_service()
log_info("✅ Instances page tests finished successfully")
except SystemExit as e:
exit_code = e.code
except KeyboardInterrupt:
exit_code = 1
except:
log_exception("Something went wrong, exiting ...")
DRIVER.save_screenshot("error.png")
exit_code = 1
finally:
DRIVER.quit()
exit(exit_code)

95
tests/ui/plugins_page.py Normal file
View file

@ -0,0 +1,95 @@
from contextlib import suppress
from logging import info as log_info, exception as log_exception, error as log_error
from pathlib import Path
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import TimeoutException
from wizard import DRIVER
from base import TEST_TYPE
from utils import access_page, assert_button_click, safe_get_element, wait_for_service
exit_code = 0
try:
log_info("Navigating to the plugins page to create a new service ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[6]/a", "plugins")
log_info("Trying to reload the plugins without adding any ...")
reload_button = safe_get_element(DRIVER, By.XPATH, "//div[@data-plugins-upload='']//button[@type='submit']")
assert isinstance(reload_button, WebElement), "Reload button is not a WebElement"
if reload_button.get_attribute("disabled") is None:
log_error("The reload button is not disabled, exiting ...")
exit(1)
log_info("Trying to filter the plugins ...")
key_word_filter_input = safe_get_element(DRIVER, By.XPATH, "//input[@placeholder='key words']")
assert isinstance(key_word_filter_input, WebElement), "Key word filter input is not a WebElement"
key_word_filter_input.send_keys("Anti")
plugins = safe_get_element(DRIVER, By.XPATH, "//div[@data-plugins-list='']", multiple=True)
assert isinstance(plugins, list), "Plugins list is not a list"
if len(plugins) != 1:
log_error("The filter is not working, exiting ...")
exit(1)
log_info("The filter is working, trying to add a bad plugin ...")
file_input = safe_get_element(DRIVER, By.XPATH, "//input[@type='file' and @name='file']")
assert isinstance(file_input, WebElement), "File input is not a WebElement"
file_input.send_keys(Path.cwd().joinpath("test.zip").as_posix())
access_page(DRIVER, "//div[@data-plugins-upload='']//button[@type='submit']", "plugins", False)
log_info("The bad plugin has been rejected, trying to add a good plugin ...")
file_input = safe_get_element(DRIVER, By.XPATH, "//input[@type='file' and @name='file']")
assert isinstance(file_input, WebElement), "File input is not a WebElement"
file_input.send_keys(Path.cwd().joinpath("discord.zip").as_posix())
access_page(DRIVER, "//div[@data-plugins-upload='']//button[@type='submit']", "plugins", False)
if TEST_TYPE == "linux":
wait_for_service()
external_plugins = safe_get_element(DRIVER, By.XPATH, "//div[@data-plugins-external=' external ']", multiple=True)
assert isinstance(external_plugins, list), "External plugins list is not a list"
if len(external_plugins) != 1:
log_error("The plugin hasn't been added, exiting ...")
exit(1)
log_info("The plugin has been added, trying delete it ...")
assert_button_click(DRIVER, "//button[@data-plugins-action='delete' and @name='discord']")
access_page(DRIVER, "//form[@data-plugins-modal-form-delete='']//button[@type='submit']", "plugins", False)
if TEST_TYPE == "linux":
wait_for_service()
with suppress(TimeoutException):
if safe_get_element(DRIVER, By.XPATH, "//button[@data-plugins-action='delete' and @name='discord']", error=True):
log_error("The plugin hasn't been deleted, exiting ...")
exit(1)
log_info("The plugin has been deleted")
# TODO add test for plugin pages
log_info("✅ Plugins page tests finished successfully")
except SystemExit as e:
exit_code = e.code
except KeyboardInterrupt:
exit_code = 1
except:
log_exception("Something went wrong, exiting ...")
DRIVER.save_screenshot("error.png")
exit_code = 1
finally:
DRIVER.quit()
exit(exit_code)

282
tests/ui/services_page.py Normal file
View file

@ -0,0 +1,282 @@
from contextlib import suppress
from logging import info as log_info, exception as log_exception, error as log_error
from time import sleep
from requests import RequestException, get
from selenium.webdriver.common.by import By
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import TimeoutException
from wizard import DRIVER
from base import TEST_TYPE
from utils import access_page, assert_alert_message, assert_button_click, safe_get_element, wait_for_service
exit_code = 0
try:
log_info("Navigating to the services page ...")
access_page(DRIVER, "/html/body/aside[1]/div[1]/div[3]/ul/li[4]/a", "services")
service_name_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='www.example.com']//h5")
assert isinstance(service_name_elem, WebElement), "Service name element is not a WebElement"
if service_name_elem.text.strip() != "www.example.com":
log_error("The service is not present, exiting ...")
exit(1)
service_method_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='www.example.com']//h6")
assert isinstance(service_method_elem, WebElement), "Service method element is not a WebElement"
if service_method_elem.text.strip() != "ui":
log_error("The service should have been created by the ui, exiting ...")
exit(1)
log_info("Service www.example.com is present, trying to edit it ...")
assert_button_click(DRIVER, "//div[@data-services-service='www.example.com']//button[@data-services-action='edit']")
modal = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-modal='']")
assert isinstance(modal, WebElement), "Modal is not a WebElement"
if "hidden" in (modal.get_attribute("class") or ""):
log_error("Modal is hidden even though it shouldn't be, exiting ...")
exit(1)
input_server_name = safe_get_element(DRIVER, By.ID, "SERVER_NAME")
assert isinstance(input_server_name, WebElement), "Input is not a WebElement"
if input_server_name.get_attribute("value") != "www.example.com":
log_error("The value is not the expected one, exiting ...")
exit(1)
log_info('The value for the "SERVER_NAME" input is the expected one, trying to edit the config ...')
assert_button_click(DRIVER, "//button[@data-tab-handler='gzip']")
gzip_select = safe_get_element(DRIVER, By.XPATH, "//button[@data-setting-select='gzip-comp-level']")
assert isinstance(gzip_select, WebElement), "Gzip select is not a WebElement"
assert_button_click(DRIVER, gzip_select)
assert_button_click(DRIVER, "//button[@data-setting-select-dropdown-btn='gzip-comp-level' and @value='6']")
access_page(DRIVER, "//button[@data-services-modal-submit='']", "services", False)
if TEST_TYPE == "linux":
wait_for_service()
log_info("The page reloaded successfully, checking if the setting has been updated ...")
assert_button_click(DRIVER, "//div[@data-services-service='www.example.com']//button[@data-services-action='edit']")
modal = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-modal='']")
assert isinstance(modal, WebElement), "Modal is not a WebElement"
if "hidden" in (modal.get_attribute("class") or ""):
log_error("Modal is hidden even though it shouldn't be, exiting ...")
exit(1)
assert_button_click(DRIVER, "//button[@data-tab-handler='gzip']")
gzip_comp_level_selected_elem = safe_get_element(DRIVER, By.XPATH, "//select[@id='GZIP_COMP_LEVEL']/option[@selected='']")
assert isinstance(gzip_comp_level_selected_elem, WebElement), "Gzip comp level selected element is not a WebElement"
if gzip_comp_level_selected_elem.get_attribute("value") != "6":
log_error("The value is not the expected one, exiting ...")
exit(1)
assert_button_click(DRIVER, "//button[@data-services-modal-close='']/*[local-name() = 'svg']")
log_info("Creating a new service ...")
assert_button_click(DRIVER, "//button[@data-services-action='new']")
server_name_input = safe_get_element(DRIVER, By.ID, "SERVER_NAME")
assert isinstance(server_name_input, WebElement), "Input is not a WebElement"
server_name_input.clear()
server_name_input.send_keys("app1.example.com")
if TEST_TYPE == "docker":
assert_button_click(DRIVER, "//button[@data-tab-handler='reverseproxy']")
use_reverse_proxy_checkbox = safe_get_element(DRIVER, By.ID, "USE_REVERSE_PROXY")
assert isinstance(use_reverse_proxy_checkbox, WebElement), "Use reverse proxy checkbox is not a WebElement"
assert_button_click(DRIVER, use_reverse_proxy_checkbox)
reverse_proxy_host_input = safe_get_element(DRIVER, By.ID, "REVERSE_PROXY_HOST")
assert isinstance(reverse_proxy_host_input, WebElement), "Reverse proxy host input is not a WebElement"
reverse_proxy_host_input.send_keys("http://app1:8080")
reverse_proxy_url_input = safe_get_element(DRIVER, By.ID, "REVERSE_PROXY_URL")
assert isinstance(reverse_proxy_url_input, WebElement), "Reverse proxy url input is not a WebElement"
reverse_proxy_url_input.send_keys("/")
access_page(DRIVER, "//button[@data-services-modal-submit='']", "services", False)
if TEST_TYPE == "linux":
wait_for_service("app1.example.com")
try:
services = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service]", multiple=True, error=True)
assert isinstance(services, list), "Services is not a list"
except TimeoutException:
log_exception("Services not found, exiting ...")
exit(1)
if len(services) < 2:
log_error("The service hasn't been created, exiting ...")
exit(1)
server_name_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='app1.example.com']//h5")
assert isinstance(server_name_elem, WebElement), "Server name element is not a WebElement"
if server_name_elem.text.strip() != "app1.example.com":
log_error('The service "app1.example.com" is not present, exiting ...')
exit(1)
service_method_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='app1.example.com']//h6")
assert isinstance(service_method_elem, WebElement), "Service method element is not a WebElement"
if service_method_elem.text.strip() != "ui":
log_error("The service should have been created by the ui, exiting ...")
exit(1)
log_info("Service app1.example.com is present, trying it ...")
try:
safe_get_element(DRIVER, By.XPATH, "//button[@data-services-action='edit' and @data-services-name='app1.example.com']//ancestor::div//a", error=True)
except TimeoutException:
log_exception("Delete button hasn't been found, even though it should be, exiting ...")
exit(1)
wait_for_service("app1.example.com")
log_info("The service is working, trying to clone it ...")
try:
clone_button = safe_get_element(DRIVER, By.XPATH, "//button[@data-services-action='clone' and @data-services-name='app1.example.com']", error=True)
assert isinstance(clone_button, WebElement), "Clone button is not a WebElement"
except TimeoutException:
log_exception("Clone button hasn't been found, even though it should be, exiting ...")
exit(1)
assert_button_click(DRIVER, clone_button)
server_name_input = safe_get_element(DRIVER, By.ID, "SERVER_NAME")
assert isinstance(server_name_input, WebElement), "Input is not a WebElement"
if server_name_input.get_attribute("value"):
log_error("The cloned service input is not empty, exiting ...")
exit(1)
server_name_input.clear()
server_name_input.send_keys("app2.example.com")
access_page(DRIVER, "//button[@data-services-modal-submit='']", "services", False)
if TEST_TYPE == "linux":
wait_for_service("app2.example.com")
try:
services = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service]", multiple=True, error=True)
assert isinstance(services, list), "Services is not a list"
except TimeoutException:
log_exception("Services not found, exiting ...")
exit(1)
if len(services) < 3:
log_error(f"The service hasn't been created ({len(services)} services found), exiting ...")
exit(1)
server_name_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='app2.example.com']//h5")
assert isinstance(server_name_elem, WebElement), "Server name element is not a WebElement"
if server_name_elem.text.strip() != "app2.example.com":
log_error('The service "app2.example.com" is not present, exiting ...')
exit(1)
service_method_elem = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='app2.example.com']//h6")
assert isinstance(service_method_elem, WebElement), "Service method element is not a WebElement"
if service_method_elem.text.strip() != "ui":
log_error("The service should have been created by the ui, exiting ...")
exit(1)
log_info("Service app2.example.com is present, trying it ...")
try:
safe_get_element(DRIVER, By.XPATH, "//button[@data-services-action='edit' and @data-services-name='app2.example.com']//ancestor::div//a", error=True)
except TimeoutException:
log_error("Delete button hasn't been found, even though it should be, exiting ...")
exit(1)
wait_for_service("app2.example.com")
log_info("The service is working, trying to set it as draft ...")
assert_button_click(DRIVER, "//div[@data-services-service='app2.example.com']//button[@data-services-action='edit']")
assert_button_click(DRIVER, "//button[@data-toggle-draft-btn='']")
access_page(DRIVER, "//button[@data-services-modal-submit='']", "services", False)
if TEST_TYPE == "linux":
wait_for_service()
try:
services = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service]", multiple=True, error=True)
assert isinstance(services, list), "Services is not a list"
except TimeoutException:
log_exception("Services not found, exiting ...")
exit(1)
if len(services) < 3:
log_error(f"The service has been deleted ({len(services)} services found), exiting ...")
exit(1)
sleep(30)
log_info("Service app2.example.com has been set as draft, making sure it's not working anymore ...")
for _ in range(5):
with suppress(RequestException):
if get("http://app2.example.com").status_code < 400:
log_error("The service is still working, exiting ...")
exit(1)
log_info("The service is not working, as expected, trying to delete it ...")
try:
delete_button = safe_get_element(DRIVER, By.XPATH, "//button[@data-services-action='delete' and @data-services-name='app2.example.com']", error=True)
assert isinstance(delete_button, WebElement), "Delete button is not a WebElement"
except TimeoutException:
log_exception("Delete button hasn't been found, even though it should be, exiting ...")
exit(1)
log_info("Delete button is present, as expected, deleting the service ...")
assert_button_click(DRIVER, delete_button)
access_page(DRIVER, "//form[@data-services-modal-form-delete='']//button[@type='submit']", "services", False)
if TEST_TYPE == "linux":
wait_for_service()
assert_alert_message(DRIVER, "has been deleted.")
log_info("Service app2.example.com has been deleted, checking if it's still present ...")
try:
services = safe_get_element(DRIVER, By.XPATH, "//div[@data-services-service='']", multiple=True, error=True)
assert isinstance(services, list), "Services is not a list"
except TimeoutException:
log_exception("Services not found, exiting ...")
exit(1)
if len(services) > 2:
log_error(f"The service hasn't been deleted ({len(services)} services found), exiting ...")
exit(1)
log_info("Service app2.example.com has been deleted successfully")
log_info("✅ Services page tests finished successfully")
except SystemExit as e:
exit_code = e.code
except KeyboardInterrupt:
exit_code = 1
except:
log_exception("Something went wrong, exiting ...")
DRIVER.save_screenshot("error.png")
exit_code = 1
finally:
DRIVER.quit()
exit(exit_code)

View file

@ -1,16 +1,22 @@
#!/bin/bash
integration=$1
test=$2
if [ -z "$integration" ] ; then
echo "Please provide an integration name as argument ❌"
exit 1
elif [ -z "$test" ] ; then
echo "Please provide a test name as argument ❌"
exit 1
elif [ "$integration" != "docker" ] && [ "$integration" != "linux" ] ; then
echo "Integration \"$integration\" is not supported ❌"
exit 1
fi
echo "🌐 Building UI stack for integration \"$integration\" ..."
test=$(basename "$test")
echo "🌐 Building UI stack for integration \"$integration\", test: $test ..."
cleanup_stack () {
echo "🌐 Cleaning up current stack ..."
@ -141,6 +147,7 @@ fi
# Start tests
if [ "$integration" == "docker" ] ; then
echo "TEST_FILE=$test" > .env
docker-compose -f docker-compose.test.yml build
# shellcheck disable=SC2181
if [ $? -ne 0 ] ; then
@ -149,8 +156,9 @@ if [ "$integration" == "docker" ] ; then
fi
docker-compose -f docker-compose.test.yml up --abort-on-container-exit --exit-code-from ui-tests
rm -f .env
else
python3 main.py
python3 "$test"
fi
# shellcheck disable=SC2181

134
tests/ui/utils.py Normal file
View file

@ -0,0 +1,134 @@
from contextlib import suppress
from logging import error as log_error, exception as log_exception, info as log_info, warning as log_warning
from time import sleep
from typing import List, Optional, Union
from requests import RequestException, get
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.support import expected_conditions as EC
from selenium.common.exceptions import ElementClickInterceptedException, TimeoutException, WebDriverException
def safe_get_element(driver, by: str, _id: str, *, driver_wait: Optional[WebDriverWait] = None, multiple: bool = False, error: bool = False) -> Union[WebElement, List[WebElement]]:
try:
return (driver_wait or WebDriverWait(driver, 4)).until(EC.presence_of_element_located((by, _id)) if not multiple else EC.presence_of_all_elements_located((by, _id)))
except TimeoutException as e:
if error:
raise e
log_exception(f'Element searched by {by}: "{_id}" not found, exiting ...')
exit(1)
def assert_button_click(driver, button: Union[str, WebElement]): # type: ignore
clicked = False
while not clicked:
with suppress(ElementClickInterceptedException):
if isinstance(button, str):
button: Union[WebElement, List[WebElement]] = safe_get_element(driver, By.XPATH, button)
assert isinstance(button, WebElement), "Button is not a WebElement"
sleep(0.5)
button.click()
clicked = True
return clicked
def assert_alert_message(driver, message: str):
safe_get_element(driver, By.XPATH, "//button[@data-flash-sidebar-open='']")
sleep(0.3)
assert_button_click(driver, "//button[@data-flash-sidebar-open='']")
error = False
while True:
try:
alerts: Union[WebElement, List[WebElement]] = safe_get_element(
driver,
By.XPATH,
"//aside[@data-flash-sidebar='']/div[2]/div",
multiple=True,
error=True,
)
assert isinstance(alerts, list), "Alerts is not a list of WebElements"
break
except TimeoutException:
if error:
log_exception("Messages list not found, exiting ...")
exit(1)
error = True
driver.refresh()
is_in = False
for alert in alerts:
if message in alert.text:
is_in = True
break
if not is_in:
log_error(f'Message "{message}" not found in one of the messages in the list, exiting ...')
exit(1)
log_info(f'Message "{message}" found in one of the messages in the list')
assert_button_click(driver, "//button[@data-flash-sidebar-close='']/*[local-name() = 'svg']")
def access_page(driver, button: Union[str, WebElement], name: str, message: bool = True, *, retries: int = 0, clicked: bool = False):
if retries > 5:
log_error("Too many retries...")
exit(1)
try:
if not clicked:
clicked = assert_button_click(driver, button)
title: Union[WebElement, List[WebElement]] = safe_get_element(driver, By.XPATH, "/html/body/div/header/div/nav/h6", driver_wait=WebDriverWait(driver, 45))
assert isinstance(title, WebElement), "Title is not a WebElement"
if title.text != name.title():
log_error(f"Didn't get redirected to {name} page, exiting ...")
exit(1)
except TimeoutException:
if "/loading" in driver.current_url:
sleep(2)
return access_page(driver, button, name, message, retries=retries + 1, clicked=clicked)
log_error(f"{name.title()} page didn't load in time, exiting ...")
exit(1)
except WebDriverException as we:
if "connectionFailure" in str(we):
log_warning("Connection failure, retrying in 5s ...")
driver.refresh()
sleep(5)
return access_page(driver, button, name, message, retries=retries + 1, clicked=clicked)
raise we
if message:
log_info(f"{name.title()} page loaded successfully")
def wait_for_service(service: str = "www.example.com"):
ready = False
retries = 0
while not ready:
with suppress(RequestException):
resp = get(f"http://{service}/ready", headers={"Host": service}, verify=False)
status_code = resp.status_code
text = resp.text
if resp.status_code >= 500:
log_error(f"An error occurred while trying to reach {service}, exiting ...")
exit(1)
ready = status_code < 400 and "ready" in text
if retries > 10:
log_error(f"Service {service} took too long to be ready, exiting ...")
exit(1)
elif not ready:
retries += 1
log_warning(f"Waiting for {service} to be ready, retrying in 5s ...")
sleep(5)

138
tests/ui/wizard.py Normal file
View file

@ -0,0 +1,138 @@
from datetime import datetime, timedelta
from logging import error as log_error, exception as log_exception, info as log_info
from time import sleep
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.remote.webelement import WebElement
from selenium.common.exceptions import TimeoutException
from base import DEFAULT_SERVER, DRIVER
from utils import access_page, assert_button_click, safe_get_element
UI_URL = ""
exit_code = None
log_info("Starting the setup wizard ...")
try:
DRIVER.delete_all_cookies()
DRIVER.maximize_window()
driver_wait = WebDriverWait(DRIVER, 45)
log_info(f"Navigating to http://{DEFAULT_SERVER}/setup ...")
DRIVER.get(f"http://{DEFAULT_SERVER}/setup")
try:
title = safe_get_element(DRIVER, By.XPATH, "/html/body/main/div/div/h1", driver_wait=driver_wait)
assert isinstance(title, WebElement), "Title is not a WebElement"
if title.text != "Setup Wizard":
log_error("Didn't get redirected to setup page, exiting ...")
exit(1)
except TimeoutException:
log_exception("Didn't get redirected to setup page, exiting ...")
exit(1)
log_info("Setup page loaded successfully, filling the form ...")
admin_username_input = safe_get_element(DRIVER, By.ID, "admin_username")
assert isinstance(admin_username_input, WebElement), "Admin username input is not a WebElement"
admin_username_input.send_keys("admin")
password_input = safe_get_element(DRIVER, By.ID, "admin_password")
assert isinstance(password_input, WebElement), "Password input is not a WebElement"
password_input.send_keys("S$cr3tP@ssw0rd")
password_check_input = safe_get_element(DRIVER, By.ID, "admin_password_check")
assert isinstance(password_check_input, WebElement), "Password check input is not a WebElement"
password_check_input.send_keys("S$cr3tP@ssw0rd")
ui_url_elem = safe_get_element(DRIVER, By.ID, "ui_url")
assert isinstance(ui_url_elem, WebElement), "UI URL input is not a WebElement"
UI_URL = ui_url_elem.get_attribute("value")
assert_button_click(DRIVER, "//button[@id='setup-button']")
log_info("Submitted the form, waiting for the wizard to finish ...")
current_time = datetime.now()
while current_time + timedelta(minutes=5) > datetime.now() and not DRIVER.current_url.endswith("/login"):
sleep(1)
if not DRIVER.current_url.endswith("/login"):
log_error("Didn't get redirected to login page, exiting ...")
exit(1)
log_info("Redirected to login page, waiting for login form ...")
safe_get_element(DRIVER, By.TAG_NAME, "form")
log_info("Form found, trying to access another page without being logged in ...")
DRIVER.get(f"http://www.example.com{UI_URL}/home")
log_info("Waiting for toast ...")
toast = safe_get_element(DRIVER, By.XPATH, "//div[@data-flash-message='']")
assert isinstance(toast, WebElement), "Toast is not a WebElement"
log_info("Toast found")
if "Please log in to access this page." not in toast.text:
log_error("Toast doesn't contain the expected message, exiting ...")
exit(1)
log_info("Toast contains the expected message, filling login form with wrong credentials ...")
sleep(1)
safe_get_element(DRIVER, By.TAG_NAME, "form")
username_input = safe_get_element(DRIVER, By.ID, "username")
assert isinstance(username_input, WebElement), "Username input is not a WebElement"
username_input.send_keys("hackerman")
password_input = safe_get_element(DRIVER, By.ID, "password")
assert isinstance(password_input, WebElement), "Password input is not a WebElement"
password_input.send_keys("password")
password_input.send_keys(Keys.RETURN)
sleep(0.3)
try:
title = safe_get_element(DRIVER, By.XPATH, "/html/body/main/div[1]/div/h1", driver_wait=driver_wait)
assert isinstance(title, WebElement), "Title is not a WebElement"
if title.text != "Log in":
log_error("Didn't get redirected to login page, exiting ...")
exit(1)
except TimeoutException:
log_exception("Login page didn't load in time, exiting ...")
exit(1)
log_info("Got redirected to login page successfully, filling login form with good credentials ...")
username_input = safe_get_element(DRIVER, By.ID, "username")
assert isinstance(username_input, WebElement), "Username input is not a WebElement"
username_input.send_keys("admin")
password_input = safe_get_element(DRIVER, By.ID, "password")
assert isinstance(password_input, WebElement), "Password input is not a WebElement"
password_input.send_keys("S$cr3tP@ssw0rd")
access_page(DRIVER, "//button[@value='login']", "home")
except SystemExit as e:
exit_code = e.code
except KeyboardInterrupt:
exit_code = 1
except:
log_exception("Something went wrong, exiting ...")
DRIVER.save_screenshot("error.png")
exit_code = 1
finally:
if exit_code is not None:
DRIVER.quit()
exit(exit_code)