From 17b20f08351c4dc64915542f6e424a52358fb0ba Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Tue, 16 Apr 2024 18:12:24 +0200 Subject: [PATCH 01/10] start with unittest --- .github/workflows/pr-tests.yml | 2 +- dsp_permissions_scripts/oap/oap_model.py | 6 +- poetry.lock | 70 +++++++++++++++++++++++- pyproject.toml | 1 + tests/test_oap_model.py | 9 +++ 5 files changed, 85 insertions(+), 3 deletions(-) create mode 100644 tests/test_oap_model.py diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 2b8b9fce..65ea696e 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -38,6 +38,6 @@ jobs: run: poetry run mypy . - name: unittests - run: poetry run python -m unittest discover tests + run: poetry run pytest -s tests diff --git a/dsp_permissions_scripts/oap/oap_model.py b/dsp_permissions_scripts/oap/oap_model.py index 375a29fe..5af3cebe 100644 --- a/dsp_permissions_scripts/oap/oap_model.py +++ b/dsp_permissions_scripts/oap/oap_model.py @@ -19,6 +19,11 @@ class Oap(BaseModel): resource_oap: ResourceOap | None value_oaps: list[ValueOap] + @model_validator(mode="after") + def check(self) -> Oap: + if not self.resource_oap and not self.value_oaps: + raise ValueError("An OAP must have at least one resource_oap or one value_oap") + class ResourceOap(BaseModel): """Model representing an object access permission of a resource""" @@ -46,7 +51,6 @@ class ValueOap(BaseModel): class OapRetrieveConfig(BaseModel): - model_config = ConfigDict(frozen=True) retrieve_resources: bool = True diff --git a/poetry.lock b/poetry.lock index e3701f28..7c204bec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -132,6 +132,17 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -184,6 +195,17 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "mypy" version = "1.9.0" @@ -255,6 +277,17 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + [[package]] name = "platformdirs" version = "4.2.0" @@ -270,6 +303,21 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pre-commit" version = "3.7.0" @@ -398,6 +446,26 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -589,4 +657,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.12.0" -content-hash = "1a10cbba17cab64c9b73631fecdf215770aa7b1ec3df3cee1fdd162b50c352a5" +content-hash = "eced2fcc562d11991fade44a74f72135337b5d9136f090b57df52ba08472a9d7" diff --git a/pyproject.toml b/pyproject.toml index 3544d3ff..8ecb0b5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ types-requests = "^2.31.0.20240406" python-dotenv = "^1.0.1" ruff = "^0.3.7" pre-commit = "^3.7.0" +pytest = "^8.1.1" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/tests/test_oap_model.py b/tests/test_oap_model.py new file mode 100644 index 00000000..31120bd1 --- /dev/null +++ b/tests/test_oap_model.py @@ -0,0 +1,9 @@ +import pytest + +from dsp_permissions_scripts.oap.oap_model import Oap + + +class TestOap: + def test_oap_no_res_no_vals(self) -> None: + with pytest.raises(ValueError): # noqa: PT011 (exception too broad) + Oap(resource_oap=None, value_oaps=[]) From d3240ea62260b33f0ebc9c5ab17b572773921cab Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Tue, 16 Apr 2024 18:13:16 +0200 Subject: [PATCH 02/10] fix mypy --- dsp_permissions_scripts/oap/oap_model.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dsp_permissions_scripts/oap/oap_model.py b/dsp_permissions_scripts/oap/oap_model.py index 5af3cebe..3b42ed18 100644 --- a/dsp_permissions_scripts/oap/oap_model.py +++ b/dsp_permissions_scripts/oap/oap_model.py @@ -20,9 +20,10 @@ class Oap(BaseModel): value_oaps: list[ValueOap] @model_validator(mode="after") - def check(self) -> Oap: + def check_consistency(self) -> Oap: if not self.resource_oap and not self.value_oaps: raise ValueError("An OAP must have at least one resource_oap or one value_oap") + return self class ResourceOap(BaseModel): From 7b97caca5c63b6ac693824fbe050fe9f0dc8fb8d Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Tue, 16 Apr 2024 18:16:44 +0200 Subject: [PATCH 03/10] edit --- dsp_permissions_scripts/oap/oap_get.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/dsp_permissions_scripts/oap/oap_get.py b/dsp_permissions_scripts/oap/oap_get.py index 2d729e98..a6599658 100644 --- a/dsp_permissions_scripts/oap/oap_get.py +++ b/dsp_permissions_scripts/oap/oap_get.py @@ -69,19 +69,22 @@ def _get_next_page( if "@graph" in result: oaps = [] for r in result["@graph"]: - oaps.append(_get_oap_of_one_resource(r, oap_config)) + if oap := _get_oap_of_one_resource(r, oap_config): + oaps.append(oap) return True, oaps # result contains only 1 resource: return it, then stop (there will be no more resources) if "@id" in result: - oaps = [_get_oap_of_one_resource(result, oap_config)] + oaps = [] + if oap := _get_oap_of_one_resource(result, oap_config): + oaps.append(oap) return False, oaps # there are no more resources return False, [] -def _get_oap_of_one_resource(r: dict[str, Any], oap_config: OapRetrieveConfig) -> Oap: +def _get_oap_of_one_resource(r: dict[str, Any], oap_config: OapRetrieveConfig) -> Oap | None: if oap_config.retrieve_resources: scope = create_scope_from_string(r["knora-api:hasPermissions"]) resource_oap = ResourceOap(scope=scope, resource_iri=r["@id"]) @@ -95,7 +98,10 @@ def _get_oap_of_one_resource(r: dict[str, Any], oap_config: OapRetrieveConfig) - else: value_oaps = _get_value_oaps(r, oap_config.specified_props) - return Oap(resource_oap=resource_oap, value_oaps=value_oaps) + if resource_oap or value_oaps: + return Oap(resource_oap=resource_oap, value_oaps=value_oaps) + else: + return None def _get_value_oaps(resource: dict[str, Any], restrict_to_props: list[str] | None = None) -> list[ValueOap]: From 440d73f55044e7983c5906a6439f65e0a077ce13 Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Wed, 17 Apr 2024 09:17:07 +0200 Subject: [PATCH 04/10] write more unit tests --- pyproject.toml | 5 +++ tests/test_oap_model.py | 87 ++++++++++++++++++++++++++++++++++++++++- 2 files changed, 91 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ecb0b5a..a12ed696 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,11 @@ select = [ "FIX", # flake8-fixme: checks for FIXME, TODO, XXX, etc. ] +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "S101", # flake8-bandit: use of assert +] + [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/tests/test_oap_model.py b/tests/test_oap_model.py index 31120bd1..386aa558 100644 --- a/tests/test_oap_model.py +++ b/tests/test_oap_model.py @@ -1,9 +1,94 @@ import pytest +from dsp_permissions_scripts.models import group +from dsp_permissions_scripts.models.scope import PermissionScope from dsp_permissions_scripts.oap.oap_model import Oap +from dsp_permissions_scripts.oap.oap_model import OapRetrieveConfig +from dsp_permissions_scripts.oap.oap_model import ResourceOap +from dsp_permissions_scripts.oap.oap_model import ValueOap + +# ruff: noqa: PT011 (exception too broad) class TestOap: + def test_oap_one_val(self) -> None: + res_iri = "http://rdfh.ch/0803/foo" + scope = PermissionScope.create(D=[group.UNKNOWN_USER]) + res_oap = ResourceOap(scope=scope, resource_iri=res_iri) + val_oaps = [ + ValueOap( + scope=scope, + property="foo:prop", + value_type="foo:valtype", + value_iri=f"{res_iri}/values/bar", + resource_iri=res_iri, + ) + ] + oap = Oap(resource_oap=res_oap, value_oaps=val_oaps) + assert oap.resource_oap == res_oap + assert oap.value_oaps == val_oaps + + def test_oap_multiple_vals(self) -> None: + res_iri = "http://rdfh.ch/0803/foo" + res_scope = PermissionScope.create(D=[group.UNKNOWN_USER]) + val_scope = PermissionScope.create(M=[group.KNOWN_USER]) + res_oap = ResourceOap(scope=res_scope, resource_iri=res_iri) + val_oap_1 = ValueOap( + scope=val_scope, + property="foo:prop", + value_type="foo:valtype", + value_iri=f"{res_iri}/values/bar", + resource_iri=res_iri, + ) + val_oap_2 = val_oap_1.model_copy(update={"value_iri": f"{res_iri}/values/baz"}) + oap = Oap(resource_oap=res_oap, value_oaps=[val_oap_1, val_oap_2]) + assert oap.resource_oap == res_oap + assert oap.value_oaps == [val_oap_1, val_oap_2] + + def test_oap_no_res(self) -> None: + res_iri = "http://rdfh.ch/0803/foo" + scope = PermissionScope.create(D=[group.UNKNOWN_USER]) + val_oaps = [ + ValueOap( + scope=scope, + property="foo:prop", + value_type="foo:valtype", + value_iri=f"{res_iri}/values/bar", + resource_iri=res_iri, + ) + ] + oap = Oap(resource_oap=None, value_oaps=val_oaps) + assert oap.resource_oap is None + assert oap.value_oaps == val_oaps + def test_oap_no_res_no_vals(self) -> None: - with pytest.raises(ValueError): # noqa: PT011 (exception too broad) + with pytest.raises(ValueError): Oap(resource_oap=None, value_oaps=[]) + + +class TestOapRetrieveConfig: + def test_with_resource(self) -> None: + conf = OapRetrieveConfig(retrieve_resources=True, retrieve_values="none") + assert conf.retrieve_resources is True + assert conf.retrieve_values == "none" + assert conf.specified_props == [] + + def test_without_resource(self) -> None: + conf = OapRetrieveConfig(retrieve_resources=False, retrieve_values="all") + assert conf.retrieve_resources is False + assert conf.retrieve_values == "all" + assert conf.specified_props == [] + + def test_empty(self) -> None: + with pytest.raises(ValueError): + OapRetrieveConfig(retrieve_resources=False, retrieve_values="none") + with pytest.raises(ValueError): + OapRetrieveConfig(retrieve_resources=False, retrieve_values="none", specified_props=[]) + + def test_all_values_but_specified(self) -> None: + with pytest.raises(ValueError): + OapRetrieveConfig(retrieve_resources=False, retrieve_values="all", specified_props=["foo"]) + + def test_no_values_but_specified(self) -> None: + with pytest.raises(ValueError): + OapRetrieveConfig(retrieve_resources=False, retrieve_values="none", specified_props=["foo"]) From ee449e8b933d8284266dc7db197e8d048d53a4af Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum <39048939+jnussbaum@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:42:56 +0200 Subject: [PATCH 05/10] chore: use pytest instead of unittest / tooling / formatting (#93) --- .github/workflows/pr-tests.yml | 4 +- dsp_permissions_scripts/models/group.py | 1 - poetry.lock | 70 ++++++++++++++++++++++++- pyproject.toml | 9 ++++ tests/test_ap.py | 1 + tests/test_ap_serialization.py | 1 + tests/test_doap_serialization.py | 1 + tests/test_helpers.py | 1 + 8 files changed, 84 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pr-tests.yml b/.github/workflows/pr-tests.yml index 2b8b9fce..9fa50d89 100644 --- a/.github/workflows/pr-tests.yml +++ b/.github/workflows/pr-tests.yml @@ -29,7 +29,7 @@ jobs: run: poetry install - name: Formatting with ruff - run: poetry run ruff format . + run: poetry run ruff format --check . - name: Linting with ruff run: poetry run ruff check . @@ -38,6 +38,6 @@ jobs: run: poetry run mypy . - name: unittests - run: poetry run python -m unittest discover tests + run: poetry run pytest -s tests diff --git a/dsp_permissions_scripts/models/group.py b/dsp_permissions_scripts/models/group.py index 3d9c6ed4..3814963d 100644 --- a/dsp_permissions_scripts/models/group.py +++ b/dsp_permissions_scripts/models/group.py @@ -8,7 +8,6 @@ class Group(BaseModel): - model_config = ConfigDict(frozen=True) val: str diff --git a/poetry.lock b/poetry.lock index e3701f28..7c204bec 100644 --- a/poetry.lock +++ b/poetry.lock @@ -132,6 +132,17 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + [[package]] name = "distlib" version = "0.3.8" @@ -184,6 +195,17 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "mypy" version = "1.9.0" @@ -255,6 +277,17 @@ files = [ [package.dependencies] setuptools = "*" +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + [[package]] name = "platformdirs" version = "4.2.0" @@ -270,6 +303,21 @@ files = [ docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pre-commit" version = "3.7.0" @@ -398,6 +446,26 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pytest" +version = "8.1.1" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"}, + {file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.4,<2.0" + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -589,4 +657,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.12.0" -content-hash = "1a10cbba17cab64c9b73631fecdf215770aa7b1ec3df3cee1fdd162b50c352a5" +content-hash = "eced2fcc562d11991fade44a74f72135337b5d9136f090b57df52ba08472a9d7" diff --git a/pyproject.toml b/pyproject.toml index 3544d3ff..b6226883 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ types-requests = "^2.31.0.20240406" python-dotenv = "^1.0.1" ruff = "^0.3.7" pre-commit = "^3.7.0" +pytest = "^8.1.1" [build-system] build-backend = "poetry.core.masonry.api" @@ -52,6 +53,14 @@ select = [ "B023", # flake8-bugbear: function-uses-loop-variable "FIX", # flake8-fixme: checks for FIXME, TODO, XXX, etc. ] +ignore = [ + "ISC001", # flake8-implicit-str-concat: single-line-implicit-string-concatenation # incompatible with the formatter +] + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "S101", # flake8-bandit: use of assert +] [tool.ruff.lint.pydocstyle] convention = "google" diff --git a/tests/test_ap.py b/tests/test_ap.py index b853eeb8..5fba5cff 100644 --- a/tests/test_ap.py +++ b/tests/test_ap.py @@ -7,6 +7,7 @@ # ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) # ruff: noqa: PT027 (pytest-unittest-raises-assertion) (remove this line when pytest is used instead of unittest) + class TestAp(unittest.TestCase): ap = Ap( forGroup=group.UNKNOWN_USER, diff --git a/tests/test_ap_serialization.py b/tests/test_ap_serialization.py index 3773d241..2e33ea60 100644 --- a/tests/test_ap_serialization.py +++ b/tests/test_ap_serialization.py @@ -12,6 +12,7 @@ # ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) + class TestApSerialization(unittest.TestCase): shortcode = "1234" project_iri = "http://rdfh.ch/projects/MsOaiQkcQ7-QPxsYBKckfQ" diff --git a/tests/test_doap_serialization.py b/tests/test_doap_serialization.py index ee21ff97..d0a9b5c0 100644 --- a/tests/test_doap_serialization.py +++ b/tests/test_doap_serialization.py @@ -13,6 +13,7 @@ # ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) + class TestDoapSerialization(unittest.TestCase): shortcode = "1234" diff --git a/tests/test_helpers.py b/tests/test_helpers.py index c0fe3773..83733c06 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -5,6 +5,7 @@ # ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) + class TestHelpers(unittest.TestCase): def test_sort_groups(self) -> None: groups_original = [ From 188f2848794114db526584b734f8b32cd43971cf Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Wed, 17 Apr 2024 09:47:06 +0200 Subject: [PATCH 06/10] remove duplicated config --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 682357e4..b6226883 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,11 +62,6 @@ ignore = [ "S101", # flake8-bandit: use of assert ] -[tool.ruff.lint.per-file-ignores] -"tests/*" = [ - "S101", # flake8-bandit: use of assert -] - [tool.ruff.lint.pydocstyle] convention = "google" From a1a434777575a2ea2b6281fe02e528bfc21e5b89 Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Wed, 17 Apr 2024 10:00:48 +0200 Subject: [PATCH 07/10] improve validation of OapRetrieveConfig --- dsp_permissions_scripts/models/errors.py | 15 +++++++++++++++ dsp_permissions_scripts/oap/oap_model.py | 15 ++++++++++++--- tests/test_oap_model.py | 4 ++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/dsp_permissions_scripts/models/errors.py b/dsp_permissions_scripts/models/errors.py index 7cb3baf8..67617057 100644 --- a/dsp_permissions_scripts/models/errors.py +++ b/dsp_permissions_scripts/models/errors.py @@ -17,3 +17,18 @@ def __str__(self) -> str: @dataclass class PermissionsAlreadyUpToDate(Exception): message: str = "The submitted permissions are the same as the current ones" + + +@dataclass +class SpecifiedPropsEmptyError(ValueError): + message: str = "specified_props must not be empty if retrieve_values is 'specified_props'" + + +@dataclass +class SpecifiedPropsNotEmptyError(ValueError): + message: str = "specified_props must be empty if retrieve_values is not 'specified_props'" + + +@dataclass +class OapRetrieveConfigEmptyError(ValueError): + message: str = "retrieve_resources cannot be False if retrieve_values is 'none'" diff --git a/dsp_permissions_scripts/oap/oap_model.py b/dsp_permissions_scripts/oap/oap_model.py index 3b42ed18..1fb9bad2 100644 --- a/dsp_permissions_scripts/oap/oap_model.py +++ b/dsp_permissions_scripts/oap/oap_model.py @@ -6,6 +6,9 @@ from pydantic import ConfigDict from pydantic import model_validator +from dsp_permissions_scripts.models.errors import OapRetrieveConfigEmptyError +from dsp_permissions_scripts.models.errors import SpecifiedPropsEmptyError +from dsp_permissions_scripts.models.errors import SpecifiedPropsNotEmptyError from dsp_permissions_scripts.models.scope import PermissionScope @@ -59,9 +62,15 @@ class OapRetrieveConfig(BaseModel): specified_props: list[str] = [] @model_validator(mode="after") - def check_consistency(self) -> OapRetrieveConfig: + def check_specified_props(self) -> OapRetrieveConfig: if self.retrieve_values == "specified_props" and not self.specified_props: - raise ValueError("specified_props must not be empty if retrieve_values is 'specified_props'") + raise SpecifiedPropsEmptyError() if self.retrieve_values != "specified_props" and self.specified_props: - raise ValueError("specified_props must be empty if retrieve_values is not 'specified_props'") + raise SpecifiedPropsNotEmptyError() + return self + + @model_validator(mode="after") + def check_config_empty(self) -> OapRetrieveConfig: + if not self.retrieve_resources and self.retrieve_values == "none": + raise OapRetrieveConfigEmptyError() return self diff --git a/tests/test_oap_model.py b/tests/test_oap_model.py index 386aa558..633a717e 100644 --- a/tests/test_oap_model.py +++ b/tests/test_oap_model.py @@ -92,3 +92,7 @@ def test_all_values_but_specified(self) -> None: def test_no_values_but_specified(self) -> None: with pytest.raises(ValueError): OapRetrieveConfig(retrieve_resources=False, retrieve_values="none", specified_props=["foo"]) + + +if __name__ == "__main__": + pytest.main([__file__]) From e524efaa3e67fddfd27a2280926363b5d5b4a68a Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum Date: Wed, 17 Apr 2024 10:12:00 +0200 Subject: [PATCH 08/10] type annotation --- dsp_permissions_scripts/oap/oap_set.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dsp_permissions_scripts/oap/oap_set.py b/dsp_permissions_scripts/oap/oap_set.py index 7ab48c05..d573d208 100644 --- a/dsp_permissions_scripts/oap/oap_set.py +++ b/dsp_permissions_scripts/oap/oap_set.py @@ -128,7 +128,7 @@ def _write_failed_res_iris_to_file( def _launch_thread_pool(oaps: list[Oap], nthreads: int, dsp_client: DspClient) -> list[str]: - all_failed_iris = [] + all_failed_iris: list[str] = [] with ThreadPoolExecutor(max_workers=nthreads) as pool: jobs = [pool.submit(_update_batch, batch, dsp_client) for batch in itertools.batched(oaps, 100)] for result in as_completed(jobs): From e76b0247f7689272e870bbebb6709b9f7b681728 Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum <39048939+jnussbaum@users.noreply.github.com> Date: Wed, 17 Apr 2024 10:16:01 +0200 Subject: [PATCH 09/10] chore: simplify value-PR (#95) --- dsp_permissions_scripts/oap/oap_get.py | 4 ++-- dsp_permissions_scripts/oap/oap_set.py | 23 +++++++++++------------ 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/dsp_permissions_scripts/oap/oap_get.py b/dsp_permissions_scripts/oap/oap_get.py index ffb60d25..1edc0348 100644 --- a/dsp_permissions_scripts/oap/oap_get.py +++ b/dsp_permissions_scripts/oap/oap_get.py @@ -13,7 +13,7 @@ logger = get_logger(__name__) -def _get_all_resource_oaps_of_resclass(resclass_iri: str, project_iri: str, dsp_client: DspClient) -> list[Oap]: +def _get_all_oaps_of_resclass(resclass_iri: str, project_iri: str, dsp_client: DspClient) -> list[Oap]: logger.info(f"Getting all resource OAPs of class {resclass_iri}...") headers = {"X-Knora-Accept-Project": project_iri} all_oaps: list[Oap] = [] @@ -103,7 +103,7 @@ def get_all_resource_oaps_of_project( resclass_iris = [x for x in resclass_iris if x not in excluded_class_iris] all_oaps = [] for resclass_iri in resclass_iris: - oaps = _get_all_resource_oaps_of_resclass(resclass_iri, project_iri, dsp_client) + oaps = _get_all_oaps_of_resclass(resclass_iri, project_iri, dsp_client) all_oaps.extend(oaps) logger.info(f"Retrieved a TOTAL of {len(all_oaps)} OAPs") return all_oaps diff --git a/dsp_permissions_scripts/oap/oap_set.py b/dsp_permissions_scripts/oap/oap_set.py index 9d836409..f4259596 100644 --- a/dsp_permissions_scripts/oap/oap_set.py +++ b/dsp_permissions_scripts/oap/oap_set.py @@ -136,21 +136,21 @@ def _update_permissions_for_resource_and_values( return resource_iri, success -def _write_failed_res_iris_to_file( - failed_res_iris: list[str], +def _write_failed_iris_to_file( + failed_iris: list[str], shortcode: str, host: str, filename: str, ) -> None: with open(filename, "w", encoding="utf-8") as f: f.write(f"Problems occurred while updating the OAPs of these resources (project {shortcode}, host {host}):\n") - f.write("\n".join(failed_res_iris)) + f.write("\n".join(failed_iris)) def _launch_thread_pool(resource_oaps: list[Oap], nthreads: int, dsp_client: DspClient) -> list[str]: counter = 0 total = len(resource_oaps) - failed_res_iris: list[str] = [] + all_failed_iris: list[str] = [] with ThreadPoolExecutor(max_workers=nthreads) as pool: jobs = [ pool.submit( @@ -165,11 +165,11 @@ def _launch_thread_pool(resource_oaps: list[Oap], nthreads: int, dsp_client: Dsp resource_iri, success = result.result() counter += 1 if not success: - failed_res_iris.append(resource_iri) + all_failed_iris.append(resource_iri) logger.info(f"Failed updating resource {counter}/{total} ({resource_iri}) and its values.") else: logger.info(f"Updated resource {counter}/{total} ({resource_iri}) and its values.") - return failed_res_iris + return all_failed_iris def apply_updated_oaps_on_server( @@ -188,19 +188,18 @@ def apply_updated_oaps_on_server( return logger.info(f"******* Updating OAPs of {len(oaps)} resources on {host}... *******") - failed_res_iris = _launch_thread_pool(oaps, nthreads, dsp_client) - - if failed_res_iris: + failed_iris = _launch_thread_pool(oaps, nthreads, dsp_client) + if failed_iris: timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") filename = f"FAILED_RESOURCES_{timestamp}.txt" - _write_failed_res_iris_to_file( - failed_res_iris=failed_res_iris, + _write_failed_iris_to_file( + failed_iris=failed_iris, shortcode=shortcode, host=host, filename=filename, ) msg = ( - f"ERROR: {len(failed_res_iris)} resources could not (or only partially) be updated. " + f"ERROR: {len(failed_iris)} resources could not (or only partially) be updated. " f"They were written to {filename}." ) logger.error(msg) From 0ff6797683a96500ba31cab428e5e1508041cc33 Mon Sep 17 00:00:00 2001 From: Johannes Nussbaum <39048939+jnussbaum@users.noreply.github.com> Date: Wed, 17 Apr 2024 15:53:42 +0200 Subject: [PATCH 10/10] test: migrate to pytest (#96) --- poetry.lock | 16 +- pyproject.toml | 1 + tests/test_ap.py | 19 +- tests/test_ap_serialization.py | 27 +-- tests/test_doap_serialization.py | 18 +- tests/test_helpers.py | 57 +++--- tests/test_oap_serialization.py | 16 +- tests/test_scope.py | 276 ++++++++++++++++-------------- tests/test_scope_serialization.py | 25 +-- 9 files changed, 238 insertions(+), 217 deletions(-) diff --git a/poetry.lock b/poetry.lock index 7c204bec..4fa5341b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -466,6 +466,20 @@ pluggy = ">=1.4,<2.0" [package.extras] testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-unordered" +version = "0.6.0" +description = "Test equality of unordered collections in pytest" +optional = false +python-versions = "*" +files = [ + {file = "pytest-unordered-0.6.0.tar.gz", hash = "sha256:f61b4f6e06a60a92db50968954efac93e2f584290a49f53ad135e3f32f57e02a"}, + {file = "pytest_unordered-0.6.0-py3-none-any.whl", hash = "sha256:86675cade320d1c54864fedd10c2277f685bf2d958ef744c578b3d622000f2ac"}, +] + +[package.dependencies] +pytest = ">=7.0.0" + [[package]] name = "python-dotenv" version = "1.0.1" @@ -657,4 +671,4 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [metadata] lock-version = "2.0" python-versions = "^3.12.0" -content-hash = "eced2fcc562d11991fade44a74f72135337b5d9136f090b57df52ba08472a9d7" +content-hash = "04caee752659b7b49ae4d3d429cf17e68c3070bdb639c6b28f150ab685501a7b" diff --git a/pyproject.toml b/pyproject.toml index b6226883..7905e24a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,6 +19,7 @@ python-dotenv = "^1.0.1" ruff = "^0.3.7" pre-commit = "^3.7.0" pytest = "^8.1.1" +pytest-unordered = "^0.6.0" [build-system] build-backend = "poetry.core.masonry.api" diff --git a/tests/test_ap.py b/tests/test_ap.py index 5fba5cff..1d534c5c 100644 --- a/tests/test_ap.py +++ b/tests/test_ap.py @@ -1,14 +1,11 @@ -import unittest +import pytest from dsp_permissions_scripts.ap.ap_model import Ap from dsp_permissions_scripts.ap.ap_model import ApValue from dsp_permissions_scripts.models import group -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) -# ruff: noqa: PT027 (pytest-unittest-raises-assertion) (remove this line when pytest is used instead of unittest) - -class TestAp(unittest.TestCase): +class TestAp: ap = Ap( forGroup=group.UNKNOWN_USER, forProject="http://rdfh.ch/projects/0001", @@ -18,16 +15,20 @@ class TestAp(unittest.TestCase): def test_add_permission(self) -> None: self.ap.add_permission(ApValue.ProjectAdminRightsAllPermission) - self.assertIn(ApValue.ProjectAdminRightsAllPermission, self.ap.hasPermissions) + assert ApValue.ProjectAdminRightsAllPermission in self.ap.hasPermissions def test_add_permission_already_exists(self) -> None: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 (exception too broad) self.ap.add_permission(ApValue.ProjectAdminGroupAllPermission) def test_remove_permission(self) -> None: self.ap.remove_permission(ApValue.ProjectAdminGroupAllPermission) - self.assertNotIn(ApValue.ProjectAdminGroupAllPermission, self.ap.hasPermissions) + assert ApValue.ProjectAdminGroupAllPermission not in self.ap.hasPermissions def test_remove_permission_not_exists(self) -> None: - with self.assertRaises(ValueError): + with pytest.raises(ValueError): # noqa: PT011 (exception too broad) self.ap.remove_permission(ApValue.ProjectAdminAllPermission) + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/tests/test_ap_serialization.py b/tests/test_ap_serialization.py index 2e33ea60..2596ac92 100644 --- a/tests/test_ap_serialization.py +++ b/tests/test_ap_serialization.py @@ -1,7 +1,10 @@ import json +import re import shutil -import unittest from pathlib import Path +from typing import Iterator + +import pytest from dsp_permissions_scripts.ap.ap_model import Ap from dsp_permissions_scripts.ap.ap_model import ApValue @@ -10,10 +13,8 @@ from dsp_permissions_scripts.models import group from dsp_permissions_scripts.models.host import Hosts -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) - -class TestApSerialization(unittest.TestCase): +class TestApSerialization: shortcode = "1234" project_iri = "http://rdfh.ch/projects/MsOaiQkcQ7-QPxsYBKckfQ" testdata_file = Path("testdata/APs_1234_serialized.json") @@ -34,10 +35,10 @@ class TestApSerialization(unittest.TestCase): iri="http://rdfh.ch/ap-2", ) - def setUp(self) -> None: + @pytest.fixture(autouse=True) + def _setup_teardown(self) -> Iterator[None]: self.output_dir.mkdir(parents=True, exist_ok=True) - - def tearDown(self) -> None: + yield if self.output_dir.is_dir(): shutil.rmtree(self.output_dir) @@ -51,10 +52,10 @@ def test_serialize_aps_of_project(self) -> None: with open(self.output_file, mode="r", encoding="utf-8") as f: aps_file = json.load(f) explanation_text = next(iter(aps_file.keys())) - self.assertRegex(explanation_text, r"Project 1234 on host .+ has \d+ APs") + assert re.search(r"Project 1234 on host .+ has \d+ APs", explanation_text) aps_as_dicts = aps_file[explanation_text] - self.assertEqual(self.ap1, Ap.model_validate(aps_as_dicts[0])) - self.assertEqual(self.ap2, Ap.model_validate(aps_as_dicts[1])) + assert self.ap1 == Ap.model_validate(aps_as_dicts[0]) + assert self.ap2 == Ap.model_validate(aps_as_dicts[1]) def test_deserialize_aps_of_project(self) -> None: shutil.copy(src=self.testdata_file, dst=self.output_file) @@ -62,9 +63,9 @@ def test_deserialize_aps_of_project(self) -> None: shortcode=self.shortcode, mode="original", ) - self.assertEqual(self.ap1, aps[0]) - self.assertEqual(self.ap2, aps[1]) + assert self.ap1 == aps[0] + assert self.ap2 == aps[1] if __name__ == "__main__": - unittest.main() + pytest.main([__file__]) diff --git a/tests/test_doap_serialization.py b/tests/test_doap_serialization.py index d0a9b5c0..e2126c9c 100644 --- a/tests/test_doap_serialization.py +++ b/tests/test_doap_serialization.py @@ -1,6 +1,8 @@ import shutil -import unittest from pathlib import Path +from typing import Iterator + +import pytest from dsp_permissions_scripts.doap.doap_model import Doap from dsp_permissions_scripts.doap.doap_model import DoapTarget @@ -11,13 +13,13 @@ from dsp_permissions_scripts.models.scope import PermissionScope from tests.test_scope_serialization import compare_scopes -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) - -class TestDoapSerialization(unittest.TestCase): +class TestDoapSerialization: shortcode = "1234" - def tearDown(self) -> None: + @pytest.fixture(autouse=True) + def _setup_teardown(self) -> Iterator[None]: + yield testdata_dir = Path(f"project_data/{self.shortcode}") if testdata_dir.is_dir(): shutil.rmtree(testdata_dir) @@ -59,10 +61,10 @@ def test_doap_serialization(self) -> None: self._compare_doaps(deserialized_doaps[1], doap2) def _compare_doaps(self, doap1: Doap, doap2: Doap) -> None: - self.assertEqual(doap1.target, doap2.target) + assert doap1.target == doap2.target compare_scopes(doap1.scope, doap2.scope) - self.assertEqual(doap1.doap_iri, doap2.doap_iri) + assert doap1.doap_iri == doap2.doap_iri if __name__ == "__main__": - unittest.main() + pytest.main([__file__]) diff --git a/tests/test_helpers.py b/tests/test_helpers.py index 83733c06..19499c3c 100644 --- a/tests/test_helpers.py +++ b/tests/test_helpers.py @@ -1,38 +1,35 @@ -import unittest +import pytest from dsp_permissions_scripts.models import group from dsp_permissions_scripts.utils.helpers import sort_groups -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) - -class TestHelpers(unittest.TestCase): - def test_sort_groups(self) -> None: - groups_original = [ - group.Group(val="http://www.knora.org/ontology/knora-admin#C_CustomGroup"), - group.UNKNOWN_USER, - group.PROJECT_ADMIN, - group.PROJECT_MEMBER, - group.CREATOR, - group.Group(val="http://www.knora.org/ontology/knora-admin#A_CustomGroup"), - group.Group(val="http://www.knora.org/ontology/knora-admin#B_CustomGroup"), - group.KNOWN_USER, - group.SYSTEM_ADMIN, - ] - groups_expected = [ - group.SYSTEM_ADMIN, - group.CREATOR, - group.PROJECT_ADMIN, - group.PROJECT_MEMBER, - group.KNOWN_USER, - group.UNKNOWN_USER, - group.Group(val="http://www.knora.org/ontology/knora-admin#A_CustomGroup"), - group.Group(val="http://www.knora.org/ontology/knora-admin#B_CustomGroup"), - group.Group(val="http://www.knora.org/ontology/knora-admin#C_CustomGroup"), - ] - groups_returned = sort_groups(groups_original) - self.assertEqual(groups_returned, groups_expected) +def test_sort_groups() -> None: + groups_original = [ + group.Group(val="http://www.knora.org/ontology/knora-admin#C_CustomGroup"), + group.UNKNOWN_USER, + group.PROJECT_ADMIN, + group.PROJECT_MEMBER, + group.CREATOR, + group.Group(val="http://www.knora.org/ontology/knora-admin#A_CustomGroup"), + group.Group(val="http://www.knora.org/ontology/knora-admin#B_CustomGroup"), + group.KNOWN_USER, + group.SYSTEM_ADMIN, + ] + groups_expected = [ + group.SYSTEM_ADMIN, + group.CREATOR, + group.PROJECT_ADMIN, + group.PROJECT_MEMBER, + group.KNOWN_USER, + group.UNKNOWN_USER, + group.Group(val="http://www.knora.org/ontology/knora-admin#A_CustomGroup"), + group.Group(val="http://www.knora.org/ontology/knora-admin#B_CustomGroup"), + group.Group(val="http://www.knora.org/ontology/knora-admin#C_CustomGroup"), + ] + groups_returned = sort_groups(groups_original) + assert groups_returned == groups_expected if __name__ == "__main__": - unittest.main() + pytest.main([__file__]) diff --git a/tests/test_oap_serialization.py b/tests/test_oap_serialization.py index 6960be78..ce0a20c3 100644 --- a/tests/test_oap_serialization.py +++ b/tests/test_oap_serialization.py @@ -1,6 +1,8 @@ import shutil -import unittest from pathlib import Path +from typing import Iterator + +import pytest from dsp_permissions_scripts.models import group from dsp_permissions_scripts.models.scope import PermissionScope @@ -9,13 +11,13 @@ from dsp_permissions_scripts.oap.oap_serialize import serialize_oaps from tests.test_scope_serialization import compare_scopes -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) - -class TestOapSerialization(unittest.TestCase): +class TestOapSerialization: shortcode = "1234" - def tearDown(self) -> None: + @pytest.fixture(autouse=True) + def _setup_teardown(self) -> Iterator[None]: + yield testdata_dir = Path(f"project_data/{self.shortcode}") if testdata_dir.is_dir(): shutil.rmtree(testdata_dir) @@ -50,8 +52,8 @@ def test_oap_serialization(self) -> None: def _compare_oaps(self, oap1: Oap, oap2: Oap) -> None: compare_scopes(oap1.scope, oap2.scope) - self.assertEqual(oap1.object_iri, oap2.object_iri) + assert oap1.object_iri == oap2.object_iri if __name__ == "__main__": - unittest.main() + pytest.main([__file__]) diff --git a/tests/test_scope.py b/tests/test_scope.py index 157cd9ba..8e041012 100644 --- a/tests/test_scope.py +++ b/tests/test_scope.py @@ -1,151 +1,161 @@ -import unittest +import re + +import pytest from dsp_permissions_scripts.models import group from dsp_permissions_scripts.models.scope import PermissionScope from tests.test_scope_serialization import compare_scopes -# ruff: noqa: PT027 (pytest-unittest-raises-assertion) (remove this line when pytest is used instead of unittest) -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) - - -class TestScope(unittest.TestCase): - def test_scope_validation_on_creation(self) -> None: - with self.assertRaisesRegex(ValueError, "must not occur in more than one field"): - PermissionScope.create( - CR={group.PROJECT_ADMIN}, - D={group.PROJECT_ADMIN}, - V={group.UNKNOWN_USER, group.KNOWN_USER}, - ) - def test_scope_validation_on_add_to_same_permission(self) -> None: - scope = PermissionScope.create( - CR={group.PROJECT_ADMIN}, - V={group.UNKNOWN_USER, group.KNOWN_USER}, - ) - rgx = "Group 'val='http://www.knora.org/ontology/knora-admin#ProjectAdmin'' is already in permission 'CR'" - with self.assertRaisesRegex(ValueError, rgx): - _ = scope.add("CR", group.PROJECT_ADMIN) - - def test_scope_validation_on_add_to_different_permission(self) -> None: - scope = PermissionScope.create( +def test_scope_validation_on_creation() -> None: + with pytest.raises(ValueError, match=re.escape("must not occur in more than one field")): + PermissionScope.create( CR={group.PROJECT_ADMIN}, + D={group.PROJECT_ADMIN}, V={group.UNKNOWN_USER, group.KNOWN_USER}, ) - with self.assertRaisesRegex(ValueError, "must not occur in more than one field"): - _ = scope.add("RV", group.PROJECT_ADMIN) - def test_add_to_scope(self) -> None: - scope = PermissionScope.create( - D={group.SYSTEM_ADMIN}, - M={group.PROJECT_MEMBER, group.KNOWN_USER}, - ) - scope_added = scope.add("CR", group.PROJECT_ADMIN) - compare_scopes( - scope1=scope_added, - scope2=PermissionScope.create( - CR={group.PROJECT_ADMIN}, - D={group.SYSTEM_ADMIN}, - M={group.PROJECT_MEMBER, group.KNOWN_USER}, - ), - ) - def test_remove_inexisting_group(self) -> None: - scope = PermissionScope.create( +def test_scope_validation_on_add_to_same_permission() -> None: + scope = PermissionScope.create( + CR={group.PROJECT_ADMIN}, + V={group.UNKNOWN_USER, group.KNOWN_USER}, + ) + rgx = "Group 'val='http://www.knora.org/ontology/knora-admin#ProjectAdmin'' is already in permission 'CR'" + with pytest.raises(ValueError, match=re.escape(rgx)): + _ = scope.add("CR", group.PROJECT_ADMIN) + + +def test_scope_validation_on_add_to_different_permission() -> None: + scope = PermissionScope.create( + CR={group.PROJECT_ADMIN}, + V={group.UNKNOWN_USER, group.KNOWN_USER}, + ) + with pytest.raises(ValueError, match=re.escape("must not occur in more than one field")): + _ = scope.add("RV", group.PROJECT_ADMIN) + + +def test_add_to_scope() -> None: + scope = PermissionScope.create( + D={group.SYSTEM_ADMIN}, + M={group.PROJECT_MEMBER, group.KNOWN_USER}, + ) + scope_added = scope.add("CR", group.PROJECT_ADMIN) + compare_scopes( + scope1=scope_added, + scope2=PermissionScope.create( + CR={group.PROJECT_ADMIN}, D={group.SYSTEM_ADMIN}, M={group.PROJECT_MEMBER, group.KNOWN_USER}, - ) - with self.assertRaisesRegex(ValueError, "is not in permission 'D'"): - _ = scope.remove("D", group.UNKNOWN_USER) - - def test_remove_from_empty_perm(self) -> None: - scope = PermissionScope.create( - D={group.PROJECT_ADMIN}, - V={group.PROJECT_MEMBER, group.UNKNOWN_USER}, - ) - with self.assertRaisesRegex(ValueError, "is not in permission 'CR'"): - _ = scope.remove("CR", group.PROJECT_ADMIN) - - def test_remove_from_scope(self) -> None: - scope = PermissionScope.create( - CR={group.PROJECT_ADMIN}, + ), + ) + + +def test_remove_inexisting_group() -> None: + scope = PermissionScope.create( + D={group.SYSTEM_ADMIN}, + M={group.PROJECT_MEMBER, group.KNOWN_USER}, + ) + with pytest.raises(ValueError, match=re.escape("is not in permission 'D'")): + _ = scope.remove("D", group.UNKNOWN_USER) + + +def test_remove_from_empty_perm() -> None: + scope = PermissionScope.create( + D={group.PROJECT_ADMIN}, + V={group.PROJECT_MEMBER, group.UNKNOWN_USER}, + ) + with pytest.raises(ValueError, match=re.escape("is not in permission 'CR'")): + _ = scope.remove("CR", group.PROJECT_ADMIN) + + +def test_remove_from_scope() -> None: + scope = PermissionScope.create( + CR={group.PROJECT_ADMIN}, + D={group.SYSTEM_ADMIN}, + M={group.PROJECT_MEMBER, group.KNOWN_USER}, + ) + scope_removed = scope.remove("CR", group.PROJECT_ADMIN) + compare_scopes( + scope1=scope_removed, + scope2=PermissionScope.create( D={group.SYSTEM_ADMIN}, M={group.PROJECT_MEMBER, group.KNOWN_USER}, - ) - scope_removed = scope.remove("CR", group.PROJECT_ADMIN) - compare_scopes( - scope1=scope_removed, - scope2=PermissionScope.create( - D={group.SYSTEM_ADMIN}, - M={group.PROJECT_MEMBER, group.KNOWN_USER}, - ), - ) - - def test_remove_duplicates_from_kwargs_CR(self) -> None: - original: dict[str, list[str]] = { - "CR": ["knora-admin:ProjectAdmin"], - "D": ["knora-admin:ProjectAdmin"], - "M": ["knora-admin:ProjectAdmin"], - "V": ["knora-admin:ProjectAdmin"], - "RV": ["knora-admin:ProjectAdmin"], - } - expected = {"CR": ["knora-admin:ProjectAdmin"], "D": [], "M": [], "V": [], "RV": []} - self.assertDictEqual(expected, PermissionScope._remove_duplicates_from_kwargs(original)) - - def test_remove_duplicates_from_kwargs_D(self) -> None: - original: dict[str, list[str]] = { - "CR": [], - "D": ["knora-admin:ProjectAdmin"], - "M": ["knora-admin:ProjectAdmin"], - "V": ["knora-admin:ProjectAdmin"], - "RV": ["knora-admin:ProjectAdmin"], - } - expected = {"CR": [], "D": ["knora-admin:ProjectAdmin"], "M": [], "V": [], "RV": []} - self.assertDictEqual(expected, PermissionScope._remove_duplicates_from_kwargs(original)) - - def test_remove_duplicates_from_kwargs_RV(self) -> None: - original: dict[str, list[str]] = {"CR": [], "D": [], "M": [], "V": [], "RV": ["knora-admin:ProjectAdmin"]} - expected: dict[str, list[str]] = {"CR": [], "D": [], "M": [], "V": [], "RV": ["knora-admin:ProjectAdmin"]} - self.assertDictEqual(expected, PermissionScope._remove_duplicates_from_kwargs(original)) - - def test_remove_duplicates_from_kwargs_mixed(self) -> None: - original: dict[str, list[str]] = { - "CR": ["knora-admin:ProjectAdmin"], - "D": ["knora-admin:ProjectMember"], - "M": ["knora-admin:ProjectMember"], - "V": ["knora-admin:ProjectMember"], - "RV": ["knora-admin:ProjectMember"], - } - expected = {"CR": ["knora-admin:ProjectAdmin"], "D": ["knora-admin:ProjectMember"], "M": [], "V": [], "RV": []} - self.assertDictEqual(expected, PermissionScope._remove_duplicates_from_kwargs(original)) - - def test_remove_duplicates_from_kwargs_mixed_M(self) -> None: - original: dict[str, list[str]] = { - "CR": ["knora-admin:ProjectAdmin"], - "D": [], - "M": ["knora-admin:ProjectMember"], - "V": ["knora-admin:ProjectMember"], - "RV": ["knora-admin:ProjectMember"], - } - expected = {"CR": ["knora-admin:ProjectAdmin"], "D": [], "M": ["knora-admin:ProjectMember"], "V": [], "RV": []} - self.assertDictEqual(expected, PermissionScope._remove_duplicates_from_kwargs(original)) - - def test_remove_duplicates_from_kwargs_mixed_and_multiple(self) -> None: - original: dict[str, list[str]] = { - "CR": ["knora-admin:ProjectAdmin"], - "D": [], - "M": ["knora-admin:ProjectMember", "knora-admin:ProjectAdmin", "knora-admin:KnownUser"], - "V": ["knora-admin:ProjectMember", "knora-admin:ProjectAdmin"], - "RV": ["knora-admin:ProjectMember", "knora-admin:KnownUser"], - } - expected = { - "CR": ["knora-admin:ProjectAdmin"], - "D": [], - "M": ["knora-admin:ProjectMember", "knora-admin:KnownUser"], - "V": [], - "RV": [], - } - self.assertDictEqual(expected, PermissionScope._remove_duplicates_from_kwargs(original)) + ), + ) + + +def test_remove_duplicates_from_kwargs_CR() -> None: + original: dict[str, list[str]] = { + "CR": ["knora-admin:ProjectAdmin"], + "D": ["knora-admin:ProjectAdmin"], + "M": ["knora-admin:ProjectAdmin"], + "V": ["knora-admin:ProjectAdmin"], + "RV": ["knora-admin:ProjectAdmin"], + } + expected = {"CR": ["knora-admin:ProjectAdmin"], "D": [], "M": [], "V": [], "RV": []} + assert expected == PermissionScope._remove_duplicates_from_kwargs(original) + + +def test_remove_duplicates_from_kwargs_D() -> None: + original: dict[str, list[str]] = { + "CR": [], + "D": ["knora-admin:ProjectAdmin"], + "M": ["knora-admin:ProjectAdmin"], + "V": ["knora-admin:ProjectAdmin"], + "RV": ["knora-admin:ProjectAdmin"], + } + expected = {"CR": [], "D": ["knora-admin:ProjectAdmin"], "M": [], "V": [], "RV": []} + assert expected == PermissionScope._remove_duplicates_from_kwargs(original) + + +def test_remove_duplicates_from_kwargs_RV() -> None: + original: dict[str, list[str]] = {"CR": [], "D": [], "M": [], "V": [], "RV": ["knora-admin:ProjectAdmin"]} + expected: dict[str, list[str]] = {"CR": [], "D": [], "M": [], "V": [], "RV": ["knora-admin:ProjectAdmin"]} + assert expected == PermissionScope._remove_duplicates_from_kwargs(original) + + +def test_remove_duplicates_from_kwargs_mixed() -> None: + original: dict[str, list[str]] = { + "CR": ["knora-admin:ProjectAdmin"], + "D": ["knora-admin:ProjectMember"], + "M": ["knora-admin:ProjectMember"], + "V": ["knora-admin:ProjectMember"], + "RV": ["knora-admin:ProjectMember"], + } + expected = {"CR": ["knora-admin:ProjectAdmin"], "D": ["knora-admin:ProjectMember"], "M": [], "V": [], "RV": []} + assert expected == PermissionScope._remove_duplicates_from_kwargs(original) + + +def test_remove_duplicates_from_kwargs_mixed_M() -> None: + original: dict[str, list[str]] = { + "CR": ["knora-admin:ProjectAdmin"], + "D": [], + "M": ["knora-admin:ProjectMember"], + "V": ["knora-admin:ProjectMember"], + "RV": ["knora-admin:ProjectMember"], + } + expected = {"CR": ["knora-admin:ProjectAdmin"], "D": [], "M": ["knora-admin:ProjectMember"], "V": [], "RV": []} + assert expected == PermissionScope._remove_duplicates_from_kwargs(original) + + +def test_remove_duplicates_from_kwargs_mixed_and_multiple() -> None: + original: dict[str, list[str]] = { + "CR": ["knora-admin:ProjectAdmin"], + "D": [], + "M": ["knora-admin:ProjectMember", "knora-admin:ProjectAdmin", "knora-admin:KnownUser"], + "V": ["knora-admin:ProjectMember", "knora-admin:ProjectAdmin"], + "RV": ["knora-admin:ProjectMember", "knora-admin:KnownUser"], + } + expected = { + "CR": ["knora-admin:ProjectAdmin"], + "D": [], + "M": ["knora-admin:ProjectMember", "knora-admin:KnownUser"], + "V": [], + "RV": [], + } + assert expected == PermissionScope._remove_duplicates_from_kwargs(original) if __name__ == "__main__": - unittest.main() + pytest.main([__file__]) diff --git a/tests/test_scope_serialization.py b/tests/test_scope_serialization.py index 1465156d..bb0ddae7 100644 --- a/tests/test_scope_serialization.py +++ b/tests/test_scope_serialization.py @@ -1,6 +1,8 @@ -import unittest from typing import Any +import pytest +from pytest_unordered import unordered + from dsp_permissions_scripts.models import group from dsp_permissions_scripts.models.scope import PermissionScope from dsp_permissions_scripts.utils.scope_serialization import create_admin_route_object_from_scope @@ -8,8 +10,6 @@ from dsp_permissions_scripts.utils.scope_serialization import create_scope_from_string from dsp_permissions_scripts.utils.scope_serialization import create_string_from_scope -# ruff: noqa: PT009 (pytest-unittest-assertion) (remove this line when pytest is used instead of unittest) - def compare_scopes( scope1: PermissionScope, @@ -20,10 +20,10 @@ def compare_scopes( scope1_dict = {k: sorted(v, key=lambda x: x["val"]) for k, v in scope1_dict.items()} scope2_dict = scope2.model_dump(mode="json") scope2_dict = {k: sorted(v, key=lambda x: x["val"]) for k, v in scope2_dict.items()} - unittest.TestCase().assertDictEqual(scope1_dict, scope2_dict, msg=msg) + assert scope1_dict == scope2_dict, msg -class TestScopeSerialization(unittest.TestCase): +class TestScopeSerialization: perm_strings = ( "CR knora-admin:SystemAdmin|V knora-admin:CustomGroup", "D knora-admin:ProjectAdmin|RV knora-admin:ProjectMember", @@ -92,20 +92,13 @@ def test_create_scope_from_admin_route_object(self) -> None: def test_create_string_from_scope(self) -> None: for perm_string, scope in zip(self.perm_strings, self.scopes): perm_string_full = self._resolve_prefixes_of_perm_string(perm_string) - self.assertEqual( - create_string_from_scope(scope), - perm_string_full, - msg=f"Failed with permission string '{perm_string}'", - ) + assert create_string_from_scope(scope) == perm_string_full, f"Failed with permission string '{perm_string}'" def test_create_admin_route_object_from_scope(self) -> None: for admin_route_object, scope, index in zip(self.admin_route_objects, self.scopes, range(len(self.scopes))): admin_route_object_full = self._resolve_prefixes_of_admin_route_object(admin_route_object) - self.assertCountEqual( - create_admin_route_object_from_scope(scope), - admin_route_object_full, - msg=f"Failed with admin group object no. {index}", - ) + returned = create_admin_route_object_from_scope(scope) + assert unordered(returned) == admin_route_object_full, f"Failed with admin group object no. {index}" def _resolve_prefixes_of_admin_route_object(self, admin_route_object: list[dict[str, Any]]) -> list[dict[str, Any]]: for obj in admin_route_object: @@ -119,4 +112,4 @@ def _resolve_prefixes_of_perm_string(self, perm_string: str) -> str: if __name__ == "__main__": - unittest.main() + pytest.main([__file__])