diff --git a/.github/workflows/api-tests.yaml b/.github/workflows/api-tests.yaml index c0ad6d09..95255c4e 100644 --- a/.github/workflows/api-tests.yaml +++ b/.github/workflows/api-tests.yaml @@ -42,6 +42,13 @@ jobs: - name: Run security tests with coverage run: | cd apps/api - uv run pytest src/tests/security/ --cov=src.security --cov-report=xml --cov-report=term-missing + uv run pytest src/tests/security/ --cov=src.security --cov-report=html --cov-report=term-missing env: TESTING: "true" + + - name: Upload coverage report to GitHub + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: apps/api/htmlcov/ + retention-days: 30 diff --git a/apps/api/pyproject.toml b/apps/api/pyproject.toml index f42eb622..edf9ab67 100644 --- a/apps/api/pyproject.toml +++ b/apps/api/pyproject.toml @@ -21,6 +21,7 @@ dependencies = [ "psycopg2-binary>=2.9.9", "pydantic[email]>=1.8.0,<2.0.0", "pytest>=8.2.2", + "pytest-cov>=4.1.0", "python-dotenv>=1.0.0", "python-multipart>=0.0.9", "pyyaml>=6.0.1", diff --git a/apps/api/src/tests/security/test_auth.py b/apps/api/src/tests/security/test_auth.py index 101eced9..c972c2c4 100644 --- a/apps/api/src/tests/security/test_auth.py +++ b/apps/api/src/tests/security/test_auth.py @@ -11,7 +11,6 @@ from src.security.auth import ( Token, TokenData, Settings, - get_config, ) from src.db.users import User, AnonymousUser, PublicUser from datetime import datetime, timedelta, timezone diff --git a/apps/api/src/tests/security/test_features_utils.py b/apps/api/src/tests/security/test_features_utils.py index 93d9a948..81233244 100644 --- a/apps/api/src/tests/security/test_features_utils.py +++ b/apps/api/src/tests/security/test_features_utils.py @@ -1,12 +1,11 @@ import pytest -from unittest.mock import Mock, patch, MagicMock +from unittest.mock import Mock, patch from fastapi import HTTPException -from sqlmodel import Session, select +from sqlmodel import Session from src.security.features_utils.usage import ( check_limits_with_usage, increase_feature_usage, decrease_feature_usage, - FeatureSet, ) from src.db.organization_config import OrganizationConfig diff --git a/apps/api/src/tests/security/test_rbac.py b/apps/api/src/tests/security/test_rbac.py index bb851f4f..450b510c 100644 --- a/apps/api/src/tests/security/test_rbac.py +++ b/apps/api/src/tests/security/test_rbac.py @@ -1,7 +1,7 @@ import pytest from unittest.mock import Mock, AsyncMock, patch from fastapi import HTTPException, Request -from sqlmodel import Session, select +from sqlmodel import Session from src.security.rbac.rbac import ( authorization_verify_if_element_is_public, authorization_verify_if_user_is_author, @@ -14,7 +14,6 @@ from src.db.courses.courses import Course from src.db.collections import Collection from src.db.resource_authors import ResourceAuthor, ResourceAuthorshipEnum, ResourceAuthorshipStatusEnum from src.db.roles import Role -from src.db.user_organizations import UserOrganization class TestRBAC: diff --git a/apps/api/src/tests/security/test_rbac_utils.py b/apps/api/src/tests/security/test_rbac_utils.py index b293618f..23aaa9e9 100644 --- a/apps/api/src/tests/security/test_rbac_utils.py +++ b/apps/api/src/tests/security/test_rbac_utils.py @@ -1,5 +1,4 @@ import pytest -from unittest.mock import AsyncMock, patch from fastapi import HTTPException from src.security.rbac.utils import ( check_element_type, diff --git a/apps/api/src/tests/security/test_security.py b/apps/api/src/tests/security/test_security.py index 8128f004..34ba4ca8 100644 --- a/apps/api/src/tests/security/test_security.py +++ b/apps/api/src/tests/security/test_security.py @@ -1,4 +1,3 @@ -import pytest from src.security.security import ( security_hash_password, security_verify_password, diff --git a/apps/api/src/tests/security/test_security_all.py b/apps/api/src/tests/security/test_security_all.py index 4803c4b4..fefecd7b 100644 --- a/apps/api/src/tests/security/test_security_all.py +++ b/apps/api/src/tests/security/test_security_all.py @@ -10,7 +10,6 @@ of the security functionality including: - Authorization utilities """ -import pytest from src.tests.security.test_security import TestSecurity from src.tests.security.test_auth import TestAuth from src.tests.security.test_rbac import TestRBAC @@ -24,49 +23,14 @@ class TestSecurityComprehensive: def test_security_module_imports(self): """Test that all security modules can be imported successfully""" # Test core security imports - from src.security.security import ( - security_hash_password, - security_verify_password, - ACCESS_TOKEN_EXPIRE_MINUTES, - SECRET_KEY, - ALGORITHM, - ) # Test auth imports - from src.security.auth import ( - authenticate_user, - create_access_token, - get_current_user, - non_public_endpoint, - Token, - TokenData, - Settings, - ) # Test RBAC imports - from src.security.rbac.rbac import ( - authorization_verify_if_element_is_public, - authorization_verify_if_user_is_author, - authorization_verify_based_on_roles, - authorization_verify_based_on_org_admin_status, - authorization_verify_based_on_roles_and_authorship, - authorization_verify_if_user_is_anon, - ) # Test RBAC utils imports - from src.security.rbac.utils import ( - check_element_type, - get_singular_form_of_element, - get_id_identifier_of_element, - ) # Test features utils imports - from src.security.features_utils.usage import ( - check_limits_with_usage, - increase_feature_usage, - decrease_feature_usage, - FeatureSet, - ) # Verify all imports succeeded assert True @@ -83,7 +47,6 @@ class TestSecurityComprehensive: def test_feature_set_definition(self): """Test that FeatureSet includes all expected features""" - from src.security.features_utils.usage import FeatureSet expected_features = [ "ai", "analytics", "api", "assignments", "collaboration", diff --git a/apps/api/uv.lock b/apps/api/uv.lock index 0de10404..379f7613 100644 --- a/apps/api/uv.lock +++ b/apps/api/uv.lock @@ -339,6 +339,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 }, ] +[[package]] +name = "coverage" +version = "7.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/0e/66dbd4c6a7f0758a8d18044c048779ba21fb94856e1edcf764bd5403e710/coverage-7.10.1.tar.gz", hash = "sha256:ae2b4856f29ddfe827106794f3589949a57da6f0d38ab01e24ec35107979ba57", size = 819938 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/3f/b051feeb292400bd22d071fdf933b3ad389a8cef5c80c7866ed0c7414b9e/coverage-7.10.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6b7dc7f0a75a7eaa4584e5843c873c561b12602439d2351ee28c7478186c4da4", size = 214934 }, + { url = "https://files.pythonhosted.org/packages/f8/e4/a61b27d5c4c2d185bdfb0bfe9d15ab4ac4f0073032665544507429ae60eb/coverage-7.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:607f82389f0ecafc565813aa201a5cade04f897603750028dd660fb01797265e", size = 215173 }, + { url = "https://files.pythonhosted.org/packages/8a/01/40a6ee05b60d02d0bc53742ad4966e39dccd450aafb48c535a64390a3552/coverage-7.10.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f7da31a1ba31f1c1d4d5044b7c5813878adae1f3af8f4052d679cc493c7328f4", size = 246190 }, + { url = "https://files.pythonhosted.org/packages/11/ef/a28d64d702eb583c377255047281305dc5a5cfbfb0ee36e721f78255adb6/coverage-7.10.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51fe93f3fe4f5d8483d51072fddc65e717a175490804e1942c975a68e04bf97a", size = 248618 }, + { url = "https://files.pythonhosted.org/packages/6a/ad/73d018bb0c8317725370c79d69b5c6e0257df84a3b9b781bda27a438a3be/coverage-7.10.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3e59d00830da411a1feef6ac828b90bbf74c9b6a8e87b8ca37964925bba76dbe", size = 250081 }, + { url = "https://files.pythonhosted.org/packages/2d/dd/496adfbbb4503ebca5d5b2de8bed5ec00c0a76558ffc5b834fd404166bc9/coverage-7.10.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:924563481c27941229cb4e16eefacc35da28563e80791b3ddc5597b062a5c386", size = 247990 }, + { url = "https://files.pythonhosted.org/packages/18/3c/a9331a7982facfac0d98a4a87b36ae666fe4257d0f00961a3a9ef73e015d/coverage-7.10.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:ca79146ee421b259f8131f153102220b84d1a5e6fb9c8aed13b3badfd1796de6", size = 246191 }, + { url = "https://files.pythonhosted.org/packages/62/0c/75345895013b83f7afe92ec595e15a9a525ede17491677ceebb2ba5c3d85/coverage-7.10.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2b225a06d227f23f386fdc0eab471506d9e644be699424814acc7d114595495f", size = 247400 }, + { url = "https://files.pythonhosted.org/packages/e2/a9/98b268cfc5619ef9df1d5d34fee408ecb1542d9fd43d467e5c2f28668cd4/coverage-7.10.1-cp312-cp312-win32.whl", hash = "sha256:5ba9a8770effec5baaaab1567be916c87d8eea0c9ad11253722d86874d885eca", size = 217338 }, + { url = "https://files.pythonhosted.org/packages/fe/31/22a5440e4d1451f253c5cd69fdcead65e92ef08cd4ec237b8756dc0b20a7/coverage-7.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:9eb245a8d8dd0ad73b4062135a251ec55086fbc2c42e0eb9725a9b553fba18a3", size = 218125 }, + { url = "https://files.pythonhosted.org/packages/d6/2b/40d9f0ce7ee839f08a43c5bfc9d05cec28aaa7c9785837247f96cbe490b9/coverage-7.10.1-cp312-cp312-win_arm64.whl", hash = "sha256:7718060dd4434cc719803a5e526838a5d66e4efa5dc46d2b25c21965a9c6fcc4", size = 216523 }, + { url = "https://files.pythonhosted.org/packages/0f/64/922899cff2c0fd3496be83fa8b81230f5a8d82a2ad30f98370b133c2c83b/coverage-7.10.1-py3-none-any.whl", hash = "sha256:fa2a258aa6bf188eb9a8948f7102a83da7c430a0dce918dbd8b60ef8fcb772d7", size = 206597 }, +] + [[package]] name = "dataclasses-json" version = "0.6.7" @@ -1033,6 +1053,7 @@ dependencies = [ { name = "pydantic", extra = ["email"] }, { name = "pytest" }, { name = "pytest-asyncio" }, + { name = "pytest-cov" }, { name = "python-dotenv" }, { name = "python-jose" }, { name = "python-multipart" }, @@ -1070,6 +1091,7 @@ requires-dist = [ { name = "pydantic", extras = ["email"], specifier = ">=1.8.0,<2.0.0" }, { name = "pytest", specifier = ">=8.2.2" }, { name = "pytest-asyncio", specifier = ">=1.1.0" }, + { name = "pytest-cov", specifier = ">=4.1.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, { name = "python-jose", specifier = ">=3.3.0" }, { name = "python-multipart", specifier = ">=0.0.9" }, @@ -1728,6 +1750,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157 }, ] +[[package]] +name = "pytest-cov" +version = "6.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pluggy" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644 }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0"