diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 58735f43..150250fe 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,11 +31,35 @@ source .tox/unit/bin/activate ### Testing ```shell -tox -e fmt # update your code according to linting rules -tox -e lint # code style tox -e unit # unit tests tox -e integration # integration tests -tox # runs 'lint' and 'unit' environments +``` + +To test this charm manually, execute the container: +```bash +kubectl exec -it hydra-0 -c hydra -n -- sh +``` + +Create an exemplary client: +```shell +# hydra create client --endpoint http://127.0.0.1:4445/ --name example-client +CLIENT ID b55b6857-968e-4fb7-be77-f701ec751405 +CLIENT SECRET b3wFYH2N_epJY6C8jCuinBRP60 +GRANT TYPES authorization_code +RESPONSE TYPES code +SCOPE offline_access offline openid +AUDIENCE +REDIRECT URIS +``` + +List the clients: +```shell +# hydra list clients --endpoint http://127.0.0.1:4445/ +CLIENT ID CLIENT SECRET GRANT TYPES RESPONSE TYPES SCOPE AUDIENCE REDIRECT URIS +b55b6857-968e-4fb7-be77-f701ec751405 authorization_code code offline_access offline openid + +NEXT PAGE TOKEN +IS LAST PAGE true ``` ## Build charm diff --git a/README.md b/README.md index 9728767e..21f72387 100644 --- a/README.md +++ b/README.md @@ -2,73 +2,18 @@ ## Description -This repository hosts the Kubernetes Python Operator for Ory Hydra - a scalable, security first OAuth 2.0 and OpenID Connect server. - -For more details and documentation, visit https://www.ory.sh/docs/hydra/ +Python Operator for Ory Hydra - a scalable, security first OAuth 2.0 and OpenID Connect server. For more details and documentation, visit https://www.ory.sh/docs/hydra/ ## Usage -The Hydra Operator may be deployed using the Juju command line as follows: - -Deploy the `postgresql-k8s` charm: - ```bash juju deploy postgresql-k8s --channel edge --trust -``` - -Clone this repository and pack the Hydra Operator with charmcraft: -```bash -charmcraft pack -``` - -Deploy the charm: - -```bash -juju deploy ./hydra*.charm --resource oci-image=$(yq eval '.resources.oci-image.upstream-source' metadata.yaml) -``` - -Finally, add the required relation: -```bash +juju deploy hydra --trust juju relate postgresql-k8s hydra ``` You can follow the deployment status with `watch -c juju status --color`. -## Testing - -Unit and integration tests can be run with tox: -```bash -tox -e unit -tox -e integration -``` - -To test this charm manually, execute the container: -```bash -kubectl exec -it hydra-0 -c hydra -n -- sh -``` - -Create an exemplary client: -```shell -# hydra create client --endpoint http://127.0.0.1:4445/ --name example-client -CLIENT ID b55b6857-968e-4fb7-be77-f701ec751405 -CLIENT SECRET b3wFYH2N_epJY6C8jCuinBRP60 -GRANT TYPES authorization_code -RESPONSE TYPES code -SCOPE offline_access offline openid -AUDIENCE -REDIRECT URIS -``` - -List the clients: -```shell -# hydra list clients --endpoint http://127.0.0.1:4445/ -CLIENT ID CLIENT SECRET GRANT TYPES RESPONSE TYPES SCOPE AUDIENCE REDIRECT URIS -b55b6857-968e-4fb7-be77-f701ec751405 authorization_code code offline_access offline openid - -NEXT PAGE TOKEN -IS LAST PAGE true -``` - ## Relations ### PostgreSQL @@ -77,16 +22,17 @@ This charm requires a relation with [postgresql-k8s-operator](https://github.com ### Ingress -The Hydra Operator offers integration with the [traefik-k8s-operator](https://github.com/canonical/traefik-k8s-operator) for ingress. -Hydra has two APIs which can be exposed through ingress, the public API and the admin API. +The Hydra Operator offers integration with the [traefik-k8s-operator](https://github.com/canonical/traefik-k8s-operator) for ingress. Hydra has two APIs which can be exposed through ingress, the public API and the admin API. If you have traefik deployed and configured in your hydra model, to provide ingress to the admin API run: -```console + +```bash juju relate traefik-admin hydra:admin-ingress ``` To provide ingress to the public API run: -```console + +```bash juju relate traefik-public hydra:public-ingress ``` diff --git a/charmcraft.yaml b/charmcraft.yaml index d5e58762..7e33b57e 100644 --- a/charmcraft.yaml +++ b/charmcraft.yaml @@ -9,7 +9,3 @@ bases: run-on: - name: "ubuntu" channel: "22.04" -parts: - charm: - build-packages: - - git diff --git a/lib/charms/hydra/v0/hydra_endpoints.py b/lib/charms/hydra/v0/hydra_endpoints.py index e39dce85..833160db 100644 --- a/lib/charms/hydra/v0/hydra_endpoints.py +++ b/lib/charms/hydra/v0/hydra_endpoints.py @@ -39,6 +39,7 @@ def some_event_function(): """ import logging +from typing import Dict, Optional from ops.charm import CharmBase, RelationCreatedEvent from ops.framework import EventBase, EventSource, Object, ObjectEvents @@ -85,19 +86,17 @@ def __init__(self, charm: CharmBase, relation_name: str = RELATION_NAME): events.relation_created, self._on_provider_endpoint_relation_created ) - def _on_provider_endpoint_relation_created(self, event: RelationCreatedEvent): + def _on_provider_endpoint_relation_created(self, event: RelationCreatedEvent) -> None: self.on.ready.emit() - def send_endpoint_relation_data( - self, charm: CharmBase, admin_endpoint: str, public_endpoint: str - ) -> None: + def send_endpoint_relation_data(self, admin_endpoint: str, public_endpoint: str) -> None: """Updates relation with endpoints info.""" if not self._charm.unit.is_leader(): return relations = self.model.relations[RELATION_NAME] for relation in relations: - relation.data[charm].update( + relation.data[self._charm.app].update( { "admin_endpoint": admin_endpoint, "public_endpoint": public_endpoint, @@ -114,7 +113,7 @@ class HydraEndpointsRelationError(Exception): class HydraEndpointsRelationMissingError(HydraEndpointsRelationError): """Raised when the relation is missing.""" - def __init__(self): + def __init__(self) -> None: self.message = "Missing endpoint-info relation with hydra" super().__init__(self.message) @@ -122,7 +121,7 @@ def __init__(self): class HydraEndpointsRelationDataMissingError(HydraEndpointsRelationError): """Raised when information is missing from the relation.""" - def __init__(self, message): + def __init__(self, message: str) -> None: self.message = message super().__init__(self.message) @@ -135,10 +134,10 @@ def __init__(self, charm: CharmBase, relation_name: str = RELATION_NAME): self.charm = charm self.relation_name = relation_name - def get_hydra_endpoints(self) -> dict: + def get_hydra_endpoints(self) -> Optional[Dict]: """Get the hydra endpoints.""" if not self.model.unit.is_leader(): - return + return None endpoints = self.model.relations[self.relation_name] if len(endpoints) == 0: raise HydraEndpointsRelationMissingError() diff --git a/lib/charms/hydra/v0/oauth.py b/lib/charms/hydra/v0/oauth.py index 4c9ec6c9..4f365800 100644 --- a/lib/charms/hydra/v0/oauth.py +++ b/lib/charms/hydra/v0/oauth.py @@ -422,7 +422,7 @@ def __init__( grant_types: List[str], audience: List, token_endpoint_auth_method: str, - relation_id: str, + relation_id: int, ) -> None: super().__init__(handle) self.redirect_uri = redirect_uri diff --git a/pyproject.toml b/pyproject.toml index de8987a1..c1d6aa52 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -38,3 +38,26 @@ docstring-convention = "google" copyright-check = "True" copyright-author = "Canonical Ltd." copyright-regexp = "Copyright\\s\\d{4}([-,]\\d{4})*\\s+%(author)s" + + +[tool.mypy] +pretty = true +mypy_path = "./src:./lib/:./tests" +# Exclude non-hydra libraries +exclude = 'lib/charms/((?!hydra).)' +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_configs = true +show_traceback = true +show_error_codes = true +namespace_packages = true +explicit_package_bases = true +check_untyped_defs = true +allow_redefinition = true +disallow_incomplete_defs = true +disallow_untyped_defs = true + +# Ignore libraries that do not have type hint nor stubs +[[tool.mypy.overrides]] +module = ["ops.*", "pytest.*", "pytest_operator.*", "urllib3.*", "jinja2.*", "lightkube.*", "pytest_mock.*"] +ignore_missing_imports = true diff --git a/src/charm.py b/src/charm.py index b0eb8244..2597d56d 100755 --- a/src/charm.py +++ b/src/charm.py @@ -8,6 +8,7 @@ import logging from os.path import join +from typing import Any from charms.data_platform_libs.v0.data_interfaces import ( DatabaseCreatedEvent, @@ -50,7 +51,7 @@ class HydraCharm(CharmBase): """Charmed Ory Hydra.""" - def __init__(self, *args): + def __init__(self, *args: Any) -> None: super().__init__(*args) self._container_name = "hydra" @@ -156,16 +157,16 @@ def _hydra_service_is_running(self) -> bool: return False return service.is_running() - def _render_conf_file(self) -> None: + def _render_conf_file(self) -> str: """Render the Hydra configuration file.""" with open("templates/hydra.yaml.j2", "r") as file: template = Template(file.read()) rendered = template.render( db_info=self._get_database_relation_info(), - consent_url=join(self.config.get("login_ui_url"), "consent"), - error_url=join(self.config.get("login_ui_url"), "oidc_error"), - login_url=join(self.config.get("login_ui_url"), "login"), + consent_url=join(self.config.get("login_ui_url", ""), "consent"), + error_url=join(self.config.get("login_ui_url", ""), "oidc_error"), + login_url=join(self.config.get("login_ui_url", ""), "login"), hydra_public_url=self.public_ingress.url if self.public_ingress.is_ready() else f"http://127.0.0.1:{HYDRA_PUBLIC_PORT}/", @@ -249,9 +250,7 @@ def _update_hydra_endpoints_relation_data(self, event: RelationEvent) -> None: f"Sending endpoints info: public - {public_endpoint[0]} admin - {admin_endpoint[0]}" ) - self.endpoints_provider.send_endpoint_relation_data( - self.app, admin_endpoint[0], public_endpoint[0] - ) + self.endpoints_provider.send_endpoint_relation_data(admin_endpoint[0], public_endpoint[0]) def _on_hydra_pebble_ready(self, event: WorkloadEvent) -> None: """Event Handler for pebble ready event.""" diff --git a/src/hydra_cli.py b/src/hydra_cli.py index 0289f1a1..f9af84ee 100644 --- a/src/hydra_cli.py +++ b/src/hydra_cli.py @@ -55,9 +55,7 @@ def _client_cmd_prefix(self, action: str) -> List[str]: "json", ] - def create_client( - self, client_config: ClientConfig, metadata: Optional[Union[Dict, str]] = None - ) -> Dict: + def create_client(self, client_config: ClientConfig, metadata: Optional[Dict] = None) -> Dict: """Create an oauth2 client.""" cmd = self._client_cmd_prefix("create") + self._client_config_to_cmd( client_config, metadata @@ -68,9 +66,7 @@ def create_client( logger.info(f"Successfully created client: {json_stdout.get('client_id')}") return json_stdout - def update_client( - self, client_config: ClientConfig, metadata: Optional[Union[Dict, str]] = None - ) -> Dict: + def update_client(self, client_config: ClientConfig, metadata: Optional[Dict] = None) -> Dict: """Update an oauth2 client.""" cmd = self._client_cmd_prefix("update") + self._client_config_to_cmd( client_config, metadata diff --git a/tests/integration/test_charm.py b/tests/integration/test_charm.py index ece7b7d4..1ba62908 100644 --- a/tests/integration/test_charm.py +++ b/tests/integration/test_charm.py @@ -26,7 +26,7 @@ async def get_unit_address(ops_test: OpsTest, app_name: str, unit_num: int) -> s @pytest.mark.abort_on_fail -async def test_build_and_deploy(ops_test: OpsTest): +async def test_build_and_deploy(ops_test: OpsTest) -> None: """Build hydra and deploy it with required charms and relations.""" charm = await ops_test.build_charm(".") hydra_image_path = METADATA["resources"]["oci-image"]["upstream-source"] @@ -60,7 +60,7 @@ async def test_build_and_deploy(ops_test: OpsTest): assert ops_test.model.applications[APP_NAME].units[0].workload_status == "active" -async def test_ingress_relation(ops_test: OpsTest): +async def test_ingress_relation(ops_test: OpsTest) -> None: await ops_test.model.deploy( TRAEFIK, application_name=TRAEFIK_PUBLIC_APP, @@ -84,7 +84,7 @@ async def test_ingress_relation(ops_test: OpsTest): ) -async def test_has_public_ingress(ops_test: OpsTest): +async def test_has_public_ingress(ops_test: OpsTest) -> None: # Get the traefik address and try to reach hydra public_address = await get_unit_address(ops_test, TRAEFIK_PUBLIC_APP, 0) @@ -95,7 +95,7 @@ async def test_has_public_ingress(ops_test: OpsTest): assert resp.status_code == 200 -async def test_has_admin_ingress(ops_test: OpsTest): +async def test_has_admin_ingress(ops_test: OpsTest) -> None: # Get the traefik address and try to reach hydra admin_address = await get_unit_address(ops_test, TRAEFIK_ADMIN_APP, 0) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py deleted file mode 100644 index 1739b9fa..00000000 --- a/tests/unit/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright 2023 Canonical Ltd. -# See LICENSE file for licensing details. - -import ops.testing - -ops.testing.SIMULATE_CAN_CONNECT = True diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index f69866e4..122fa8b3 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -1,16 +1,18 @@ # Copyright 2023 Canonical Ltd. # See LICENSE file for licensing details. -from typing import Generator +from typing import Dict, Generator +from unittest.mock import MagicMock import pytest from ops.testing import Harness +from pytest_mock import MockerFixture from charm import HydraCharm @pytest.fixture() -def harness(mocked_kubernetes_service_patcher): +def harness(mocked_kubernetes_service_patcher: MagicMock) -> Harness: harness = Harness(HydraCharm) harness.set_model_name("testing") harness.set_leader(True) @@ -19,56 +21,67 @@ def harness(mocked_kubernetes_service_patcher): @pytest.fixture() -def mocked_kubernetes_service_patcher(mocker): +def mocked_kubernetes_service_patcher(mocker: MockerFixture) -> Generator: mocked_service_patcher = mocker.patch("charm.KubernetesServicePatch") mocked_service_patcher.return_value = lambda x, y: None yield mocked_service_patcher @pytest.fixture() -def mocked_hydra_is_running(mocker) -> Generator: +def mocked_hydra_is_running(mocker: MockerFixture) -> Generator: yield mocker.patch("charm.HydraCharm._hydra_service_is_running", return_value=True) @pytest.fixture() -def mocked_sql_migration(mocker): +def mocked_sql_migration(mocker: MockerFixture) -> Generator: mocked_sql_migration = mocker.patch("charm.HydraCharm._run_sql_migration") yield mocked_sql_migration @pytest.fixture() -def mocked_create_client(mocker): +def mocked_create_client(mocker: MockerFixture) -> Generator: mock = mocker.patch("charm.HydraCLI.create_client") mock.return_value = {"client_id": "client_id", "client_secret": "client_secret"} yield mock @pytest.fixture() -def mocked_update_client(mocker): +def mocked_update_client(mocker: MockerFixture) -> Generator: mock = mocker.patch("charm.HydraCLI.update_client") mock.return_value = {"client_id": "client_id", "client_secret": "client_secret"} yield mock @pytest.fixture() -def mocked_delete_client(mocker): +def mocked_delete_client(mocker: MockerFixture) -> Generator: mock = mocker.patch("charm.HydraCLI.delete_client") mock.return_value = "client_id" yield mock @pytest.fixture() -def mocked_set_provider_info(mocker): +def mocked_set_provider_info(mocker: MockerFixture) -> Generator: yield mocker.patch("charm.OAuthProvider.set_provider_info_in_relation_data") @pytest.fixture() -def mocked_set_client_credentials(mocker): +def mocked_set_client_credentials(mocker: MockerFixture) -> Generator: yield mocker.patch("charm.OAuthProvider.set_client_credentials_in_relation_data") @pytest.fixture() -def mocked_fqdn(mocker): +def mocked_fqdn(mocker: MockerFixture) -> Generator: mocked_fqdn = mocker.patch("socket.getfqdn") mocked_fqdn.return_value = "hydra" return mocked_fqdn + + +@pytest.fixture() +def client_config() -> Dict: + return { + "redirect_uri": "https://example.oidc.client/callback", + "scope": "openid email offline_access", + "grant_types": ["authorization_code", "refresh_token"], + "audience": [], + "token_endpoint_auth_method": "client_secret_basic", + } diff --git a/tests/unit/test_charm.py b/tests/unit/test_charm.py index 2ee339f2..32931b5e 100644 --- a/tests/unit/test_charm.py +++ b/tests/unit/test_charm.py @@ -11,8 +11,7 @@ from ops.model import ActiveStatus, BlockedStatus, WaitingStatus from ops.pebble import Error, ExecError from ops.testing import Harness - -from tests.unit.test_oauth_requirer import CLIENT_CONFIG +from test_oauth_requirer import CLIENT_CONFIG # type: ignore CONTAINER_NAME = "hydra" DB_USERNAME = "test-username" @@ -20,7 +19,7 @@ DB_ENDPOINT = "postgresql-k8s-primary.namespace.svc.cluster.local:5432" -def setup_postgres_relation(harness): +def setup_postgres_relation(harness: Harness) -> int: db_relation_id = harness.add_relation("pg-database", "postgresql-k8s") harness.add_relation_unit(db_relation_id, "postgresql-k8s/0") harness.update_relation_data( @@ -37,7 +36,7 @@ def setup_postgres_relation(harness): return db_relation_id -def setup_ingress_relation(harness, type): +def setup_ingress_relation(harness: Harness, type: str) -> int: relation_id = harness.add_relation(f"{type}-ingress", f"{type}-traefik") harness.add_relation_unit(relation_id, f"{type}-traefik/0") harness.update_relation_data( @@ -55,7 +54,7 @@ def setup_oauth_relation(harness: Harness) -> Tuple[int, str]: return relation_id, app_name -def test_not_leader(harness): +def test_not_leader(harness: Harness) -> None: harness.set_leader(False) setup_postgres_relation(harness) @@ -69,14 +68,14 @@ def test_not_leader(harness): ) in harness._get_backend_calls() -def test_install_without_relation(harness): +def test_install_without_relation(harness: Harness) -> None: harness.set_can_connect(CONTAINER_NAME, True) harness.charm.on.hydra_pebble_ready.emit(CONTAINER_NAME) assert harness.charm.unit.status == BlockedStatus("Missing required relation with postgresql") -def test_install_without_database(harness): +def test_install_without_database(harness: Harness) -> None: db_relation_id = harness.add_relation("pg-database", "postgresql-k8s") harness.add_relation_unit(db_relation_id, "postgresql-k8s/0") @@ -86,7 +85,7 @@ def test_install_without_database(harness): assert harness.charm.unit.status == WaitingStatus("Waiting for database creation") -def test_relation_data(harness, mocked_sql_migration): +def test_relation_data(harness: Harness, mocked_sql_migration: MagicMock) -> None: db_relation_id = setup_postgres_relation(harness) relation_data = harness.get_relation_data(db_relation_id, "postgresql-k8s") @@ -95,14 +94,14 @@ def test_relation_data(harness, mocked_sql_migration): assert relation_data["endpoints"] == "postgresql-k8s-primary.namespace.svc.cluster.local:5432" -def test_relation_departed(harness, mocked_sql_migration): +def test_relation_departed(harness: Harness, mocked_sql_migration: MagicMock) -> None: db_relation_id = setup_postgres_relation(harness) harness.remove_relation_unit(db_relation_id, "postgresql-k8s/0") assert harness.charm.unit.status == BlockedStatus("Missing required relation with postgresql") -def test_pebble_container_can_connect(harness, mocked_sql_migration): +def test_pebble_container_can_connect(harness: Harness, mocked_sql_migration: MagicMock) -> None: setup_postgres_relation(harness) harness.set_can_connect(CONTAINER_NAME, True) @@ -113,7 +112,9 @@ def test_pebble_container_can_connect(harness, mocked_sql_migration): assert service.is_running() -def test_pebble_container_cannot_connect(harness, mocked_sql_migration): +def test_pebble_container_cannot_connect( + harness: Harness, mocked_sql_migration: MagicMock +) -> None: setup_postgres_relation(harness) harness.set_can_connect(CONTAINER_NAME, False) @@ -122,7 +123,7 @@ def test_pebble_container_cannot_connect(harness, mocked_sql_migration): assert harness.charm.unit.status == WaitingStatus("Waiting to connect to Hydra container") -def test_update_container_config(harness, mocked_sql_migration): +def test_update_container_config(harness: Harness, mocked_sql_migration: MagicMock) -> None: harness.set_can_connect(CONTAINER_NAME, True) setup_postgres_relation(harness) @@ -166,14 +167,14 @@ def test_update_container_config(harness, mocked_sql_migration): assert yaml.safe_load(harness.charm._render_conf_file()) == expected_config -def test_on_config_changed_without_service(harness) -> None: +def test_on_config_changed_without_service(harness: Harness) -> None: setup_postgres_relation(harness) harness.update_config({"login_ui_url": "http://some-url"}) assert harness.charm.unit.status == WaitingStatus("Waiting to connect to Hydra container") -def test_on_config_changed_without_database(harness) -> None: +def test_on_config_changed_without_database(harness: Harness) -> None: harness.set_can_connect(CONTAINER_NAME, True) harness.charm.on.hydra_pebble_ready.emit(CONTAINER_NAME) harness.update_config({"login_ui_url": "http://some-url"}) @@ -181,7 +182,9 @@ def test_on_config_changed_without_database(harness) -> None: assert harness.charm.unit.status == BlockedStatus("Missing required relation with postgresql") -def test_config_updated_on_config_changed(harness, mocked_sql_migration) -> None: +def test_config_updated_on_config_changed( + harness: Harness, mocked_sql_migration: MagicMock +) -> None: harness.set_can_connect(CONTAINER_NAME, True) harness.charm.on.hydra_pebble_ready.emit(CONTAINER_NAME) setup_postgres_relation(harness) @@ -227,7 +230,9 @@ def test_config_updated_on_config_changed(harness, mocked_sql_migration) -> None @pytest.mark.parametrize("api_type,port", [("admin", "4445"), ("public", "4444")]) -def test_ingress_relation_created(harness, mocked_fqdn, api_type, port) -> None: +def test_ingress_relation_created( + harness: Harness, mocked_fqdn: MagicMock, api_type: str, port: str +) -> None: harness.set_can_connect(CONTAINER_NAME, True) relation_id = setup_ingress_relation(harness, api_type) @@ -242,7 +247,7 @@ def test_ingress_relation_created(harness, mocked_fqdn, api_type, port) -> None: } -def test_config_updated_on_ingress_relation_joined(harness) -> None: +def test_config_updated_on_ingress_relation_joined(harness: Harness) -> None: harness.set_can_connect(CONTAINER_NAME, True) setup_postgres_relation(harness) @@ -286,7 +291,7 @@ def test_config_updated_on_ingress_relation_joined(harness) -> None: assert yaml.safe_load(harness.charm._render_conf_file()) == expected_config -def test_hydra_config_on_pebble_ready_without_ingress_relation_data(harness) -> None: +def test_hydra_config_on_pebble_ready_without_ingress_relation_data(harness: Harness) -> None: harness.set_can_connect(CONTAINER_NAME, True) # set relation without data @@ -336,7 +341,7 @@ def test_hydra_config_on_pebble_ready_without_ingress_relation_data(harness) -> assert yaml.load(container_config.read(), yaml.Loader) == expected_config -def test_hydra_endpoint_info_relation_data_without_ingress_relation_data(harness) -> None: +def test_hydra_endpoint_info_relation_data_without_ingress_relation_data(harness: Harness) -> None: harness.set_can_connect(CONTAINER_NAME, True) # set relations without data @@ -356,7 +361,7 @@ def test_hydra_endpoint_info_relation_data_without_ingress_relation_data(harness assert harness.get_relation_data(endpoint_info_relation_id, "hydra") == expected_data -def test_hydra_endpoint_info_relation_data_with_ingress_relation_data(harness) -> None: +def test_hydra_endpoint_info_relation_data_with_ingress_relation_data(harness: Harness) -> None: harness.set_can_connect(CONTAINER_NAME, True) setup_ingress_relation(harness, "public") diff --git a/tests/unit/test_oauth_provider.py b/tests/unit/test_oauth_provider.py index a4eb81d1..1b412081 100644 --- a/tests/unit/test_oauth_provider.py +++ b/tests/unit/test_oauth_provider.py @@ -2,7 +2,7 @@ # See LICENSE file for licensing details. from os.path import join -from typing import Any, Generator +from typing import Any, Generator, List import pytest from charms.hydra.v0.oauth import ( @@ -31,7 +31,7 @@ class OAuthProviderCharm(CharmBase): def __init__(self, *args: Any) -> None: super().__init__(*args) self.oauth = OAuthProvider(self) - self.events = [] + self.events: List = [] self.framework.observe(self.on.oauth_relation_created, self._on_relation_created) self.framework.observe(self.oauth.on.client_created, self._on_client_created) diff --git a/tests/unit/test_oauth_requirer.py b/tests/unit/test_oauth_requirer.py index e1ef46ac..3b578354 100644 --- a/tests/unit/test_oauth_requirer.py +++ b/tests/unit/test_oauth_requirer.py @@ -2,7 +2,7 @@ # See LICENSE file for licensing details. import json -from typing import Any, Dict, Generator +from typing import Any, Dict, Generator, List import pytest from charms.hydra.v0.oauth import ( @@ -56,7 +56,7 @@ def provider_info() -> Dict: } -def dict_to_relation_data(dic): +def dict_to_relation_data(dic: Dict) -> Dict: return {k: json.dumps(v) if isinstance(v, (list, dict)) else v for k, v in dic.items()} @@ -66,7 +66,7 @@ def __init__(self, *args: Any) -> None: client_config = ClientConfig(**CLIENT_CONFIG) self.oauth = OAuthRequirer(self, client_config=client_config) - self.events = [] + self.events: List = [] self.framework.observe(self.oauth.on.oauth_info_changed, self._record_event) self.framework.observe(self.oauth.on.invalid_client_config, self._record_event) @@ -191,7 +191,7 @@ def __init__(self, *args: Any) -> None: client_config.redirect_uri = "http://some.callback" self.oauth = OAuthRequirer(self, client_config=client_config) - self.events = [] + self.events: List = [] self.framework.observe(self.oauth.on.oauth_info_changed, self._record_event) self.framework.observe(self.oauth.on.invalid_client_config, self._record_event)