diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 433c1b9..f85a3a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,3 +1,5 @@ +name: Test + on: pull_request jobs: diff --git a/pyproject.toml b/pyproject.toml index a4c028d..d6e4998 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -51,6 +51,7 @@ dev = [ "pydantic-settings>=2.6.1", "pytest-asyncio>=0.24.0", "pytest-cov>=6.0.0", + "pytest-dotenv>=0.5.2", "pytest-mock>=3.14.0", "pytest>=8.3.4", "respx>=0.21.1", @@ -63,3 +64,4 @@ dev = [ addopts = "-v -x --cov=src --cov-report=term-missing:skip-covered" asyncio_mode = "auto" asyncio_default_fixture_loop_scope = "function" +env_files = ["tests/.env.test"] diff --git a/src/fastapi_zitadel_auth/models.py b/src/fastapi_zitadel_auth/models.py index 011df5e..057f44a 100644 --- a/src/fastapi_zitadel_auth/models.py +++ b/src/fastapi_zitadel_auth/models.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any, List from pydantic import BaseModel, Field diff --git a/tests/.env.test b/tests/.env.test new file mode 100644 index 0000000..e2d81c1 --- /dev/null +++ b/tests/.env.test @@ -0,0 +1,3 @@ +ZITADEL_HOST=https://test-zitadel-xs2hs.zitadel.cloud +ZITADEL_PROJECT_ID=123456789 +OAUTH_CLIENT_ID=7777777777 diff --git a/tests/test_config.py b/tests/test_config.py new file mode 100644 index 0000000..837b06b --- /dev/null +++ b/tests/test_config.py @@ -0,0 +1,92 @@ +""" +Test the auth config module +""" + +import pytest +from pydantic import ValidationError + +from fastapi_zitadel_auth.config import AuthConfig + + +@pytest.fixture +def valid_config_data() -> dict: + """ + Valid configuration data for testing + """ + return { + "client_id": "test-client-123", + "project_id": "proj-123", + "base_url": "https://auth.example.com/", + } + + +class TestAuthConfig: + """Test suite for AuthConfig class""" + + def test_valid_config(self, valid_config_data): + """Test creating config with valid data""" + config = AuthConfig(**valid_config_data) + assert config.client_id == "test-client-123" + assert config.project_id == "proj-123" + assert str(config.base_url) == "https://auth.example.com/" + assert config.algorithm == "RS256" # default value + assert config.scopes is None # default value + + def test_computed_issuer(self, valid_config_data): + """Test issuer computed field removes trailing slash""" + config = AuthConfig(**valid_config_data) + assert config.issuer == "https://auth.example.com" + + # Test without trailing slash + valid_config_data["base_url"] = "https://auth.example.com" + config = AuthConfig(**valid_config_data) + assert config.issuer == "https://auth.example.com" + + def test_computed_urls(self, valid_config_data): + """Test computed URL fields""" + config = AuthConfig(**valid_config_data) + + assert config.jwks_url == "https://auth.example.com/oauth/v2/keys" + assert config.authorization_url == "https://auth.example.com/oauth/v2/authorize" + assert config.token_url == "https://auth.example.com/oauth/v2/token" + + def test_custom_scopes(self, valid_config_data): + """Test configuration with custom scopes""" + valid_config_data["scopes"] = { + "openid": "OpenID scope", + "profile": "Profile information", + } + config = AuthConfig(**valid_config_data) + assert config.scopes == { + "openid": "OpenID scope", + "profile": "Profile information", + } + + def test_custom_algorithm(self, valid_config_data): + """Test configuration with custom algorithm""" + valid_config_data["algorithm"] = "ES256" + config = AuthConfig(**valid_config_data) + assert config.algorithm == "ES256" + + def test_invalid_url(self, valid_config_data): + """Test validation error for invalid URL""" + valid_config_data["base_url"] = "not-a-url" + with pytest.raises(ValidationError) as exc_info: + AuthConfig(**valid_config_data) + errors = exc_info.value.errors() + assert any(error["type"] == "url_parsing" for error in errors) + + def test_missing_required_fields(self): + """Test validation error for missing required fields""" + with pytest.raises(ValidationError) as exc_info: + AuthConfig() + errors = exc_info.value.errors() + required_fields = {"client_id", "project_id", "base_url"} + error_fields = {error["loc"][0] for error in errors} + assert required_fields.issubset(error_fields) + + def test_config_immutability(self, valid_config_data): + """Test that config is immutable after creation""" + config = AuthConfig(**valid_config_data) + with pytest.raises(Exception): + config.client_id = "new-client" # type: ignore diff --git a/uv.lock b/uv.lock index 34d8ff0..320143b 100644 --- a/uv.lock +++ b/uv.lock @@ -283,6 +283,7 @@ dev = [ { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, + { name = "pytest-dotenv" }, { name = "pytest-mock" }, { name = "respx" }, { name = "ruff" }, @@ -307,6 +308,7 @@ dev = [ { name = "pytest", specifier = ">=8.3.4" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "pytest-cov", specifier = ">=6.0.0" }, + { name = "pytest-dotenv", specifier = ">=0.5.2" }, { name = "pytest-mock", specifier = ">=3.14.0" }, { name = "respx", specifier = ">=0.21.1" }, { name = "ruff", specifier = ">=0.8.1" }, @@ -637,6 +639,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, ] +[[package]] +name = "pytest-dotenv" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "python-dotenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/b0/cafee9c627c1bae228eb07c9977f679b3a7cb111b488307ab9594ba9e4da/pytest-dotenv-0.5.2.tar.gz", hash = "sha256:2dc6c3ac6d8764c71c6d2804e902d0ff810fa19692e95fe138aefc9b1aa73732", size = 3782 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/da/9da67c67b3d0963160e3d2cbc7c38b6fae342670cc8e6d5936644b2cf944/pytest_dotenv-0.5.2-py3-none-any.whl", hash = "sha256:40a2cece120a213898afaa5407673f6bd924b1fa7eafce6bda0e8abffe2f710f", size = 3993 }, +] + [[package]] name = "pytest-mock" version = "3.14.0"