Update tests
This commit is contained in:
@ -7,6 +7,7 @@ source =
|
|||||||
packages/sshecret-sshd/src/sshecret_sshd
|
packages/sshecret-sshd/src/sshecret_sshd
|
||||||
|
|
||||||
omit =
|
omit =
|
||||||
|
packages/sshecret-backend/src/sshecret_backend/frontend/*
|
||||||
*/__init__.py
|
*/__init__.py
|
||||||
*/types.py
|
*/types.py
|
||||||
*/testing.py
|
*/testing.py
|
||||||
@ -17,7 +18,7 @@ omit =
|
|||||||
*/test_*.py
|
*/test_*.py
|
||||||
*/conftest.py
|
*/conftest.py
|
||||||
*/site-packages/*
|
*/site-packages/*
|
||||||
concurrency = multiprocessing
|
concurrency = thread
|
||||||
|
|
||||||
[report]
|
[report]
|
||||||
show_missing = True
|
show_missing = True
|
||||||
|
|||||||
@ -7,7 +7,7 @@ all = [ {ref="fmt"}, {ref="lint"}, {ref="check"}, {ref="test"} ]
|
|||||||
"ci:fmt" = "ruff format --check ${PWD}" # fail if not formatted
|
"ci:fmt" = "ruff format --check ${PWD}" # fail if not formatted
|
||||||
"ci:lint" = "ruff check ${PWD}"
|
"ci:lint" = "ruff check ${PWD}"
|
||||||
[tool.poe.tasks.coverage]
|
[tool.poe.tasks.coverage]
|
||||||
cmd = "pytest --cov-config=${PWD}/.coveragerc --cov --cov-report=html --cov-report=term-missing"
|
cmd = "pytest --ignore tests/integration/frontend --cov-config=${PWD}/.coveragerc --cov --cov-report=html --cov-report=term-missing --alluredir allure-results"
|
||||||
cwd = "${POE_PWD}"
|
cwd = "${POE_PWD}"
|
||||||
|
|
||||||
|
|
||||||
@ -32,6 +32,7 @@ dependencies = [
|
|||||||
"pykeepass>=4.1.1.post1",
|
"pykeepass>=4.1.1.post1",
|
||||||
"pytest-asyncio>=0.26.0",
|
"pytest-asyncio>=0.26.0",
|
||||||
"pytest-cov>=6.1.1",
|
"pytest-cov>=6.1.1",
|
||||||
|
"pytest-selenium>=4.1.0",
|
||||||
"python-dotenv>=1.0.1",
|
"python-dotenv>=1.0.1",
|
||||||
"python-json-logger>=3.3.0",
|
"python-json-logger>=3.3.0",
|
||||||
]
|
]
|
||||||
@ -65,9 +66,13 @@ dev = [
|
|||||||
"python-dotenv>=1.0.1",
|
"python-dotenv>=1.0.1",
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
|
"allure-pytest>=2.14.2",
|
||||||
"coverage>=7.8.0",
|
"coverage>=7.8.0",
|
||||||
"pytest>=8.3.5",
|
"pytest>=8.3.5",
|
||||||
"pytest-asyncio>=0.26.0",
|
"pytest-asyncio>=0.26.0",
|
||||||
"pytest-cov>=6.1.1",
|
"pytest-cov>=6.1.1",
|
||||||
|
"pytest-selenium>=4.1.0",
|
||||||
|
"requests>=2.32.3",
|
||||||
"robotframework>=7.2.2",
|
"robotframework>=7.2.2",
|
||||||
|
"selenium>=4.32.0",
|
||||||
]
|
]
|
||||||
|
|||||||
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 sshecret_sshd.ssh_server import start_sshecret_sshd
|
||||||
|
|
||||||
from .clients import ClientData
|
from .clients import ClientData
|
||||||
from .helpers import create_sshd_server_key, create_test_admin_user, in_tempdir
|
from tests.helpers import create_sshd_server_key, create_test_admin_user, in_tempdir
|
||||||
from .types import PortFactory, TestPorts
|
from tests.types import PortFactory, TestPorts
|
||||||
|
|
||||||
TEST_SCOPE = "function"
|
TEST_SCOPE = "function"
|
||||||
LOOP_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,
|
"sshecret_backend_url": backend_url,
|
||||||
"backend_token": backend_token,
|
"backend_token": backend_token,
|
||||||
"secret_key": secret_key,
|
"secret_key": secret_key,
|
||||||
"listen_address": "127.0.0.1",
|
"listen_address": "0.0.0.0",
|
||||||
"port": port,
|
"port": port,
|
||||||
"database": str(admin_db.absolute()),
|
"database": str(admin_db.absolute()),
|
||||||
"password_manager_directory": str(admin_work_path.absolute()),
|
"password_manager_directory": str(admin_work_path.absolute()),
|
||||||
|
|||||||
@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
|
import allure
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
|
from allure_commons.types import Severity
|
||||||
|
|
||||||
from sshecret.backend import Client
|
from sshecret.backend import Client
|
||||||
|
|
||||||
from sshecret.crypto import generate_private_key, generate_public_key_string
|
from sshecret.crypto import generate_private_key, generate_public_key_string
|
||||||
@ -70,9 +73,12 @@ class BaseAdminTests:
|
|||||||
return client
|
return client
|
||||||
|
|
||||||
|
|
||||||
|
@allure.title("Admin API")
|
||||||
class TestAdminAPI(BaseAdminTests):
|
class TestAdminAPI(BaseAdminTests):
|
||||||
"""Tests of the Admin REST API."""
|
"""Tests of the Admin REST API."""
|
||||||
|
|
||||||
|
@allure.title("Test health test endpoint")
|
||||||
|
@allure.severity(Severity.TRIVIAL)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_health_check(
|
async def test_health_check(
|
||||||
self, admin_server: tuple[str, tuple[str, str]]
|
self, admin_server: tuple[str, tuple[str, str]]
|
||||||
@ -82,6 +88,8 @@ class TestAdminAPI(BaseAdminTests):
|
|||||||
resp = await client.get("/health")
|
resp = await client.get("/health")
|
||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
@allure.title("Test login over API")
|
||||||
|
@allure.severity(Severity.BLOCKER)
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_admin_login(self, admin_server: AdminServer) -> None:
|
async def test_admin_login(self, admin_server: AdminServer) -> None:
|
||||||
"""Test admin login."""
|
"""Test admin login."""
|
||||||
@ -95,9 +103,12 @@ class TestAdminAPI(BaseAdminTests):
|
|||||||
assert resp.status_code == 200
|
assert resp.status_code == 200
|
||||||
|
|
||||||
|
|
||||||
|
@allure.title("Admin API Client API")
|
||||||
class TestAdminApiClients(BaseAdminTests):
|
class TestAdminApiClients(BaseAdminTests):
|
||||||
"""Test client routes."""
|
"""Test client routes."""
|
||||||
|
|
||||||
|
@allure.title("Test creating a client")
|
||||||
|
@allure.description("Ensure we can create a new client.")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_create_client(self, admin_server: AdminServer) -> None:
|
async def test_create_client(self, admin_server: AdminServer) -> None:
|
||||||
"""Test create_client."""
|
"""Test create_client."""
|
||||||
@ -106,6 +117,8 @@ class TestAdminApiClients(BaseAdminTests):
|
|||||||
assert client.id is not None
|
assert client.id is not None
|
||||||
assert client.name == "testclient"
|
assert client.name == "testclient"
|
||||||
|
|
||||||
|
@allure.title("Test reading clients")
|
||||||
|
@allure.description("Ensure we can retrieve a list of current clients.")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_get_clients(self, admin_server: AdminServer) -> None:
|
async def test_get_clients(self, admin_server: AdminServer) -> None:
|
||||||
"""Test get_clients."""
|
"""Test get_clients."""
|
||||||
@ -124,6 +137,8 @@ class TestAdminApiClients(BaseAdminTests):
|
|||||||
client_name = entry.get("name")
|
client_name = entry.get("name")
|
||||||
assert client_name in client_names
|
assert client_name in client_names
|
||||||
|
|
||||||
|
@allure.title("Test client deletion")
|
||||||
|
@allure.description("Ensure we can delete a client.")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_delete_client(self, admin_server: AdminServer) -> None:
|
async def test_delete_client(self, admin_server: AdminServer) -> None:
|
||||||
"""Test delete_client."""
|
"""Test delete_client."""
|
||||||
@ -146,9 +161,12 @@ class TestAdminApiClients(BaseAdminTests):
|
|||||||
assert len(data) == 0
|
assert len(data) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@allure.title("Test secret management")
|
||||||
class TestAdminApiSecrets(BaseAdminTests):
|
class TestAdminApiSecrets(BaseAdminTests):
|
||||||
"""Test secret management."""
|
"""Test secret management."""
|
||||||
|
|
||||||
|
@allure.title("Test adding a secret")
|
||||||
|
@allure.description("Ensure that we can add a secret to a client.")
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_add_secret(self, admin_server: AdminServer) -> None:
|
async def test_add_secret(self, admin_server: AdminServer) -> None:
|
||||||
"""Test add_secret."""
|
"""Test add_secret."""
|
||||||
@ -162,6 +180,8 @@ class TestAdminApiSecrets(BaseAdminTests):
|
|||||||
resp = await http_client.post("api/v1/secrets/", json=data)
|
resp = await http_client.post("api/v1/secrets/", json=data)
|
||||||
assert resp.status_code == 200
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_get_secret(self, admin_server: AdminServer) -> None:
|
async def test_get_secret(self, admin_server: AdminServer) -> None:
|
||||||
"""Test get_secret."""
|
"""Test get_secret."""
|
||||||
@ -175,6 +195,8 @@ class TestAdminApiSecrets(BaseAdminTests):
|
|||||||
assert data["secret"] == "secretstring"
|
assert data["secret"] == "secretstring"
|
||||||
assert "testclient" in data["clients"]
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_add_secret_auto(self, admin_server: AdminServer) -> None:
|
async def test_add_secret_auto(self, admin_server: AdminServer) -> None:
|
||||||
"""Test adding a secret with an auto-generated value."""
|
"""Test adding a secret with an auto-generated value."""
|
||||||
@ -195,6 +217,8 @@ class TestAdminApiSecrets(BaseAdminTests):
|
|||||||
assert len(data["secret"]) == 17
|
assert len(data["secret"]) == 17
|
||||||
assert "testclient" in data["clients"]
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_update_secret(self, admin_server: AdminServer) -> None:
|
async def test_update_secret(self, admin_server: AdminServer) -> None:
|
||||||
"""Test updating secrets."""
|
"""Test updating secrets."""
|
||||||
|
|||||||
@ -2,25 +2,13 @@
|
|||||||
import asyncssh
|
import asyncssh
|
||||||
|
|
||||||
from typing import Any, AsyncContextManager, Protocol
|
from typing import Any, AsyncContextManager, Protocol
|
||||||
from dataclasses import dataclass
|
|
||||||
from collections.abc import Callable, Awaitable
|
from collections.abc import Callable, Awaitable
|
||||||
|
|
||||||
from .clients import ClientData
|
from .clients import ClientData
|
||||||
|
|
||||||
|
|
||||||
PortFactory = Callable[[], int]
|
|
||||||
|
|
||||||
AdminServer = tuple[str, tuple[str, str]]
|
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]]
|
CommandRunner = Callable[[ClientData, str], Awaitable[asyncssh.SSHCompletedProcess]]
|
||||||
|
|
||||||
class ProcessRunner(Protocol):
|
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
|
||||||
|
|
||||||
273
uv.lock
generated
273
uv.lock
generated
@ -23,6 +23,32 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/41/18/d89a443ed1ab9bcda16264716f809c663866d4ca8de218aa78fd50b38ead/alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53", size = 231911 },
|
{ url = "https://files.pythonhosted.org/packages/41/18/d89a443ed1ab9bcda16264716f809c663866d4ca8de218aa78fd50b38ead/alembic-1.15.2-py3-none-any.whl", hash = "sha256:2e76bd916d547f6900ec4bb5a90aeac1485d2c92536923d0b138c02b126edc53", size = 231911 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allure-pytest"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "allure-python-commons" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/f4/59d3d3ca7cbcdb5efae990072f6b4aafebff524237fa277c14daac8b84f8/allure_pytest-2.14.2.tar.gz", hash = "sha256:d387492178d27805863d95350bdc38b7feca3ed7165841997630fd4073cc9101", size = 17153 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/bb/d6b28ee5cced087a171715945e578bd6f2c0a32e6bbbd3578fe701f7ae4c/allure_pytest-2.14.2-py3-none-any.whl", hash = "sha256:18f3baa9ebd1b6148223cfa898bacfc2794bb9446221adac1be71deeb26ed79a", size = 11673 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "allure-python-commons"
|
||||||
|
version = "2.14.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/79/53fe62ff56fa1f972b9fcbc46d04687786907cecc40b412421c4554d3734/allure_python_commons-2.14.2.tar.gz", hash = "sha256:7acdc4fe3efbe709604895e2393f082b2659d8e5653e77ff6367682e6e4a41bc", size = 15186 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/b7/41b4323f65911b216a616087c37a8fd337d2cd92f7b8fa89bc35cb91cd1d/allure_python_commons-2.14.2-py3-none-any.whl", hash = "sha256:ad50385a4c601ec31c86eed773d8ccfdcc687fecbb6535c9768af3bf03b50a19", size = 16191 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
@ -91,6 +117,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/99/56/db25216aa7f385ec71fdc489af80812171515cddbe68c0e515e98a291390/asyncssh-2.21.0-py3-none-any.whl", hash = "sha256:cf7f3dfa52b2cb4ad31f0d77ff0d0a8fdd850203da84a0e72e62c36fdd4daf4b", size = 374919 },
|
{ url = "https://files.pythonhosted.org/packages/99/56/db25216aa7f385ec71fdc489af80812171515cddbe68c0e515e98a291390/asyncssh-2.21.0-py3-none-any.whl", hash = "sha256:cf7f3dfa52b2cb4ad31f0d77ff0d0a8fdd850203da84a0e72e62c36fdd4daf4b", size = 374919 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "25.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bcrypt"
|
name = "bcrypt"
|
||||||
version = "4.3.0"
|
version = "4.3.0"
|
||||||
@ -172,6 +207,28 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "3.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641 },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.2.0"
|
version = "8.2.0"
|
||||||
@ -574,6 +631,18 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "outcome"
|
||||||
|
version = "1.3.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "25.0"
|
version = "25.0"
|
||||||
@ -755,6 +824,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c3/c0/c33c8792c3e50193ef55adb95c1c3c2786fe281123291c2dbf0eaab95a6f/pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612", size = 13376 },
|
{ url = "https://files.pythonhosted.org/packages/c3/c0/c33c8792c3e50193ef55adb95c1c3c2786fe281123291c2dbf0eaab95a6f/pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612", size = 13376 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pysocks"
|
||||||
|
version = "1.7.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytailwindcss"
|
name = "pytailwindcss"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
@ -791,6 +869,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
|
{ url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-base-url"
|
||||||
|
version = "2.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "requests" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ae/1a/b64ac368de6b993135cb70ca4e5d958a5c268094a3a2a4cac6f0021b6c4f/pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45", size = 6702 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/98/1c/b00940ab9eb8ede7897443b771987f2f4a76f06be02f1b3f01eb7567e24a/pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6", size = 5302 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytest-cov"
|
name = "pytest-cov"
|
||||||
version = "6.1.1"
|
version = "6.1.1"
|
||||||
@ -804,6 +895,62 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
|
{ url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-html"
|
||||||
|
version = "4.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "jinja2" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-metadata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/bb/ab/4862dcb5a8a514bd87747e06b8d55483c0c9e987e1b66972336946e49b49/pytest_html-4.1.1.tar.gz", hash = "sha256:70a01e8ae5800f4a074b56a4cb1025c8f4f9b038bba5fe31e3c98eb996686f07", size = 150773 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/c7/c160021cbecd956cc1a6f79e5fe155f7868b2e5b848f1320dad0b3e3122f/pytest_html-4.1.1-py3-none-any.whl", hash = "sha256:c8152cea03bd4e9bee6d525573b67bbc6622967b72b9628dda0ea3e2a0b5dd71", size = 23491 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-metadata"
|
||||||
|
version = "3.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a6/85/8c969f8bec4e559f8f2b958a15229a35495f5b4ce499f6b865eac54b878d/pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8", size = 9952 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/43/7e7b2ec865caa92f67b8f0e9231a798d102724ca4c0e1f414316be1c1ef2/pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b", size = 11428 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-selenium"
|
||||||
|
version = "4.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-base-url" },
|
||||||
|
{ name = "pytest-html" },
|
||||||
|
{ name = "pytest-variables" },
|
||||||
|
{ name = "requests" },
|
||||||
|
{ name = "selenium" },
|
||||||
|
{ name = "tenacity" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/c275839e461fdde9ccaaf7ec46f67ad08dbd28bfebc5f8480f883d9da690/pytest_selenium-4.1.0.tar.gz", hash = "sha256:b0a4e1f27750cde631c513c87ae4863dcf9e180e5a1d680a66077da8a669156c", size = 41059 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/27/bac432528c2b20a382ef2423731c1bb8c6292ea5328e3522eccfa9bf0687/pytest_selenium-4.1.0-py3-none-any.whl", hash = "sha256:c6f2c18e91596d3ef360d74c450953767a15193879d3971296498151d1843c01", size = 24131 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-variables"
|
||||||
|
version = "3.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/65/52/d756c9704a80119f2e8a84e418290f8bd1b7e1415c3417a1b9c13fcd2a87/pytest_variables-3.1.0.tar.gz", hash = "sha256:4719b07f0f6e5d07829b19284a99d9159543a2e0336311f7bc4ee3b1617f595d", size = 7420 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/fe/30dbeccfeafa242b3c9577db059019022cd96db20942c4a74ef9361c5b3c/pytest_variables-3.1.0-py3-none-any.whl", hash = "sha256:4c864d2b7093f9053a2bed61e4b1d027bb26456924e637fcef2d1455d32732b1", size = 6070 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@ -848,6 +995,21 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
{ url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.32.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "charset-normalizer" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "urllib3" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "14.0.0"
|
version = "14.0.0"
|
||||||
@ -884,6 +1046,23 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4a/9a/7498a0a40a32ba522840da2b8c0ecb4794114ec332992fda09a0733c25a0/robotframework-7.2.2-py3-none-any.whl", hash = "sha256:1cb4ec69d52aae515bf6037cee66a2a2d8dc3256368081c0f4b3d4578d40904e", size = 777676 },
|
{ url = "https://files.pythonhosted.org/packages/4a/9a/7498a0a40a32ba522840da2b8c0ecb4794114ec332992fda09a0733c25a0/robotframework-7.2.2-py3-none-any.whl", hash = "sha256:1cb4ec69d52aae515bf6037cee66a2a2d8dc3256368081c0f4b3d4578d40904e", size = 777676 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "selenium"
|
||||||
|
version = "4.32.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "trio" },
|
||||||
|
{ name = "trio-websocket" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "urllib3", extra = ["socks"] },
|
||||||
|
{ name = "websocket-client" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/54/2d/fafffe946099033ccf22bf89e12eede14c1d3c5936110c5f6f2b9830722c/selenium-4.32.0.tar.gz", hash = "sha256:b9509bef4056f4083772abb1ae19ff57247d617a29255384b26be6956615b206", size = 870997 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/37/d07ed9d13e571b2115d4ed6956d156c66816ceec0b03b2e463e80d09f572/selenium-4.32.0-py3-none-any.whl", hash = "sha256:c4d9613f8a45693d61530c9660560fadb52db7d730237bc788ddedf442391f97", size = 9369668 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shellingham"
|
name = "shellingham"
|
||||||
version = "1.5.4"
|
version = "1.5.4"
|
||||||
@ -902,6 +1081,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "sortedcontainers"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.40"
|
version = "2.0.40"
|
||||||
@ -952,6 +1140,7 @@ dependencies = [
|
|||||||
{ name = "pykeepass" },
|
{ name = "pykeepass" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "pytest-cov" },
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-selenium" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
{ name = "python-json-logger" },
|
{ name = "python-json-logger" },
|
||||||
]
|
]
|
||||||
@ -964,11 +1153,15 @@ dev = [
|
|||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
|
{ name = "allure-pytest" },
|
||||||
{ name = "coverage" },
|
{ name = "coverage" },
|
||||||
{ name = "pytest" },
|
{ name = "pytest" },
|
||||||
{ name = "pytest-asyncio" },
|
{ name = "pytest-asyncio" },
|
||||||
{ name = "pytest-cov" },
|
{ name = "pytest-cov" },
|
||||||
|
{ name = "pytest-selenium" },
|
||||||
|
{ name = "requests" },
|
||||||
{ name = "robotframework" },
|
{ name = "robotframework" },
|
||||||
|
{ name = "selenium" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
@ -984,6 +1177,7 @@ requires-dist = [
|
|||||||
{ name = "pykeepass", specifier = ">=4.1.1.post1" },
|
{ name = "pykeepass", specifier = ">=4.1.1.post1" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.26.0" },
|
{ name = "pytest-asyncio", specifier = ">=0.26.0" },
|
||||||
{ name = "pytest-cov", specifier = ">=6.1.1" },
|
{ name = "pytest-cov", specifier = ">=6.1.1" },
|
||||||
|
{ name = "pytest-selenium", specifier = ">=4.1.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||||
{ name = "python-json-logger", specifier = ">=3.3.0" },
|
{ name = "python-json-logger", specifier = ">=3.3.0" },
|
||||||
]
|
]
|
||||||
@ -996,11 +1190,15 @@ dev = [
|
|||||||
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
{ name = "python-dotenv", specifier = ">=1.0.1" },
|
||||||
]
|
]
|
||||||
test = [
|
test = [
|
||||||
|
{ name = "allure-pytest", specifier = ">=2.14.2" },
|
||||||
{ name = "coverage", specifier = ">=7.8.0" },
|
{ name = "coverage", specifier = ">=7.8.0" },
|
||||||
{ name = "pytest", specifier = ">=8.3.5" },
|
{ name = "pytest", specifier = ">=8.3.5" },
|
||||||
{ name = "pytest-asyncio", specifier = ">=0.26.0" },
|
{ name = "pytest-asyncio", specifier = ">=0.26.0" },
|
||||||
{ name = "pytest-cov", specifier = ">=6.1.1" },
|
{ name = "pytest-cov", specifier = ">=6.1.1" },
|
||||||
|
{ name = "pytest-selenium", specifier = ">=4.1.0" },
|
||||||
|
{ name = "requests", specifier = ">=2.32.3" },
|
||||||
{ name = "robotframework", specifier = ">=7.2.2" },
|
{ name = "robotframework", specifier = ">=7.2.2" },
|
||||||
|
{ name = "selenium", specifier = ">=4.32.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -1110,6 +1308,46 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
|
{ url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tenacity"
|
||||||
|
version = "9.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0a/d4/2b0cd0fe285e14b36db076e78c93766ff1d529d70408bd1d2a5a84f1d929/tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", size = 48036 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/30/643397144bfbfec6f6ef821f36f33e57d35946c44a2352d3c9f0ae847619/tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138", size = 28248 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trio"
|
||||||
|
version = "0.30.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "attrs" },
|
||||||
|
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "outcome" },
|
||||||
|
{ name = "sniffio" },
|
||||||
|
{ name = "sortedcontainers" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "trio-websocket"
|
||||||
|
version = "0.12.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "outcome" },
|
||||||
|
{ name = "trio" },
|
||||||
|
{ name = "wsproto" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.15.3"
|
version = "0.15.3"
|
||||||
@ -1167,6 +1405,20 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "2.4.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
socks = [
|
||||||
|
{ name = "pysocks" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.34.2"
|
version = "0.34.2"
|
||||||
@ -1228,6 +1480,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087 },
|
{ url = "https://files.pythonhosted.org/packages/a8/b4/c57b99518fadf431f3ef47a610839e46e5f8abf9814f969859d1c65c02c7/watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11", size = 291087 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websocket-client"
|
||||||
|
version = "1.8.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826 },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "websockets"
|
name = "websockets"
|
||||||
version = "15.0.1"
|
version = "15.0.1"
|
||||||
@ -1247,3 +1508,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
|
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
|
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wsproto"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425 }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226 },
|
||||||
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user