Update tests
This commit is contained in:
15
tests/conftest.py
Normal file
15
tests/conftest.py
Normal file
@ -0,0 +1,15 @@
|
||||
"""Capture screenshots on failure."""
|
||||
import allure
|
||||
import base64
|
||||
|
||||
from allure_commons.types import AttachmentType
|
||||
|
||||
def pytest_selenium_capture_debug(item, report, extra):
|
||||
for log_type in extra:
|
||||
if log_type["name"] == "Screenshot":
|
||||
content = base64.b64decode(log_type["content"].encode("utf-8"))
|
||||
allure.attach(
|
||||
content,
|
||||
name="Screenshot on failure",
|
||||
attachment_type=AttachmentType.PNG,
|
||||
)
|
||||
1
tests/frontend/__init__.py
Normal file
1
tests/frontend/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
129
tests/frontend/conftest.py
Normal file
129
tests/frontend/conftest.py
Normal file
@ -0,0 +1,129 @@
|
||||
"""Test fixtures for the frontend."""
|
||||
|
||||
import asyncio
|
||||
import secrets
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import uvicorn
|
||||
|
||||
|
||||
from sshecret_admin.core.app import create_admin_app
|
||||
from sshecret_admin.core.settings import AdminServerSettings
|
||||
from sshecret_backend.app import create_backend_app
|
||||
from sshecret_backend.settings import BackendSettings
|
||||
from sshecret_backend.testing import create_test_token
|
||||
|
||||
from tests.helpers import create_test_admin_user, in_tempdir
|
||||
from tests.types import PortFactory, TestPorts
|
||||
|
||||
|
||||
@pytest.fixture(name="ui_test_ports", scope="session")
|
||||
def generate_test_ports(unused_tcp_port_factory: PortFactory) -> TestPorts:
|
||||
"""Generate the test ports."""
|
||||
test_ports = TestPorts(
|
||||
backend=unused_tcp_port_factory(),
|
||||
admin=unused_tcp_port_factory(),
|
||||
sshd=unused_tcp_port_factory(),
|
||||
)
|
||||
print(f"{test_ports=!r}")
|
||||
return test_ports
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", name="ui_backend_server")
|
||||
def run_backend_server(ui_test_ports: TestPorts):
|
||||
"""Run the backend server in a thread."""
|
||||
port = ui_test_ports.backend
|
||||
|
||||
with tempfile.TemporaryDirectory() as tmp_dir:
|
||||
backend_work_path = Path(tmp_dir)
|
||||
db_file = backend_work_path / "backend.db"
|
||||
backend_settings = BackendSettings(database=str(db_file.absolute()))
|
||||
backend_app = create_backend_app(backend_settings)
|
||||
token = create_test_token(backend_settings)
|
||||
|
||||
config = uvicorn.Config(
|
||||
app=backend_app, port=port, host="127.0.0.1", log_level="warning"
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
def run():
|
||||
asyncio.run(server.serve())
|
||||
|
||||
thread = threading.Thread(target=run)
|
||||
thread.start()
|
||||
|
||||
backend_url = f"http://127.0.0.1:{port}"
|
||||
for _ in range(30):
|
||||
try:
|
||||
r = requests.get(backend_url)
|
||||
if r.status_code < 500:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise RuntimeError("Backend server did not start in time")
|
||||
|
||||
yield backend_url, token
|
||||
|
||||
server.should_exit = True
|
||||
thread.join()
|
||||
|
||||
|
||||
@pytest.fixture(scope="function", name="ui_admin_server")
|
||||
def run_admin_server(ui_test_ports: TestPorts, ui_backend_server: tuple[str, str]):
|
||||
"""Run the admin server in a thread."""
|
||||
backend_url, backend_token = ui_backend_server
|
||||
port = ui_test_ports.admin
|
||||
secret_key = secrets.token_urlsafe(32)
|
||||
|
||||
with in_tempdir() as admin_work_path:
|
||||
admin_db = admin_work_path / "ssh_admin.db"
|
||||
admin_settings = AdminServerSettings.model_validate(
|
||||
{
|
||||
"sshecret_backend_url": backend_url,
|
||||
"backend_token": backend_token,
|
||||
"secret_key": secret_key,
|
||||
"listen_address": "127.0.0.1",
|
||||
"port": port,
|
||||
"database": str(admin_db.absolute()),
|
||||
"password_manager_directory": str(admin_work_path.absolute()),
|
||||
}
|
||||
)
|
||||
|
||||
admin_app = create_admin_app(admin_settings)
|
||||
config = uvicorn.Config(
|
||||
app=admin_app, port=port, host="127.0.0.1", log_level="warning"
|
||||
)
|
||||
server = uvicorn.Server(config)
|
||||
|
||||
def run():
|
||||
asyncio.run(server.serve())
|
||||
|
||||
thread = threading.Thread(target=run)
|
||||
thread.start()
|
||||
|
||||
admin_url = f"http://127.0.0.1:{port}"
|
||||
admin_password = secrets.token_urlsafe(10)
|
||||
create_test_admin_user(admin_settings, "test", admin_password)
|
||||
|
||||
for _ in range(30):
|
||||
try:
|
||||
r = requests.get(admin_url)
|
||||
if r.status_code < 500:
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise RuntimeError("Admin server did not start in time")
|
||||
|
||||
yield admin_url, ("test", admin_password)
|
||||
|
||||
server.should_exit = True
|
||||
thread.join()
|
||||
1
tests/frontend/helpers/__init__.py
Normal file
1
tests/frontend/helpers/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
|
||||
30
tests/frontend/helpers/auth.py
Normal file
30
tests/frontend/helpers/auth.py
Normal file
@ -0,0 +1,30 @@
|
||||
"""Auth helpers."""
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from tests.integration.types import AdminServer
|
||||
from .wait_helpers import wait_until_url_contains
|
||||
|
||||
def login(ui_admin_server: AdminServer, driver: WebDriver) -> WebDriver:
|
||||
"""Log in."""
|
||||
admin_url, credentials = ui_admin_server
|
||||
username, password = credentials
|
||||
|
||||
driver.get(admin_url + "/login")
|
||||
username_input = driver.find_element(By.NAME, "username")
|
||||
password_input = driver.find_element(By.NAME, "password")
|
||||
submit_button = driver.find_element(By.XPATH, "//button[@type='submit']")
|
||||
assert username_input is not None
|
||||
assert password_input is not None
|
||||
assert submit_button.text.lower() == "sign in"
|
||||
|
||||
username_input.clear()
|
||||
username_input.send_keys(username)
|
||||
password_input.send_keys(password)
|
||||
|
||||
|
||||
submit_button.click()
|
||||
|
||||
wait_until_url_contains(driver, "/dashboard")
|
||||
|
||||
return driver
|
||||
66
tests/frontend/helpers/db.py
Normal file
66
tests/frontend/helpers/db.py
Normal file
@ -0,0 +1,66 @@
|
||||
"""Database helpers.
|
||||
|
||||
Allows pre-loading database for tests.
|
||||
"""
|
||||
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
import httpx
|
||||
from sshecret.crypto import generate_private_key, generate_public_key_string
|
||||
|
||||
|
||||
class DatabasePreloader:
|
||||
"""Database preloader class."""
|
||||
|
||||
def __init__(self, admin_url: str, username: str, password: str) -> None:
|
||||
"""Instantiate class to populate database."""
|
||||
self.admin_url: str = admin_url
|
||||
self.username: str = username
|
||||
self.password: str = password
|
||||
|
||||
@contextmanager
|
||||
def login(self) -> Iterator[httpx.Client]:
|
||||
"""Login and yield client."""
|
||||
login_client = httpx.Client(base_url=self.admin_url)
|
||||
resp = login_client.post(
|
||||
"api/v1/token",
|
||||
data={"username": self.username, "password": self.password}
|
||||
)
|
||||
assert resp.status_code == 200
|
||||
data = resp.json()
|
||||
token = data["access_token"]
|
||||
headers = {"Authorization": f"Bearer {token}"}
|
||||
with httpx.Client(base_url=self.admin_url, headers=headers) as client:
|
||||
yield client
|
||||
|
||||
def create_client(self, *names: str) -> None:
|
||||
"""Create one or more clients."""
|
||||
with self.login() as http_client:
|
||||
for name in names:
|
||||
private_key = generate_private_key()
|
||||
public_key = generate_public_key_string(private_key.public_key())
|
||||
data = {
|
||||
"name": name,
|
||||
"description": "Test client",
|
||||
"public_key": public_key,
|
||||
"sources": ["0.0.0.0/0", "::/0"],
|
||||
}
|
||||
resp = http_client.post("api/v1/clients/", json=data)
|
||||
resp.raise_for_status()
|
||||
|
||||
def create_secret(self, *secrets: tuple[str, list[str]]) -> None:
|
||||
"""Create secret.
|
||||
|
||||
Argument format is (secret_name, [client1, client2, ...])
|
||||
|
||||
Clients must exist.
|
||||
"""
|
||||
with self.login() as http_client:
|
||||
for name, clients in secrets:
|
||||
data = {
|
||||
"name": name,
|
||||
"clients": clients,
|
||||
"value": {"auto_generate": True, "length": 32}
|
||||
}
|
||||
|
||||
http_client.post("api/v1/secrets/", json=data)
|
||||
63
tests/frontend/helpers/wait_helpers.py
Normal file
63
tests/frontend/helpers/wait_helpers.py
Normal file
@ -0,0 +1,63 @@
|
||||
"""
|
||||
Collection of waiting statements.
|
||||
"""
|
||||
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
|
||||
def wait_for_url_change(driver: WebDriver, old_url: str, timeout: int = 10) -> None:
|
||||
WebDriverWait(driver, timeout).until(lambda d: d.current_url != old_url)
|
||||
|
||||
|
||||
def wait_until_url_contains(driver: WebDriver, text: str, timeout: int = 10) -> None:
|
||||
WebDriverWait(driver, timeout).until(lambda d: text in d.current_url)
|
||||
|
||||
|
||||
def wait_for_element(driver: WebDriver, by: str, value: str, timeout: int = 10):
|
||||
return WebDriverWait(driver, timeout).until(
|
||||
EC.presence_of_element_located((by, value))
|
||||
)
|
||||
|
||||
|
||||
def wait_for_element_with_text(
|
||||
driver: WebDriver, tag: str, text: str, timeout: int = 10
|
||||
):
|
||||
return WebDriverWait(driver, timeout).until(
|
||||
EC.text_to_be_present_in_element((By.TAG_NAME, tag), text)
|
||||
)
|
||||
|
||||
|
||||
def wait_for_clickable(driver: WebDriver, by: str, value: str, timeout: int = 10):
|
||||
return WebDriverWait(driver, timeout).until(EC.element_to_be_clickable((by, value)))
|
||||
|
||||
|
||||
def wait_for_element_to_be_visisble(driver: WebDriver, id: str, timeout: int = 10):
|
||||
return WebDriverWait(driver, timeout).until(
|
||||
EC.visibility_of_element_located((By.ID, id))
|
||||
)
|
||||
|
||||
|
||||
def wait_for_element_to_be_disabled(
|
||||
driver: WebDriver, by: str, value: str, timeout: int = 10
|
||||
):
|
||||
"""Wait for an element to be disabled."""
|
||||
return WebDriverWait(driver, timeout).until(
|
||||
EC.none_of(EC.element_to_be_clickable((by, value)))
|
||||
)
|
||||
|
||||
|
||||
def wait_for_alert(driver: WebDriver, timeout: int = 10):
|
||||
"""Wait for an alert."""
|
||||
return WebDriverWait(driver, timeout).until(lambda d: d.switch_to.alert)
|
||||
|
||||
|
||||
def wait_for_element_to_disappear(
|
||||
driver: WebDriver, by: str, value: str, timeout: int = 10
|
||||
):
|
||||
"""Wait for an element to disappear."""
|
||||
return WebDriverWait(driver, timeout).until(
|
||||
EC.none_of(EC.presence_of_element_located((by, value)))
|
||||
)
|
||||
217
tests/frontend/test_clients.py
Normal file
217
tests/frontend/test_clients.py
Normal file
@ -0,0 +1,217 @@
|
||||
"""Tests for the client page."""
|
||||
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
import pytest
|
||||
from selenium.webdriver.support.ui import WebDriverWait
|
||||
from selenium.webdriver.support import expected_conditions as EC
|
||||
from sshecret.crypto import generate_private_key, generate_public_key_string
|
||||
|
||||
from tests.integration.types import AdminServer
|
||||
|
||||
from .helpers.auth import login
|
||||
from .helpers import wait_helpers
|
||||
|
||||
@pytest.fixture
|
||||
def client_page(ui_admin_server: AdminServer, driver: WebDriver) -> WebDriver:
|
||||
"""Log in and show the client page."""
|
||||
admin_url = ui_admin_server[0]
|
||||
driver = login(ui_admin_server, driver)
|
||||
driver.get(admin_url + "/clients")
|
||||
return driver
|
||||
|
||||
|
||||
class TestClientPage:
|
||||
"""Test client page."""
|
||||
|
||||
def test_client_page_loaded(self, client_page: WebDriver) -> None:
|
||||
"""Test that the client page loads."""
|
||||
# Ensure that the create client button is present
|
||||
client_page.refresh()
|
||||
create_client_button = client_page.find_element(By.ID, "createClientButton")
|
||||
assert create_client_button is not None
|
||||
# Ensure that the table is loaded
|
||||
client_table = client_page.find_element(By.ID, "clientListTable")
|
||||
assert client_table is not None
|
||||
|
||||
def test_create_client_button(self, client_page: WebDriver) -> None:
|
||||
"""Test that the Create Client button works."""
|
||||
client_page.refresh()
|
||||
create_client_button = client_page.find_element(By.ID, "createClientButton")
|
||||
assert create_client_button is not None
|
||||
|
||||
create_client_button.click()
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, "drawer-create-client-default")
|
||||
|
||||
add_client_button = client_page.find_element(By.XPATH, "//button[@type='submit']")
|
||||
assert add_client_button.text.lower() == "add client"
|
||||
|
||||
def test_create_client(self, client_page: WebDriver) -> None:
|
||||
"""Test create clients."""
|
||||
client_page.refresh()
|
||||
private_key = generate_private_key()
|
||||
public_key = generate_public_key_string(private_key.public_key())
|
||||
|
||||
create_client_button = client_page.find_element(By.ID, "createClientButton")
|
||||
assert create_client_button is not None
|
||||
|
||||
create_client_button.click()
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, "drawer-create-client-default")
|
||||
|
||||
drawer = client_page.find_element(By.ID, "drawer-create-client-default")
|
||||
assert drawer is not None
|
||||
name_input = drawer.find_element(By.NAME, "name")
|
||||
assert name_input is not None
|
||||
description_input = drawer.find_element(By.NAME, "description")
|
||||
assert description_input is not None
|
||||
sources_input = drawer.find_element(By.NAME, "sources")
|
||||
assert sources_input is not None
|
||||
public_key_input = drawer.find_element(By.NAME, "public_key")
|
||||
assert public_key_input is not None
|
||||
|
||||
|
||||
name_input.send_keys("testuser")
|
||||
description_input.send_keys("Test")
|
||||
sources_input.clear()
|
||||
sources_input.send_keys("0.0.0.0/0, ::/0")
|
||||
public_key_input.send_keys(public_key)
|
||||
validation_field = drawer.find_element(By.ID, "clientPublicKeyValidation")
|
||||
assert validation_field is not None
|
||||
error_message = validation_field.find_elements(By.TAG_NAME, "p")
|
||||
assert len(error_message) == 0
|
||||
|
||||
# Submit the request
|
||||
|
||||
add_client_button = drawer.find_element(By.XPATH, "//button[@type='submit']")
|
||||
assert add_client_button.text.lower() == "add client"
|
||||
|
||||
add_client_button.click()
|
||||
|
||||
client_appeared = wait_helpers.wait_for_element_with_text(client_page, "td", "testuser")
|
||||
assert client_appeared is not False
|
||||
|
||||
def test_delete_client(self, client_page: WebDriver) -> None:
|
||||
"""Test deletion of a test client."""
|
||||
self.test_create_client(client_page)
|
||||
client_page.refresh()
|
||||
client_field = client_page.find_element(By.XPATH, "//tr/td[contains(text(), 'testuser')]")
|
||||
assert client_field is not None
|
||||
row = client_field.find_element(By.XPATH, "./..")
|
||||
assert row is not None
|
||||
assert row.tag_name == "tr"
|
||||
row_id = row.get_attribute("id")
|
||||
assert row_id is not None
|
||||
print(row_id)
|
||||
client_id = row_id[7:]
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, row_id)
|
||||
|
||||
delete_button = client_page.find_element(By.ID, f"deleteClientButton-{client_id}")
|
||||
assert delete_button is not None
|
||||
|
||||
delete_button.click()
|
||||
|
||||
drawer_name = f"drawer-delete-{row_id}"
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, drawer_name)
|
||||
drawer = client_page.find_element(By.ID, drawer_name)
|
||||
|
||||
assert drawer is not None
|
||||
|
||||
confirm_button = drawer.find_element(By.XPATH, "//button[contains(text(), 'Yes, delete the client')]")
|
||||
assert confirm_button is not None
|
||||
confirm_button.click()
|
||||
|
||||
WebDriverWait(client_page, 10).until(
|
||||
EC.none_of(
|
||||
EC.presence_of_element_located((By.ID, row_id))
|
||||
)
|
||||
)
|
||||
|
||||
def test_update_client(self, client_page: WebDriver) -> None:
|
||||
"""Test updating a client."""
|
||||
self.test_create_client(client_page)
|
||||
|
||||
client_page.refresh()
|
||||
client_field = client_page.find_element(By.XPATH, "//tr/td[contains(text(), 'testuser')]")
|
||||
assert client_field is not None
|
||||
row = client_field.find_element(By.XPATH, "./..")
|
||||
assert row is not None
|
||||
assert row.tag_name == "tr"
|
||||
row_id = row.get_attribute("id")
|
||||
assert row_id is not None
|
||||
print(row_id)
|
||||
client_id = row_id[7:]
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, row_id)
|
||||
update_button = client_page.find_element(By.ID, f"updateClientButton-{client_id}")
|
||||
assert update_button is not None
|
||||
|
||||
update_button.click()
|
||||
|
||||
drawer_name = f"drawer-update-{row_id}"
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, drawer_name)
|
||||
|
||||
drawer = client_page.find_element(By.ID, drawer_name)
|
||||
description_input = drawer.find_element(By.NAME, "description")
|
||||
|
||||
description_input.clear()
|
||||
|
||||
description_input.send_keys("New Description")
|
||||
|
||||
confirm_button = drawer.find_element(By.XPATH, "//button[contains(text(), 'Update')]")
|
||||
assert confirm_button is not None
|
||||
confirm_button.click()
|
||||
|
||||
WebDriverWait(client_page, 10).until(
|
||||
EC.text_to_be_present_in_element((By.XPATH, f"//tr[@id='{row_id}']/td[3]"), "New Description")
|
||||
)
|
||||
|
||||
|
||||
def test_delete_from_update_client(self, client_page: WebDriver) -> None:
|
||||
"""Test updating a client."""
|
||||
self.test_create_client(client_page)
|
||||
|
||||
client_page.refresh()
|
||||
client_field = client_page.find_element(By.XPATH, "//tr/td[contains(text(), 'testuser')]")
|
||||
assert client_field is not None
|
||||
row = client_field.find_element(By.XPATH, "./..")
|
||||
assert row is not None
|
||||
assert row.tag_name == "tr"
|
||||
row_id = row.get_attribute("id")
|
||||
assert row_id is not None
|
||||
print(row_id)
|
||||
client_id = row_id[7:]
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, row_id)
|
||||
update_button = client_page.find_element(By.ID, f"updateClientButton-{client_id}")
|
||||
assert update_button is not None
|
||||
|
||||
update_button.click()
|
||||
|
||||
drawer_name = f"drawer-update-{row_id}"
|
||||
|
||||
wait_helpers.wait_for_element_to_be_visisble(client_page, drawer_name)
|
||||
|
||||
drawer = client_page.find_element(By.ID, drawer_name)
|
||||
description_input = drawer.find_element(By.NAME, "description")
|
||||
|
||||
description_input.clear()
|
||||
|
||||
description_input.send_keys("New Description")
|
||||
|
||||
delete_button = drawer.find_element(By.ID, f"delete-button-{client_id}")
|
||||
assert delete_button is not None
|
||||
delete_button.click()
|
||||
|
||||
alert = WebDriverWait(client_page, 10).until(lambda d: d.switch_to.alert)
|
||||
assert alert.text == "Are you sure?"
|
||||
alert.accept()
|
||||
|
||||
WebDriverWait(client_page, 10).until(
|
||||
EC.none_of(
|
||||
EC.presence_of_element_located((By.ID, row_id))
|
||||
)
|
||||
)
|
||||
79
tests/frontend/test_dashboard.py
Normal file
79
tests/frontend/test_dashboard.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Test of the dashboard landing page."""
|
||||
|
||||
import time
|
||||
import allure
|
||||
import pytest
|
||||
from selenium.webdriver import ActionChains
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support.select import Select
|
||||
|
||||
from tests.integration.types import AdminServer
|
||||
|
||||
from .helpers.auth import login
|
||||
from .helpers.db import DatabasePreloader
|
||||
from .helpers.wait_helpers import (
|
||||
wait_for_element_to_be_visisble,
|
||||
)
|
||||
|
||||
|
||||
class TestDashboardPage:
|
||||
"""Test dashboard page."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def login(self, ui_admin_server: AdminServer, driver: WebDriver) -> None:
|
||||
"""Login."""
|
||||
login(ui_admin_server, driver)
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def create_testdata(self, ui_admin_server: AdminServer) -> None:
|
||||
"""Preload some test data."""
|
||||
test_clients = ["client1", "client2", "client3"]
|
||||
admin_url, (username, password) = ui_admin_server
|
||||
db = DatabasePreloader(admin_url, username, password)
|
||||
db.create_client(*test_clients)
|
||||
secrets = [
|
||||
("secret1", ["client1", "client2"]),
|
||||
("secret2", ["client1"]),
|
||||
("secret3", ["client3"]),
|
||||
("secret4", ["client2"]),
|
||||
]
|
||||
db.create_secret(*secrets)
|
||||
|
||||
@allure.title("Test Dashboard view")
|
||||
def test_dashboard_elements(self, driver: WebDriver) -> None:
|
||||
"""Test elements on the dashboard."""
|
||||
wait_for_element_to_be_visisble(driver, "dashboard-stats-panel")
|
||||
stats_clients = driver.find_element(By.ID, "stats-client-count")
|
||||
assert stats_clients.text == "3"
|
||||
stats_secrets = driver.find_element(By.ID, "stats-secret-count")
|
||||
assert stats_secrets.text == "4"
|
||||
stats_audit = driver.find_element(By.ID, "stats-audit-count")
|
||||
assert stats_audit.text.isdecimal()
|
||||
assert int(stats_audit.text) > 0
|
||||
|
||||
# Check that there is at least one row in each audit table
|
||||
login_table = driver.find_element(By.ID, "last-login-events")
|
||||
login_table_rows = login_table.find_elements(By.XPATH, ".//tr")
|
||||
assert len(login_table_rows) > 1
|
||||
|
||||
audit_table = driver.find_element(By.ID, "last-audit-events")
|
||||
audit_table_rows = audit_table.find_elements(By.XPATH, ".//tr")
|
||||
assert len(audit_table_rows) > 1
|
||||
|
||||
# Find a questionmark hover
|
||||
login_info_btn = login_table_rows[-1].find_element(By.XPATH, "./td[1]//button")
|
||||
login_info_target = login_info_btn.get_attribute("data-popover-target")
|
||||
assert login_info_target is not None
|
||||
|
||||
ActionChains(driver).move_to_element(login_info_btn).perform()
|
||||
|
||||
wait_for_element_to_be_visisble(driver, login_info_target)
|
||||
|
||||
audit_info_btn = audit_table_rows[-1].find_element(By.XPATH, "./td[1]//button")
|
||||
|
||||
audit_info_target = audit_info_btn.get_attribute("data-popover-target")
|
||||
assert audit_info_target is not None
|
||||
|
||||
ActionChains(driver).move_to_element(audit_info_btn).perform()
|
||||
wait_for_element_to_be_visisble(driver, audit_info_target)
|
||||
23
tests/frontend/test_login.py
Normal file
23
tests/frontend/test_login.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""Tests for the login."""
|
||||
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from tests.integration.types import AdminServer
|
||||
|
||||
from .helpers.auth import login
|
||||
from .helpers import wait_helpers
|
||||
|
||||
def test_login(ui_admin_server: AdminServer, driver: WebDriver) -> None:
|
||||
"""Test login."""
|
||||
driver = login(ui_admin_server, driver)
|
||||
print(driver.current_url)
|
||||
assert driver.current_url.endswith("/dashboard")
|
||||
|
||||
|
||||
def test_logout(ui_admin_server: AdminServer, driver: WebDriver) -> None:
|
||||
"""Test logout function."""
|
||||
admin_url = ui_admin_server[0]
|
||||
driver = login(ui_admin_server, driver)
|
||||
assert driver.current_url.endswith("/dashboard")
|
||||
driver.get(admin_url + "/logout")
|
||||
wait_helpers.wait_until_url_contains(driver, "/login")
|
||||
assert driver.current_url.endswith("/login")
|
||||
189
tests/frontend/test_secrets.py
Normal file
189
tests/frontend/test_secrets.py
Normal file
@ -0,0 +1,189 @@
|
||||
"""Tests for the secrets page."""
|
||||
|
||||
import allure
|
||||
import pytest
|
||||
from selenium.webdriver.common.by import By
|
||||
from selenium.webdriver.remote.webdriver import WebDriver
|
||||
from selenium.webdriver.support.select import Select
|
||||
from tests.integration.types import AdminServer
|
||||
|
||||
from .helpers.auth import login
|
||||
from .helpers.db import DatabasePreloader
|
||||
from .helpers.wait_helpers import (
|
||||
wait_for_alert,
|
||||
wait_for_element,
|
||||
wait_for_element_to_be_disabled,
|
||||
wait_for_element_to_be_visisble,
|
||||
wait_for_element_to_disappear,
|
||||
wait_for_element_with_text,
|
||||
)
|
||||
|
||||
|
||||
class TestSecretsPage:
|
||||
"""Test secrets page."""
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def login(self, ui_admin_server: AdminServer, driver: WebDriver) -> None:
|
||||
"""Log in and navigate to secrets page.."""
|
||||
admin_url = ui_admin_server[0]
|
||||
driver = login(ui_admin_server, driver)
|
||||
driver.get(admin_url + "/secrets")
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def create_testdata(self, ui_admin_server: AdminServer) -> None:
|
||||
"""Preload some test data."""
|
||||
test_clients = ["client1", "client2", "client3"]
|
||||
admin_url, (username, password) = ui_admin_server
|
||||
db = DatabasePreloader(admin_url, username, password)
|
||||
db.create_client(*test_clients)
|
||||
|
||||
@allure.title("Test secret creation")
|
||||
@allure.description("Verify that secrets can be created")
|
||||
def test_create_secret(self, driver: WebDriver) -> None:
|
||||
"""Test creation of secrets."""
|
||||
driver.refresh()
|
||||
create_secret_button = driver.find_element(By.ID, "createSecretButton")
|
||||
assert create_secret_button is not None
|
||||
create_secret_button.click()
|
||||
wait_for_element_to_be_visisble(driver, "drawer-create-secret-default")
|
||||
|
||||
drawer = driver.find_element(By.ID, "drawer-create-secret-default")
|
||||
assert drawer is not None
|
||||
name_input = drawer.find_element(By.NAME, "name")
|
||||
value_input = drawer.find_element(By.NAME, "value")
|
||||
client_select = drawer.find_element(By.NAME, "clients")
|
||||
client_select_node = Select(client_select)
|
||||
|
||||
name_input.send_keys("testsecret")
|
||||
value_input.send_keys("secret")
|
||||
client_select_node.select_by_visible_text("client1")
|
||||
|
||||
add_secret_button = drawer.find_element(By.XPATH, "//button[@type='submit']")
|
||||
add_secret_button.click()
|
||||
|
||||
client_appeared = wait_for_element_with_text(driver, "td", "testsecret")
|
||||
|
||||
assert client_appeared is not False
|
||||
|
||||
secret_name_field = driver.find_element(
|
||||
By.XPATH, "//tr/td[contains(text(), 'testsecret')]"
|
||||
)
|
||||
secret_row = secret_name_field.find_element(By.XPATH, "./..")
|
||||
client_field = secret_row.find_element(By.CLASS_NAME, "secret-client-list")
|
||||
secret_client = client_field.find_element(
|
||||
By.XPATH, "//span[contains(text(), 'client1')]"
|
||||
)
|
||||
assert secret_client is not None
|
||||
|
||||
@allure.title("Test auto-generating secrets")
|
||||
@allure.description("Test creation of a secret with automatic value")
|
||||
def test_auto_secret_creation(self, driver: WebDriver) -> None:
|
||||
"""Test creation of secret with automatic value."""
|
||||
driver.refresh()
|
||||
create_secret_button = driver.find_element(By.ID, "createSecretButton")
|
||||
assert create_secret_button is not None
|
||||
create_secret_button.click()
|
||||
wait_for_element_to_be_visisble(driver, "drawer-create-secret-default")
|
||||
|
||||
drawer = driver.find_element(By.ID, "drawer-create-secret-default")
|
||||
assert drawer is not None
|
||||
name_input = drawer.find_element(By.NAME, "name")
|
||||
value_input = drawer.find_element(By.NAME, "value")
|
||||
|
||||
# The auto generate checkbox is obscured by a dynamic div.
|
||||
# We find the label and its nested div
|
||||
|
||||
auto_generate_label = drawer.find_element(By.ID, "autoGenerateCheckboxLabel")
|
||||
# find the first div
|
||||
checkbox_div = auto_generate_label.find_element(By.TAG_NAME, "div")
|
||||
checkbox_div.click()
|
||||
wait_for_element_to_be_disabled(driver, By.NAME, "value")
|
||||
client_select = drawer.find_element(By.NAME, "clients")
|
||||
client_select_node = Select(client_select)
|
||||
|
||||
name_input.send_keys("autosecret")
|
||||
client_select_node.deselect_all()
|
||||
client_select_node.select_by_visible_text("client1")
|
||||
client_select_node.select_by_visible_text("client2")
|
||||
|
||||
add_secret_button = drawer.find_element(By.XPATH, "//button[@type='submit']")
|
||||
add_secret_button.click()
|
||||
|
||||
client_appeared = wait_for_element_with_text(driver, "td", "autosecret")
|
||||
|
||||
secret_name_field = driver.find_element(
|
||||
By.XPATH, "//tr/td[contains(text(), 'autosecret')]"
|
||||
)
|
||||
secret_row = secret_name_field.find_element(By.XPATH, "./..")
|
||||
client_field = secret_row.find_element(By.CLASS_NAME, "secret-client-list")
|
||||
secret_client1 = client_field.find_element(
|
||||
By.XPATH, "//span[contains(text(), 'client1')]"
|
||||
)
|
||||
assert secret_client1 is not None
|
||||
|
||||
secret_client2 = client_field.find_element(
|
||||
By.XPATH, "//span[contains(text(), 'client2')]"
|
||||
)
|
||||
assert secret_client2 is not None
|
||||
|
||||
@allure.title("Test manage client access")
|
||||
def test_manage_client_access(self, driver: WebDriver) -> None:
|
||||
"""Test the manage client access button."""
|
||||
# Use the previous step to create a secret assigned to two clients.
|
||||
self.test_auto_secret_creation(driver)
|
||||
driver.refresh()
|
||||
# Find the manage client access button
|
||||
# btn_id = "client-secret-modal-autosecret"
|
||||
manage_btn = driver.find_element(By.ID, "manage-client-access-btn-autosecret")
|
||||
assert manage_btn is not None
|
||||
manage_btn.click()
|
||||
wait_for_element_to_be_visisble(driver, "client-secret-modal-autosecret")
|
||||
modal = driver.find_element(By.ID, "client-secret-modal-autosecret")
|
||||
assert modal is not None
|
||||
client_pills = modal.find_elements(By.CLASS_NAME, "pill-client-secret")
|
||||
assert len(client_pills) == 2
|
||||
|
||||
# Remove client1
|
||||
remove_pill_btn_id = "btn-remove-client-client1-secret-autosecret"
|
||||
remove_btn = modal.find_element(By.ID, remove_pill_btn_id)
|
||||
assert remove_btn is not None
|
||||
remove_btn.click()
|
||||
alert = wait_for_alert(driver)
|
||||
alert.accept()
|
||||
|
||||
# Wait for the client pill to disappear.
|
||||
wait_for_element_to_disappear(
|
||||
driver,
|
||||
By.XPATH,
|
||||
"//td[@id='secret-client-list-autosecret']/span[contains(text(), 'client1')]",
|
||||
)
|
||||
|
||||
# Add a different client.
|
||||
client_select_field = modal.find_element(By.NAME, "client")
|
||||
assert client_select_field is not None
|
||||
assert client_select_field.tag_name == "select"
|
||||
|
||||
client_select = Select(client_select_field)
|
||||
client_select.select_by_visible_text("client3")
|
||||
|
||||
give_access = modal.find_element(By.XPATH, "//button[@type='submit']")
|
||||
assert "give access" in give_access.text.lower()
|
||||
give_access.click()
|
||||
wait_for_element(driver, By.ID, "client-secret-autosecret-pill-client3")
|
||||
|
||||
@allure.title("Test secret deletion")
|
||||
def test_delete_secret(self, driver: WebDriver) -> None:
|
||||
"""Test deleting a secret."""
|
||||
self.test_auto_secret_creation(driver)
|
||||
driver.refresh()
|
||||
delete_btn_id = "delete-secret-btn-autosecret"
|
||||
delete_btn = driver.find_element(By.ID, delete_btn_id)
|
||||
|
||||
delete_btn.click()
|
||||
|
||||
alert = wait_for_alert(driver)
|
||||
alert.accept()
|
||||
|
||||
wait_for_element_to_disappear(
|
||||
driver, By.XPATH, "//td[contains(text(), 'autosecret')]"
|
||||
)
|
||||
@ -34,8 +34,8 @@ from sshecret_sshd.settings import ServerSettings
|
||||
from sshecret_sshd.ssh_server import start_sshecret_sshd
|
||||
|
||||
from .clients import ClientData
|
||||
from .helpers import create_sshd_server_key, create_test_admin_user, in_tempdir
|
||||
from .types import PortFactory, TestPorts
|
||||
from tests.helpers import create_sshd_server_key, create_test_admin_user, in_tempdir
|
||||
from tests.types import PortFactory, TestPorts
|
||||
|
||||
TEST_SCOPE = "function"
|
||||
LOOP_SCOPE = "function"
|
||||
@ -92,7 +92,7 @@ async def run_admin_server(test_ports: TestPorts, backend_server: tuple[str, str
|
||||
"sshecret_backend_url": backend_url,
|
||||
"backend_token": backend_token,
|
||||
"secret_key": secret_key,
|
||||
"listen_address": "127.0.0.1",
|
||||
"listen_address": "0.0.0.0",
|
||||
"port": port,
|
||||
"database": str(admin_db.absolute()),
|
||||
"password_manager_directory": str(admin_work_path.absolute()),
|
||||
|
||||
@ -2,10 +2,13 @@
|
||||
|
||||
from collections.abc import AsyncIterator
|
||||
from contextlib import asynccontextmanager
|
||||
import allure
|
||||
import pytest
|
||||
|
||||
import httpx
|
||||
|
||||
from allure_commons.types import Severity
|
||||
|
||||
from sshecret.backend import Client
|
||||
|
||||
from sshecret.crypto import generate_private_key, generate_public_key_string
|
||||
@ -70,9 +73,12 @@ class BaseAdminTests:
|
||||
return client
|
||||
|
||||
|
||||
@allure.title("Admin API")
|
||||
class TestAdminAPI(BaseAdminTests):
|
||||
"""Tests of the Admin REST API."""
|
||||
|
||||
@allure.title("Test health test endpoint")
|
||||
@allure.severity(Severity.TRIVIAL)
|
||||
@pytest.mark.asyncio
|
||||
async def test_health_check(
|
||||
self, admin_server: tuple[str, tuple[str, str]]
|
||||
@ -82,6 +88,8 @@ class TestAdminAPI(BaseAdminTests):
|
||||
resp = await client.get("/health")
|
||||
assert resp.status_code == 200
|
||||
|
||||
@allure.title("Test login over API")
|
||||
@allure.severity(Severity.BLOCKER)
|
||||
@pytest.mark.asyncio
|
||||
async def test_admin_login(self, admin_server: AdminServer) -> None:
|
||||
"""Test admin login."""
|
||||
@ -95,9 +103,12 @@ class TestAdminAPI(BaseAdminTests):
|
||||
assert resp.status_code == 200
|
||||
|
||||
|
||||
@allure.title("Admin API Client API")
|
||||
class TestAdminApiClients(BaseAdminTests):
|
||||
"""Test client routes."""
|
||||
|
||||
@allure.title("Test creating a client")
|
||||
@allure.description("Ensure we can create a new client.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_client(self, admin_server: AdminServer) -> None:
|
||||
"""Test create_client."""
|
||||
@ -106,6 +117,8 @@ class TestAdminApiClients(BaseAdminTests):
|
||||
assert client.id is not None
|
||||
assert client.name == "testclient"
|
||||
|
||||
@allure.title("Test reading clients")
|
||||
@allure.description("Ensure we can retrieve a list of current clients.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_clients(self, admin_server: AdminServer) -> None:
|
||||
"""Test get_clients."""
|
||||
@ -124,6 +137,8 @@ class TestAdminApiClients(BaseAdminTests):
|
||||
client_name = entry.get("name")
|
||||
assert client_name in client_names
|
||||
|
||||
@allure.title("Test client deletion")
|
||||
@allure.description("Ensure we can delete a client.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_client(self, admin_server: AdminServer) -> None:
|
||||
"""Test delete_client."""
|
||||
@ -146,9 +161,12 @@ class TestAdminApiClients(BaseAdminTests):
|
||||
assert len(data) == 0
|
||||
|
||||
|
||||
@allure.title("Test secret management")
|
||||
class TestAdminApiSecrets(BaseAdminTests):
|
||||
"""Test secret management."""
|
||||
|
||||
@allure.title("Test adding a secret")
|
||||
@allure.description("Ensure that we can add a secret to a client.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_secret(self, admin_server: AdminServer) -> None:
|
||||
"""Test add_secret."""
|
||||
@ -162,6 +180,8 @@ class TestAdminApiSecrets(BaseAdminTests):
|
||||
resp = await http_client.post("api/v1/secrets/", json=data)
|
||||
assert resp.status_code == 200
|
||||
|
||||
@allure.title("Test read a secret")
|
||||
@allure.description("Ensure that we can retrieve a secret we have stored.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_secret(self, admin_server: AdminServer) -> None:
|
||||
"""Test get_secret."""
|
||||
@ -175,6 +195,8 @@ class TestAdminApiSecrets(BaseAdminTests):
|
||||
assert data["secret"] == "secretstring"
|
||||
assert "testclient" in data["clients"]
|
||||
|
||||
@allure.title("Test adding a secret with automatic value")
|
||||
@allure.description("Test that we can add a secret where we let the system come up with the value of a given length.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_add_secret_auto(self, admin_server: AdminServer) -> None:
|
||||
"""Test adding a secret with an auto-generated value."""
|
||||
@ -195,6 +217,8 @@ class TestAdminApiSecrets(BaseAdminTests):
|
||||
assert len(data["secret"]) == 17
|
||||
assert "testclient" in data["clients"]
|
||||
|
||||
@allure.title("Test updating a secret")
|
||||
@allure.description("Test that we can update the value of a stored secret.")
|
||||
@pytest.mark.asyncio
|
||||
async def test_update_secret(self, admin_server: AdminServer) -> None:
|
||||
"""Test updating secrets."""
|
||||
|
||||
@ -2,25 +2,13 @@
|
||||
import asyncssh
|
||||
|
||||
from typing import Any, AsyncContextManager, Protocol
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable, Awaitable
|
||||
|
||||
from .clients import ClientData
|
||||
|
||||
|
||||
PortFactory = Callable[[], int]
|
||||
|
||||
AdminServer = tuple[str, tuple[str, str]]
|
||||
|
||||
@dataclass
|
||||
class TestPorts:
|
||||
"""Test port dataclass."""
|
||||
|
||||
backend: int
|
||||
admin: int
|
||||
sshd: int
|
||||
|
||||
|
||||
CommandRunner = Callable[[ClientData, str], Awaitable[asyncssh.SSHCompletedProcess]]
|
||||
|
||||
class ProcessRunner(Protocol):
|
||||
|
||||
16
tests/types.py
Normal file
16
tests/types.py
Normal file
@ -0,0 +1,16 @@
|
||||
"""Typings."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from collections.abc import Callable
|
||||
|
||||
PortFactory = Callable[[], int]
|
||||
|
||||
|
||||
@dataclass
|
||||
class TestPorts:
|
||||
"""Test port dataclass."""
|
||||
|
||||
backend: int
|
||||
admin: int
|
||||
sshd: int
|
||||
|
||||
Reference in New Issue
Block a user