diff --git a/.github/workflows/integration_test.yaml b/.github/workflows/integration_test.yaml index 7064062f..2c62c178 100644 --- a/.github/workflows/integration_test.yaml +++ b/.github/workflows/integration_test.yaml @@ -12,6 +12,8 @@ jobs: secrets: inherit with: modules: '["test_actions.py", "test_charm.py", "test_s3.py", "test_saml.py", "test_loki.py"]' + channel: 1.34-strict/stable + juju-channel: 3.6/stable trivy-exit-code: 0 allure-report: if: ${{ !cancelled() && github.event_name == 'schedule' }} diff --git a/docs/release-notes/artifacts/migrate_to_jubilant.yaml b/docs/release-notes/artifacts/migrate_to_jubilant.yaml new file mode 100644 index 00000000..b562e904 --- /dev/null +++ b/docs/release-notes/artifacts/migrate_to_jubilant.yaml @@ -0,0 +1,16 @@ +version_schema: 2 + +changes: +- title: Migrated integration tests from pytest-operator to jubilant + author: tonyandrewmeyer + type: minor + description: | + Migrated integration tests to use the jubilant library and + pytest-jubilant plugin instead of pytest-operator. + urls: + pr: + - https://github.com/canonical/indico-operator/pull/3 + related_doc: + related_issue: + visibility: internal + highlight: false diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 5dc71520..f72e5928 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -3,15 +3,30 @@ """Fixtures for Indico charm integration tests.""" -import asyncio +import shlex +import subprocess # nosec B404 from pathlib import Path from secrets import token_hex +from typing import Dict, Union -import pytest_asyncio +import jubilant +import pytest import yaml -from ops import Application from pytest import Config, fixture -from pytest_operator.plugin import OpsTest + + +# Shim: the CI action passes --keep-models (pytest-jubilant 1.x flag) but +# pytest-jubilant 2.x renamed it to --no-juju-teardown. +# Remove this once the action is updated to use --no-juju-teardown. +def pytest_addoption(parser): + """Register the legacy --keep-models flag.""" # noqa: DCO020 + parser.addoption("--keep-models", action="store_true", default=False) + + +def pytest_configure(config): + """Translate --keep-models into --no-juju-teardown.""" # noqa: DCO020 + if config.getoption("--keep-models", default=False): + config.option.no_juju_teardown = True @fixture(scope="module", name="external_url") @@ -22,7 +37,7 @@ def external_url_fixture(): @fixture(scope="module") def saml_email(pytestconfig: Config): - """SAML login email address test argument for SAML integration tests""" + """SAML login email address test argument for SAML integration tests.""" email = pytestconfig.getoption("--saml-email") if not email: raise ValueError("--saml-email argument is required for selected test cases") @@ -31,7 +46,7 @@ def saml_email(pytestconfig: Config): @fixture(scope="module") def saml_password(pytestconfig: Config): - """SAML login password test argument for SAML integration tests""" + """SAML login password test argument for SAML integration tests.""" password = pytestconfig.getoption("--saml-password") if not password: raise ValueError("--saml-password argument is required for selected test cases") @@ -52,13 +67,13 @@ def app_name_fixture(metadata): @fixture(scope="module") def requests_timeout(): - """Provides a global default timeout for HTTP requests""" + """Provides a global default timeout for HTTP requests.""" yield 15 -@pytest_asyncio.fixture(scope="module", name="app") -async def app_fixture( - ops_test: OpsTest, +@pytest.fixture(scope="module", name="app") +def app_fixture( + juju: jubilant.Juju, app_name: str, pytestconfig: Config, ): @@ -66,126 +81,118 @@ async def app_fixture( Builds the charm and deploys it and the relations it depends on. """ - assert ops_test.model - # Deploy relations to speed up overall execution - postgresql_config = { - "plugin_pg_trgm_enable": str(True), - "plugin_unaccent_enable": str(True), + postgresql_config: Dict[str, Union[bool, str]] = { + "plugin_pg_trgm_enable": True, + "plugin_unaccent_enable": True, "profile": "testing", } - await asyncio.gather( - ops_test.model.deploy( - "postgresql-k8s", channel="14/edge", config=postgresql_config, trust=True - ), - ops_test.model.deploy("redis-k8s", "redis-broker", channel="latest/edge"), - ops_test.model.deploy("redis-k8s", "redis-cache", channel="latest/edge"), - ops_test.model.deploy( - "nginx-ingress-integrator", - channel="latest/edge", - revision=133, - trust=True, - series="focal", - ), + juju.deploy("postgresql-k8s", channel="14/edge", config=postgresql_config, trust=True) + juju.deploy("redis-k8s", "redis-broker", channel="latest/edge") + juju.deploy("redis-k8s", "redis-cache", channel="latest/edge") + juju.deploy( + "nginx-ingress-integrator", + channel="latest/edge", + revision=133, + trust=True, + base="ubuntu@20.04", ) + resources = { "indico-image": pytestconfig.getoption("--indico-image"), "indico-nginx-image": pytestconfig.getoption("--indico-nginx-image"), } if charm := pytestconfig.getoption("--charm-file"): - application = await ops_test.model.deploy( - f"./{charm}", - resources=resources, - application_name=app_name, - series="focal", - ) + juju.deploy(f"./{charm}", app_name, resources=resources, base="ubuntu@20.04") else: - charm = await ops_test.build_charm(".") - application = await ops_test.model.deploy( + proc = subprocess.run( # nosec B603 + shlex.split("charmcraft pack -p ./"), + check=True, + capture_output=True, + text=True, + ) + packed = [ + line.split()[1] + for line in proc.stderr.strip().splitlines() + if line.startswith("Packed") + ] + if not packed: + raise RuntimeError(f"No packed charm found in charmcraft output: {proc.stderr}") + charm = Path(packed[0]).resolve() + juju.deploy( charm, + app_name, resources=resources, - application_name=app_name, config={ "external_plugins": "https://github.com/canonical/flask-multipass-saml-groups/releases/download/1.2.2/flask_multipass_saml_groups-1.2.2-py3-none-any.whl" # noqa: E501 pylint: disable=line-too-long }, - series="focal", + base="ubuntu@20.04", ) - await asyncio.gather( - ops_test.model.add_relation(app_name, "postgresql-k8s"), - ops_test.model.add_relation(f"{app_name}:redis-broker", "redis-broker"), - ops_test.model.add_relation(f"{app_name}:redis-cache", "redis-cache"), - ops_test.model.add_relation(app_name, "nginx-ingress-integrator"), - ) - await ops_test.model.wait_for_idle( - apps=[ - application.name, + juju.integrate(app_name, "postgresql-k8s") + juju.integrate(f"{app_name}:redis-broker", "redis-broker") + juju.integrate(f"{app_name}:redis-cache", "redis-cache") + juju.integrate(app_name, "nginx-ingress-integrator") + juju.wait( + lambda status: jubilant.all_active( + status, + app_name, "postgresql-k8s", "redis-broker", "redis-cache", "nginx-ingress-integrator", - ], - status="active", - raise_on_error=True, + ), + error=jubilant.any_error, ) - yield application + yield app_name -@pytest_asyncio.fixture(scope="module", name="saml_integrator") -async def saml_integrator_fixture(ops_test: OpsTest, app: Application): +@pytest.fixture(scope="module", name="saml_integrator") +def saml_integrator_fixture(juju: jubilant.Juju, app: str): """SAML integrator charm used for integration testing.""" - assert ops_test.model saml_config = { "entity_id": "https://login.staging.ubuntu.com", "metadata_url": "https://login.staging.ubuntu.com/saml/metadata", } - saml_integrator = await ops_test.model.deploy( - "saml-integrator", channel="latest/stable", config=saml_config, trust=True - ) - await ops_test.model.add_relation(app.name, saml_integrator.name) - await ops_test.model.wait_for_idle( - apps=[saml_integrator.name, app.name], status="active", raise_on_error=True + juju.deploy("saml-integrator", channel="latest/stable", config=saml_config, trust=True) + juju.integrate(app, "saml-integrator") + juju.wait( + lambda status: jubilant.all_active(status, "saml-integrator", app), + error=jubilant.any_error, ) - yield saml_integrator + yield "saml-integrator" -@pytest_asyncio.fixture(scope="module", name="s3_integrator") -async def s3_integrator_fixture(ops_test: OpsTest, app: Application): - """SAML integrator charm used for integration testing.""" - assert ops_test.model +@pytest.fixture(scope="module", name="s3_integrator") +def s3_integrator_fixture(juju: jubilant.Juju, app: str): + """S3 integrator charm used for integration testing.""" s3_config = { "bucket": "some-bucket", "endpoint": "s3.example.com", } - s3_integrator = await ops_test.model.deploy( - "s3-integrator", channel="latest/edge", config=s3_config - ) - await ops_test.model.wait_for_idle(apps=[s3_integrator.name], idle_period=5, status="blocked") + juju.deploy("s3-integrator", channel="latest/edge", config=s3_config) + juju.wait(lambda status: jubilant.all_blocked(status, "s3-integrator")) params = {"access-key": token_hex(16), "secret-key": token_hex(16)} - # Application actually does have units - action_sync_s3_credentials = await s3_integrator.units[0].run_action( # type: ignore - "sync-s3-credentials", **params + juju.run("s3-integrator/0", "sync-s3-credentials", params=params) + juju.wait( + lambda status: jubilant.all_active(status, "s3-integrator"), + error=jubilant.any_error, ) - await action_sync_s3_credentials.wait() - await ops_test.model.wait_for_idle( - apps=[s3_integrator.name], status="active", raise_on_error=True + juju.integrate(app, "s3-integrator") + juju.wait( + lambda status: jubilant.all_active(status, "s3-integrator", app), + error=jubilant.any_error, ) - await ops_test.model.add_relation(app.name, s3_integrator.name) - await ops_test.model.wait_for_idle( - apps=[s3_integrator.name, app.name], status="active", raise_on_error=True - ) - yield s3_integrator + yield "s3-integrator" -@pytest_asyncio.fixture(scope="module", name="loki") -async def loki_fixture(ops_test: OpsTest, app: Application): +@pytest.fixture(scope="module", name="loki") +def loki_fixture(juju: jubilant.Juju, app: str): """Loki charm used for integration testing.""" - assert ops_test.model - loki = await ops_test.model.deploy( - "loki-k8s", channel="1/edge", trust=True, revision=97, series="focal" - ) - await ops_test.model.add_relation(app.name, loki.name) - await ops_test.model.wait_for_idle( - apps=[loki.name, app.name], status="active", raise_on_error=True + juju.deploy("loki-k8s", channel="1/edge", trust=True, revision=97, base="ubuntu@20.04") + juju.integrate(app, "loki-k8s") + juju.wait( + lambda status: jubilant.all_active(status, "loki-k8s", app), + error=jubilant.any_error, ) - yield loki + yield "loki-k8s" diff --git a/tests/integration/test_actions.py b/tests/integration/test_actions.py index 8f8b4cff..c3414c9b 100644 --- a/tests/integration/test_actions.py +++ b/tests/integration/test_actions.py @@ -6,83 +6,63 @@ from secrets import token_hex -import juju.action +import jubilant import pytest -import pytest_asyncio -from ops import Application ADMIN_USER_EMAIL = "sample@email.com" ADMIN_USER_EMAIL_FAIL = "sample2@email.com" -@pytest_asyncio.fixture(scope="module") -async def add_admin(app: Application): +@pytest.fixture(scope="module") +def add_admin(juju: jubilant.Juju, app: str): """ arrange: given charm in its initial state act: run the add-admin action assert: check the output in the action result """ - assert hasattr(app, "units") - - assert app.units[0] - email = ADMIN_USER_EMAIL email_fail = ADMIN_USER_EMAIL_FAIL # This is a test password password = token_hex(16) - # Application actually does have units - action: juju.action.Action = await app.units[0].run_action( # type: ignore - "add-admin", email=email, password=password - ) - await action.wait() - assert action.status == "completed" - assert action.results["user"] == email - assert f'Admin with email "{email}" correctly created' in action.results["output"] - action2: juju.action.Action = await app.units[0].run_action( # type: ignore - "add-admin", email=email_fail, password=password - ) - await action2.wait() - assert action2.status == "completed" - assert action2.results["user"] == email_fail - assert f'Admin with email "{email_fail}" correctly created' in action2.results["output"] + task = juju.run(f"{app}/0", "add-admin", params={"email": email, "password": password}) + assert task.success + assert task.results["user"] == email + assert f'Admin with email "{email}" correctly created' in task.results["output"] + + task2 = juju.run(f"{app}/0", "add-admin", params={"email": email_fail, "password": password}) + assert task2.success + assert task2.results["user"] == email_fail + assert f'Admin with email "{email_fail}" correctly created' in task2.results["output"] -@pytest.mark.asyncio @pytest.mark.abort_on_fail @pytest.mark.usefixtures("add_admin") -async def test_anonymize_user(app: Application): +def test_anonymize_user(juju: jubilant.Juju, app: str): """ arrange: admin user created act: run the anonymize-user action assert: check the output in the action result """ - # Application actually does have units - action_anonymize: juju.action.Action = await app.units[0].run_action( # type: ignore - "anonymize-user", email=ADMIN_USER_EMAIL - ) - await action_anonymize.wait() - assert action_anonymize.status == "completed" - assert action_anonymize.results["user"] == ADMIN_USER_EMAIL + task = juju.run(f"{app}/0", "anonymize-user", params={"email": ADMIN_USER_EMAIL}) + assert task.success + assert task.results["user"] == ADMIN_USER_EMAIL expected_words = [ADMIN_USER_EMAIL, "correctly anonymized"] - assert all(word in action_anonymize.results["output"] for word in expected_words) + assert all(word in task.results["output"] for word in expected_words) -@pytest.mark.asyncio @pytest.mark.abort_on_fail @pytest.mark.usefixtures("add_admin") -async def test_anonymize_user_fail(app: Application): +def test_anonymize_user_fail(juju: jubilant.Juju, app: str): """ arrange: admin user created act: run the anonymize-user action assert: check the output in the action result """ - # Application actually does have units - action_anonymize: juju.action.Action = await app.units[0].run_action( # type: ignore - "anonymize-user", email=f",{ADMIN_USER_EMAIL_FAIL}" - ) - await action_anonymize.wait() - assert action_anonymize.status == "failed" - assert action_anonymize.results["user"] == f",{ADMIN_USER_EMAIL_FAIL}" + with pytest.raises(jubilant.TaskError) as exc_info: + juju.run(f"{app}/0", "anonymize-user", params={"email": f",{ADMIN_USER_EMAIL_FAIL}"}) + task = exc_info.value.task + assert task.status == "failed" + assert task.results["user"] == f",{ADMIN_USER_EMAIL_FAIL}" expected_words = [ADMIN_USER_EMAIL_FAIL, "correctly anonymized", "Failed to anonymize user"] - assert all(word in action_anonymize.results["output"] for word in expected_words) + assert all(word in task.results["output"] for word in expected_words) diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index 25241225..f1a0f513 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -6,10 +6,9 @@ import logging +import jubilant import pytest import requests -from ops import ActiveStatus, Application -from pytest_operator.plugin import OpsTest from charm import CELERY_PROMEXP_PORT, NGINX_PROMEXP_PORT, STATSD_PROMEXP_PORT @@ -19,87 +18,63 @@ logger = logging.getLogger() -@pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_active(app: Application): +def test_active(juju: jubilant.Juju, app: str): """Check that the charm is active. Assume that the charm has already been built and is running. """ - # Application actually does have units - assert app.units[0].workload_status == ActiveStatus.name # type: ignore + status = juju.status() + assert status.apps[app].app_status.current == "active" -@pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_indico_is_up(ops_test: OpsTest, app: Application): +def test_indico_is_up(juju: jubilant.Juju, app: str): """Check that the bootstrap page is reachable. Assume that the charm has already been built and is running. """ - assert ops_test.model - # Read the IP address of indico - status = await ops_test.model.get_status() - unit = list(status.applications[app.name].units)[0] - address = status["applications"][app.name]["units"][unit]["address"] - # Send request to bootstrap page and set Host header to app_name (which the application - # expects) + status = juju.status() + unit_status = list(status.apps[app].units.values())[0] + address = unit_status.address response = requests.get( - f"http://{address}:8080/bootstrap", headers={"Host": f"{app.name}.local"}, timeout=10 + f"http://{address}:8080/bootstrap", headers={"Host": f"{app}.local"}, timeout=10 ) assert response.status_code == 200 -@pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_health_checks(app: Application): +def test_health_checks(juju: jubilant.Juju, app: str): """Runs health checks for each container. Assume that the charm has already been built and is running. """ - container__checks_list = [("indico-nginx", 2), ("indico", 4)] - # Application actually does have units - indico_unit = app.units[0] # type: ignore - for container_checks in container__checks_list: - container = container_checks[0] - cmd = f"PEBBLE_SOCKET=/charm/containers/{container}/pebble.socket /charm/bin/pebble checks" - action = await indico_unit.run(cmd, timeout=10) - # Change this if upgrading Juju lib version to >= 3 - # See https://github.com/juju/python-libjuju/issues/707#issuecomment-1212296289 - result = action.data - code = result["results"].get("Code") - stdout = result["results"].get("Stdout") - stderr = result["results"].get("Stderr") - assert code == "0", f"{cmd} failed ({code}): {stderr or stdout}" + container_checks_list = [("indico-nginx", 2), ("indico", 4)] + for container, expected_count in container_checks_list: + cmd = ( + f"PEBBLE_SOCKET=/charm/containers/{container}/pebble.socket" + " /charm/bin/pebble checks" + ) + stdout = juju.cli("exec", "--unit", f"{app}/0", "--", "bash", "-c", cmd) # When executing the checks, `0/3` means there are 0 errors of 3. - # Each check has it's own `0/3`, so we will count `n` times, - # where `n` is the number of checks for that container. - assert stdout.count("0/3") == container_checks[1] + # Each check has its own `0/3`, so we count `n` times where `n` is the + # number of checks for that container. + assert stdout.count("0/3") == expected_count -@pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_prom_exporters_are_up(app: Application): +def test_prom_exporters_are_up(juju: jubilant.Juju, app: str): """ arrange: given charm in its initial state act: when the metrics endpoints are scraped assert: the response is 200 (HTTP OK) """ - # Application actually does have units - indico_unit = app.units[0] # type: ignore prometheus_targets = [ f"localhost:{NGINX_PROMEXP_PORT}", f"localhost:{STATSD_PROMEXP_PORT}", f"localhost:{CELERY_PROMEXP_PORT}", ] - # Send request to /metrics for each target and check the response for target in prometheus_targets: - cmd = f"curl -m 10 http://{target}/metrics" - action = await indico_unit.run(cmd, timeout=15) - # Change this if upgrading Juju lib version to >= 3 - # See https://github.com/juju/python-libjuju/issues/707#issuecomment-1212296289 - result = action.data - code = result["results"].get("Code") - stdout = result["results"].get("Stdout") - stderr = result["results"].get("Stderr") - assert code == "0", f"{cmd} failed ({code}): {stderr or stdout}" + cmd = f"curl -f -m 10 http://{target}/metrics" + # CLIError is raised if the command fails, so a successful return means status 200. + juju.cli("exec", "--unit", f"{app}/0", "--", "bash", "-c", cmd) diff --git a/tests/integration/test_loki.py b/tests/integration/test_loki.py index b738b25d..bf3c01dd 100644 --- a/tests/integration/test_loki.py +++ b/tests/integration/test_loki.py @@ -4,30 +4,26 @@ """Indico Loki integration tests.""" -import asyncio -import json import logging import time +import jubilant import pytest import requests -from juju.application import Application -from pytest_operator.plugin import OpsTest logger = logging.getLogger(__name__) -@pytest.mark.asyncio @pytest.mark.abort_on_fail -async def test_loki(loki: Application, ops_test: OpsTest): +def test_loki(juju: jubilant.Juju, loki: str): """ arrange: given charm integrated with Loki. act: do nothing. assert: Loki starts to receive log files from indico. """ - _, status, _ = await ops_test.juju("status", "--format", "json") - status = json.loads(status) - loki_ip = list(status["applications"][loki.name]["units"].values())[0]["address"] + status = juju.status() + loki_unit = list(status.apps[loki].units.values())[0] + loki_ip = loki_unit.address logger.info("loki IP: %s", loki_ip) deadline = time.time() + 1200 logged_files = [] @@ -42,7 +38,7 @@ async def test_loki(loki: Application, ops_test: OpsTest): .get("data", []) ) except (requests.exceptions.RequestException, TimeoutError): - await asyncio.sleep(1) + time.sleep(1) if all( file in logged_files for file in ["/srv/indico/log/celery.log", "/srv/indico/log/indico.log"] diff --git a/tests/integration/test_s3.py b/tests/integration/test_s3.py index b9097049..20fec475 100644 --- a/tests/integration/test_s3.py +++ b/tests/integration/test_s3.py @@ -6,41 +6,30 @@ import re -import juju +import jubilant import pytest import yaml -from ops import Application -from pytest_operator.plugin import OpsTest -@pytest.mark.asyncio @pytest.mark.abort_on_fail @pytest.mark.usefixtures("s3_integrator") -async def test_s3(app: Application, s3_integrator: Application, ops_test: OpsTest): +def test_s3(juju: jubilant.Juju, app: str, s3_integrator: str): """ arrange: given charm integrated with S3. act: do nothing. assert: the pebble plan matches the S3 values as configured by the integrator. """ - # Application actually does have units - return_code, stdout, _ = await ops_test.juju( - "ssh", "--container", app.name, app.units[0].name, "pebble", "plan" # type: ignore - ) - assert return_code == 0 + stdout = juju.cli("ssh", "--container", app, f"{app}/0", "pebble", "plan") plan = yaml.safe_load(stdout) indico_env = plan["services"]["indico"]["environment"] # STORAGE_DICT is a string representation of a Python dict # pylint: disable=eval-used storage_config = eval(indico_env["STORAGE_DICT"]) # nosec - # Application actually does have units - action: juju.action.Action = await s3_integrator.units[0].run_action( # type: ignore - "get-s3-connection-info" - ) - await action.wait() - assert action.status == "completed" + task = juju.run(f"{s3_integrator}/0", "get-s3-connection-info") + assert task.success # in get-s3-connection-info, access_key and secret_key are redacted assert re.match( - f"s3:bucket={action.results['bucket']}," - f"access_key=[^=]+,secret_key=[^=]+,proxy=true,host={action.results['endpoint']}", + f"s3:bucket={task.results['bucket']}," + f"access_key=[^=]+,secret_key=[^=]+,proxy=true,host={task.results['endpoint']}", storage_config["s3"], ) diff --git a/tests/integration/test_saml.py b/tests/integration/test_saml.py index 6ebafb92..dfe535e4 100644 --- a/tests/integration/test_saml.py +++ b/tests/integration/test_saml.py @@ -9,19 +9,17 @@ from unittest.mock import patch from urllib.parse import urlparse +import jubilant import pytest import requests import urllib3.exceptions -from ops import Application -from pytest_operator.plugin import OpsTest -@pytest.mark.asyncio @pytest.mark.abort_on_fail @pytest.mark.usefixtures("saml_integrator") -async def test_saml_auth( # pylint: disable=too-many-arguments, too-many-positional-arguments - ops_test: OpsTest, - app: Application, +def test_saml_auth( # pylint: disable=too-many-arguments, too-many-positional-arguments + juju: jubilant.Juju, + app: str, saml_email: str, saml_password: str, requests_timeout: float, @@ -32,13 +30,8 @@ async def test_saml_auth( # pylint: disable=too-many-arguments, too-many-positi act: configure a SAML target url and fire SAML authentication assert: The SAML authentication process is executed successfully. """ - # The linter does not recognize set_config as a method, so this errors must be ignored. - await app.set_config( # type: ignore[attr-defined] # pylint: disable=W0106 - {"site_url": external_url} - ) - # The linter does not recognize wait_for_idle as a method, - # since ops_test has a model as Optional, so this error must be ignored. - await ops_test.model.wait_for_idle(status="active", idle_period=15) # type: ignore[union-attr] + juju.config(app, {"site_url": external_url}) + juju.wait(jubilant.all_active) urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) host = urlparse(external_url).netloc @@ -111,6 +104,4 @@ def patched_getaddrinfo(*args): ) assert dashboard_page.status_code == 200 # Revert SAML config for zap to be able to run - await app.set_config( # type: ignore[attr-defined] # pylint: disable=W0106 - {"site_url": ""} - ) + juju.config(app, {"site_url": ""}) diff --git a/tox.ini b/tox.ini index 4639e64a..95851781 100644 --- a/tox.ini +++ b/tox.ini @@ -50,9 +50,9 @@ deps = pydocstyle>=2.10 pylint pyproject-flake8 + jubilant pytest - pytest-asyncio - pytest-operator + pytest-jubilant requests types-PyYAML types-requests @@ -96,14 +96,12 @@ deps = pydocstyle>=2.10 pylint pyproject-flake8 + jubilant pytest - pytest-asyncio - pytest-operator + pytest-jubilant requests types-PyYAML types-requests - ; 2024/11/19 - there is an incompatibility issue with latest websockets lib release and pylib juju - websockets<14.0 # https://github.com/juju/python-libjuju/issues/1184 -r{toxinidir}/requirements.txt commands = codespell {[vars]plugins_path} --skip {toxinidir}/.git --skip {toxinidir}/.tox \ @@ -157,11 +155,10 @@ deps = allure-pytest>=2.8.18 cosl git+https://github.com/canonical/data-platform-workflows@v24.0.0\#subdirectory=python/pytest_plugins/allure_pytest_collection_report - juju==2.9.49.0 + jubilant pytest - pytest-asyncio - pytest-operator - websockets<14.0 # https://github.com/juju/python-libjuju/issues/1184 + pytest-jubilant + requests commands = pytest -v --tb native --ignore={[vars]tst_path}unit --log-cli-level=INFO -s {posargs} diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..a5bc5147 --- /dev/null +++ b/uv.lock @@ -0,0 +1,3 @@ +version = 1 +revision = 3 +requires-python = ">=3.14"