From 316becada97fad30ed3d91983672177a7d0769f5 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:02:49 +0100 Subject: [PATCH 01/18] test script --- .github/workflows/test.yml | 10 +++- test_script.py | 81 +++++++++++++++++++++++++++ tests/core/__init__.py | 0 tests/{ => core}/test_auth.py | 0 tests/{ => core}/test_core.py | 0 tests/{ => core}/test_dependencies.py | 0 tests/{ => core}/test_factories.py | 34 +++++------ tests/{ => core}/test_groups.py | 0 tests/{ => core}/test_memberships.py | 0 tests/{ => core}/test_migrations.py | 0 tests/{ => core}/test_notification.py | 0 tests/{ => core}/test_ratelimit.py | 0 tests/{ => core}/test_schools.py | 0 tests/{ => core}/test_user_fusion.py | 0 tests/{ => core}/test_users.py | 0 tests/{ => core}/test_utils.py | 0 16 files changed, 106 insertions(+), 19 deletions(-) create mode 100644 test_script.py create mode 100644 tests/core/__init__.py rename tests/{ => core}/test_auth.py (100%) rename tests/{ => core}/test_core.py (100%) rename tests/{ => core}/test_dependencies.py (100%) rename tests/{ => core}/test_factories.py (97%) rename tests/{ => core}/test_groups.py (100%) rename tests/{ => core}/test_memberships.py (100%) rename tests/{ => core}/test_migrations.py (100%) rename tests/{ => core}/test_notification.py (100%) rename tests/{ => core}/test_ratelimit.py (100%) rename tests/{ => core}/test_schools.py (100%) rename tests/{ => core}/test_user_fusion.py (100%) rename tests/{ => core}/test_users.py (100%) rename tests/{ => core}/test_utils.py (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ca393c56ec..d727a1fb79 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -79,8 +79,14 @@ jobs: path: .pytest_cache key: pytest_cache-${{ github.head_ref }} - - name: Run unit tests with Postgresql - run: python -m pytest --cov + # Run unit tests, run them all when push to main branch + - name: Run all unit tests + run: python test_script.py --cov --all + if: github.event_name == 'push' + + - name: Run unit tests for changed files + run: python test_script.py --cov + if: github.event_name == 'pull_request' - name: Upload coverage reports to Codecov uses: codecov/codecov-action@v5.5.1 diff --git a/test_script.py b/test_script.py new file mode 100644 index 0000000000..7ed0e595a1 --- /dev/null +++ b/test_script.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +import subprocess +import sys +from pathlib import Path + + +def get_changed_files(): + """Enumerate files changed compared to main branch.""" + try: + diff = subprocess.check_output( + ["git", "diff", "--name-only", "origin/main..."], + text=True, + ).strip() + return diff.splitlines() + except subprocess.CalledProcessError: + return [] + + +def detect_modules(changed_files): + """DDetect impacted modules based on file paths.""" + modules = set() + for f in changed_files: + print(f) + if f.startswith("app/modules/"): + module = f.split("/")[2] + modules.add(module) + return sorted(modules) + + +def is_module_scope_only(changed_files): + """Check if the changes are only within module scopes.""" + # TODO: could be improved to ignore certain files like docs, etc. + return all(f.startswith("app/modules/") for f in changed_files) + + +def run_tests(modules, coverage=True, run_all=False): + """Run pytest with coverage on core + modified modules.""" + base_cmd = [ + "pytest", + ] + if coverage: + base_cmd += [ + "--cov", + ] + + if run_all: + print("Running all tests.") + return sys.exit(subprocess.call(base_cmd)) + + # core always tested + patterns = ["tests/core/"] + + for mod in modules: + path = f"tests/test_{mod}*.py" + if Path(path).exists(): + patterns.append(path) + + if not modules: + print("No specific module modified, testing core only.") + else: + print(f"Impacted modules: {', '.join(modules)}") + + print(f"Launching tests : {patterns}") + sys.exit(subprocess.call(base_cmd + patterns)) + + +if __name__ == "__main__": + changed = get_changed_files() + modules = detect_modules(changed) + scope_only = is_module_scope_only(changed) + + # Detect arg --cov + coverage = "--cov" in sys.argv + run_all = "--all" in sys.argv + + if scope_only and not run_all: + print("Changes are module-scoped only.") + run_tests(modules, coverage=coverage) + else: + print("Changes affect broader scope, running all tests.") + run_tests(modules, coverage=coverage, run_all=True) diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/test_auth.py b/tests/core/test_auth.py similarity index 100% rename from tests/test_auth.py rename to tests/core/test_auth.py diff --git a/tests/test_core.py b/tests/core/test_core.py similarity index 100% rename from tests/test_core.py rename to tests/core/test_core.py diff --git a/tests/test_dependencies.py b/tests/core/test_dependencies.py similarity index 100% rename from tests/test_dependencies.py rename to tests/core/test_dependencies.py diff --git a/tests/test_factories.py b/tests/core/test_factories.py similarity index 97% rename from tests/test_factories.py rename to tests/core/test_factories.py index 75b96fe810..8727fd9734 100644 --- a/tests/test_factories.py +++ b/tests/core/test_factories.py @@ -1,17 +1,17 @@ -import pytest -from fastapi.testclient import TestClient - -from app.module import all_modules -from tests.commons import get_TestingSessionLocal - - -@pytest.mark.parametrize("client", [True], indirect=True) -async def test_factories(client: TestClient) -> None: - async with get_TestingSessionLocal()() as db: - factories = [ - module.factory for module in all_modules if module.factory is not None - ] - for factory in factories: - assert not await factory.should_run( - db, - ), f"Factory {factory.__class__.__name__} should not run" +import pytest +from fastapi.testclient import TestClient + +from app.module import all_modules +from tests.commons import get_TestingSessionLocal + + +@pytest.mark.parametrize("client", [True], indirect=True) +async def test_factories(client: TestClient) -> None: + async with get_TestingSessionLocal()() as db: + factories = [ + module.factory for module in all_modules if module.factory is not None + ] + for factory in factories: + assert not await factory.should_run( + db, + ), f"Factory {factory.__class__.__name__} should not run" diff --git a/tests/test_groups.py b/tests/core/test_groups.py similarity index 100% rename from tests/test_groups.py rename to tests/core/test_groups.py diff --git a/tests/test_memberships.py b/tests/core/test_memberships.py similarity index 100% rename from tests/test_memberships.py rename to tests/core/test_memberships.py diff --git a/tests/test_migrations.py b/tests/core/test_migrations.py similarity index 100% rename from tests/test_migrations.py rename to tests/core/test_migrations.py diff --git a/tests/test_notification.py b/tests/core/test_notification.py similarity index 100% rename from tests/test_notification.py rename to tests/core/test_notification.py diff --git a/tests/test_ratelimit.py b/tests/core/test_ratelimit.py similarity index 100% rename from tests/test_ratelimit.py rename to tests/core/test_ratelimit.py diff --git a/tests/test_schools.py b/tests/core/test_schools.py similarity index 100% rename from tests/test_schools.py rename to tests/core/test_schools.py diff --git a/tests/test_user_fusion.py b/tests/core/test_user_fusion.py similarity index 100% rename from tests/test_user_fusion.py rename to tests/core/test_user_fusion.py diff --git a/tests/test_users.py b/tests/core/test_users.py similarity index 100% rename from tests/test_users.py rename to tests/core/test_users.py diff --git a/tests/test_utils.py b/tests/core/test_utils.py similarity index 100% rename from tests/test_utils.py rename to tests/core/test_utils.py From 9b02413dfc73a0ce5d12825cfcec4d01a946a9c7 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:07:41 +0100 Subject: [PATCH 02/18] Fetch main git history --- .github/workflows/test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d727a1fb79..f8a6aabf37 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,6 +52,9 @@ jobs: - name: Check out the code uses: actions/checkout@v5 + - name: Fetch main branch + run: git fetch origin main --depth=1 + # Setup Python (faster than using Python container) - name: Setup Python uses: actions/setup-python@v6 From 72effc750d5b2666eed53709b15fa204d817077f Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:12:39 +0100 Subject: [PATCH 03/18] Full history --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f8a6aabf37..e0647f5357 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,8 +52,8 @@ jobs: - name: Check out the code uses: actions/checkout@v5 - - name: Fetch main branch - run: git fetch origin main --depth=1 + - name: Fetch full main branch history # We don't known the the base commit of the PR + run: git fetch origin main --unshallow # Setup Python (faster than using Python container) - name: Setup Python From 20c11a084c8873e859689a8e1a5fdf04ac103593 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sun, 9 Nov 2025 12:16:17 +0100 Subject: [PATCH 04/18] Use logger instead of print --- test_script.py | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/test_script.py b/test_script.py index 7ed0e595a1..f2b49b999b 100644 --- a/test_script.py +++ b/test_script.py @@ -1,14 +1,19 @@ #!/usr/bin/env python3 +import logging import subprocess import sys from pathlib import Path +# Configure logging for GitHub Actions +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + def get_changed_files(): """Enumerate files changed compared to main branch.""" try: - diff = subprocess.check_output( - ["git", "diff", "--name-only", "origin/main..."], + diff = subprocess.check_output( # noqa: S603 + ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 text=True, ).strip() return diff.splitlines() @@ -20,7 +25,7 @@ def detect_modules(changed_files): """DDetect impacted modules based on file paths.""" modules = set() for f in changed_files: - print(f) + logger.info(f"Changed file: {f}") if f.startswith("app/modules/"): module = f.split("/")[2] modules.add(module) @@ -44,8 +49,8 @@ def run_tests(modules, coverage=True, run_all=False): ] if run_all: - print("Running all tests.") - return sys.exit(subprocess.call(base_cmd)) + logger.info("Running all tests.") + return sys.exit(subprocess.call(base_cmd)) # noqa: S603 # core always tested patterns = ["tests/core/"] @@ -56,12 +61,12 @@ def run_tests(modules, coverage=True, run_all=False): patterns.append(path) if not modules: - print("No specific module modified, testing core only.") + logger.info("No specific module modified, testing core only.") else: - print(f"Impacted modules: {', '.join(modules)}") + logger.info(f"Impacted modules: {', '.join(modules)}") - print(f"Launching tests : {patterns}") - sys.exit(subprocess.call(base_cmd + patterns)) + logger.info(f"Launching tests: {patterns}") + sys.exit(subprocess.call(base_cmd + patterns)) # noqa: S603 if __name__ == "__main__": @@ -74,8 +79,8 @@ def run_tests(modules, coverage=True, run_all=False): run_all = "--all" in sys.argv if scope_only and not run_all: - print("Changes are module-scoped only.") + logger.info("Changes are module-scoped only.") run_tests(modules, coverage=coverage) else: - print("Changes affect broader scope, running all tests.") + logger.info("Changes affect broader scope, running all tests.") run_tests(modules, coverage=coverage, run_all=True) From 7148bf36548af7394e2313b7db71363fb6fcef0b Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:28:52 +0100 Subject: [PATCH 05/18] Better tests script --- test_script.py | 86 ---------------------------------- tests_script.py | 121 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 86 deletions(-) delete mode 100644 test_script.py create mode 100644 tests_script.py diff --git a/test_script.py b/test_script.py deleted file mode 100644 index f2b49b999b..0000000000 --- a/test_script.py +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env python3 -import logging -import subprocess -import sys -from pathlib import Path - -# Configure logging for GitHub Actions -logging.basicConfig(level=logging.INFO, format="%(message)s") -logger = logging.getLogger(__name__) - - -def get_changed_files(): - """Enumerate files changed compared to main branch.""" - try: - diff = subprocess.check_output( # noqa: S603 - ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 - text=True, - ).strip() - return diff.splitlines() - except subprocess.CalledProcessError: - return [] - - -def detect_modules(changed_files): - """DDetect impacted modules based on file paths.""" - modules = set() - for f in changed_files: - logger.info(f"Changed file: {f}") - if f.startswith("app/modules/"): - module = f.split("/")[2] - modules.add(module) - return sorted(modules) - - -def is_module_scope_only(changed_files): - """Check if the changes are only within module scopes.""" - # TODO: could be improved to ignore certain files like docs, etc. - return all(f.startswith("app/modules/") for f in changed_files) - - -def run_tests(modules, coverage=True, run_all=False): - """Run pytest with coverage on core + modified modules.""" - base_cmd = [ - "pytest", - ] - if coverage: - base_cmd += [ - "--cov", - ] - - if run_all: - logger.info("Running all tests.") - return sys.exit(subprocess.call(base_cmd)) # noqa: S603 - - # core always tested - patterns = ["tests/core/"] - - for mod in modules: - path = f"tests/test_{mod}*.py" - if Path(path).exists(): - patterns.append(path) - - if not modules: - logger.info("No specific module modified, testing core only.") - else: - logger.info(f"Impacted modules: {', '.join(modules)}") - - logger.info(f"Launching tests: {patterns}") - sys.exit(subprocess.call(base_cmd + patterns)) # noqa: S603 - - -if __name__ == "__main__": - changed = get_changed_files() - modules = detect_modules(changed) - scope_only = is_module_scope_only(changed) - - # Detect arg --cov - coverage = "--cov" in sys.argv - run_all = "--all" in sys.argv - - if scope_only and not run_all: - logger.info("Changes are module-scoped only.") - run_tests(modules, coverage=coverage) - else: - logger.info("Changes affect broader scope, running all tests.") - run_tests(modules, coverage=coverage, run_all=True) diff --git a/tests_script.py b/tests_script.py new file mode 100644 index 0000000000..7d24dd3767 --- /dev/null +++ b/tests_script.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +import logging +import subprocess +import sys +from pathlib import Path + +# Configure logging for GitHub Actions +logging.basicConfig(level=logging.INFO, format="%(message)s") +logger = logging.getLogger(__name__) + +# We ignore .md files and GitHub workflows and app/modules to detect the scope of the changes +IGNORE_PATHS_START = ("app/modules/", ".github/") +IGNORE_EXTENSIONS = (".md",) + + +def get_changed_files(): + """Enumerate files changed compared to main branch.""" + try: + diff = subprocess.check_output( # noqa: S603 + ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 + text=True, + ).strip() + return diff.splitlines() + except subprocess.CalledProcessError: + return [] + + +def detect_modules(changed_files): + """DDetect impacted modules based on file paths.""" + modules = set() + for f in changed_files: + logger.info(f"Changed file: {f}") + if f.startswith("app/modules/"): + module = f.split("/")[2] + modules.add(module) + return sorted(modules) + + +def is_module_scope_only(changed_files): + """Check if the changes are only within module scopes.""" + return all( + f.startswith(IGNORE_PATHS_START) or f.endswith(IGNORE_EXTENSIONS) + for f in changed_files + ) + + +def get_modules_tests_patterns(modules, coverage=True, run_all=False): + """Run pytest with coverage on core + modified modules.""" + patterns = [] + for mod in modules: + path = f"tests/test_{mod}*.py" + if Path(path).exists(): + patterns.append(path) + else: + logger.warning(f"No tests found for module: {mod}") + + return patterns + + +def get_other_tests_patterns(changed_files: list[str]) -> list[str]: + """Get patterns for other tests based on changed files.""" + patterns = [] + # If a database model changed, run migrations + if any("models" in f for f in changed_files): + patterns.append("tests/test_migrations.py") + # If a factory changed, run factories tests + if any("factory" in f for f in changed_files): + patterns.append("tests/test_factories.py") + return patterns + + +def run_tests(modules, changed_files, coverage=True, run_all=False): + """Run tests based on changed modules.""" + base_cmd = [ + "pytest", + ] + if coverage: + base_cmd += [ + "--cov", + ] + + if run_all: + logger.info("Running all tests.") + return sys.exit(subprocess.call(base_cmd)) # noqa: S603 + + module_patterns = get_modules_tests_patterns( + modules, coverage=coverage, run_all=run_all, + ) + if not module_patterns: + logger.warning("No tests found for the changed modules.") + else: + logger.info(f"Impacted modules tests: {', '.join(module_patterns)}") + base_cmd += module_patterns + + get_other_tests = get_other_tests_patterns(changed_files) + if get_other_tests: + logger.info(f"Additional tests to run: {', '.join(get_other_tests)}") + base_cmd += get_other_tests + + logger.info(f"Running tests with command: {' '.join(base_cmd)}") + sys.exit(subprocess.call(base_cmd)) # noqa: S603 + + +if __name__ == "__main__": + changed_files = get_changed_files() + modules = detect_modules(changed_files) + scope_only = is_module_scope_only(changed_files) + + # Detect arg --cov + coverage = "--cov" in sys.argv + run_all = "--all" in sys.argv + + # First we check if changes are module-scoped only + # If so, we run tests only for those modules + if scope_only and not run_all: + logger.info("Changes are module-scoped only.") + run_tests(modules, changed_files, coverage=coverage) + # Else + else: + logger.info("Changes affect broader scope, running all tests.") + run_tests(modules, changed_files, coverage=coverage, run_all=True) From a249649902c98932d24cbc02480eb6f5975e8247 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:45:06 +0100 Subject: [PATCH 06/18] Organize tests --- .../sport_competition/__init__.py | 0 .../sport_competition/test_purchases.py | 2836 +++---- .../test_sport_inscription.py | 6852 ++++++++--------- .../sport_competition/test_validation.py | 0 tests/{ => modules}/test_PH.py | 0 tests/{ => modules}/test_advert.py | 0 tests/{ => modules}/test_amap.py | 0 tests/{ => modules}/test_booking.py | 0 tests/{ => modules}/test_calendar.py | 0 tests/{ => modules}/test_campaign.py | 0 tests/{ => modules}/test_cdr.py | 0 tests/{ => modules}/test_cdr_result.py | 0 tests/{ => modules}/test_cinema.py | 0 tests/{ => modules}/test_flappybird.py | 0 tests/{ => modules}/test_loan.py | 0 tests/{ => modules}/test_myeclpay.py | 0 tests/{ => modules}/test_payment.py | 0 tests/{ => modules}/test_phonebook.py | 0 tests/{ => modules}/test_raffle.py | 0 tests/{ => modules}/test_raid.py | 0 tests/{ => modules}/test_recommendation.py | 0 tests/{ => modules}/test_seed_library.py | 0 tests/{core => }/test_factories.py | 0 tests/{core => }/test_migrations.py | 0 tests_script.py | 28 +- 25 files changed, 4864 insertions(+), 4852 deletions(-) rename tests/{ => modules}/sport_competition/__init__.py (100%) rename tests/{ => modules}/sport_competition/test_purchases.py (96%) rename tests/{ => modules}/sport_competition/test_sport_inscription.py (96%) rename tests/{ => modules}/sport_competition/test_validation.py (100%) rename tests/{ => modules}/test_PH.py (100%) rename tests/{ => modules}/test_advert.py (100%) rename tests/{ => modules}/test_amap.py (100%) rename tests/{ => modules}/test_booking.py (100%) rename tests/{ => modules}/test_calendar.py (100%) rename tests/{ => modules}/test_campaign.py (100%) rename tests/{ => modules}/test_cdr.py (100%) rename tests/{ => modules}/test_cdr_result.py (100%) rename tests/{ => modules}/test_cinema.py (100%) rename tests/{ => modules}/test_flappybird.py (100%) rename tests/{ => modules}/test_loan.py (100%) rename tests/{ => modules}/test_myeclpay.py (100%) rename tests/{ => modules}/test_payment.py (100%) rename tests/{ => modules}/test_phonebook.py (100%) rename tests/{ => modules}/test_raffle.py (100%) rename tests/{ => modules}/test_raid.py (100%) rename tests/{ => modules}/test_recommendation.py (100%) rename tests/{ => modules}/test_seed_library.py (100%) rename tests/{core => }/test_factories.py (100%) rename tests/{core => }/test_migrations.py (100%) diff --git a/tests/sport_competition/__init__.py b/tests/modules/sport_competition/__init__.py similarity index 100% rename from tests/sport_competition/__init__.py rename to tests/modules/sport_competition/__init__.py diff --git a/tests/sport_competition/test_purchases.py b/tests/modules/sport_competition/test_purchases.py similarity index 96% rename from tests/sport_competition/test_purchases.py rename to tests/modules/sport_competition/test_purchases.py index 31346cef80..0fe34697f8 100644 --- a/tests/sport_competition/test_purchases.py +++ b/tests/modules/sport_competition/test_purchases.py @@ -1,1418 +1,1418 @@ -from datetime import UTC, datetime -from uuid import uuid4 - -import pytest -import pytest_asyncio -from fastapi.testclient import TestClient - -from app.core.groups.groups_type import GroupType -from app.core.payment import models_payment -from app.core.schools import models_schools -from app.core.schools.schools_type import SchoolType -from app.core.users import models_users -from app.modules.sport_competition import ( - cruds_sport_competition, - models_sport_competition, - schemas_sport_competition, -) -from app.modules.sport_competition.types_sport_competition import ( - ProductPublicType, - ProductSchoolType, - SportCategory, -) -from tests.commons import ( - add_object_to_db, - create_api_access_token, - create_user_with_groups, - get_TestingSessionLocal, - mocked_checkout_id, -) - -school_from_lyon: models_schools.CoreSchool -school_others: models_schools.CoreSchool - -active_edition: models_sport_competition.CompetitionEdition -old_edition: models_sport_competition.CompetitionEdition - -admin_user: models_users.CoreUser -user_from_lyon: models_users.CoreUser -user_others: models_users.CoreUser -user_cameraman: models_users.CoreUser -user_pompom: models_users.CoreUser -user_fanfare: models_users.CoreUser -user_volunteer: models_users.CoreUser -user_multiple: models_users.CoreUser -admin_token: str -user_from_lyon_token: str -user_others_token: str -user_cameraman_token: str -user_pompom_token: str -user_fanfare_token: str -user_volunteer_token: str -user_multiple_token: str - -competition_user_admin: models_sport_competition.CompetitionUser -competition_user_from_lyon: models_sport_competition.CompetitionUser -competition_user_others: models_sport_competition.CompetitionUser -competition_user_cameraman: models_sport_competition.CompetitionUser -competition_user_pompom: models_sport_competition.CompetitionUser -competition_user_fanfare: models_sport_competition.CompetitionUser -competition_user_volunteer: models_sport_competition.CompetitionUser -competition_user_multiple: models_sport_competition.CompetitionUser - -ecl_extension: models_sport_competition.SchoolExtension -school_from_lyon_extension: models_sport_competition.SchoolExtension -school_others_extension: models_sport_competition.SchoolExtension - -product1: models_sport_competition.CompetitionProduct -product2: models_sport_competition.CompetitionProduct -product_old_edition: models_sport_competition.CompetitionProduct - -school_product1_quota: models_sport_competition.SchoolProductQuota - -variant_for_athlete: models_sport_competition.CompetitionProductVariant -variant_for_cameraman: models_sport_competition.CompetitionProductVariant -variant_for_pompom: models_sport_competition.CompetitionProductVariant -variant_for_fanfare: models_sport_competition.CompetitionProductVariant -variant_for_volunteer: models_sport_competition.CompetitionProductVariant -variant_for_centrale: models_sport_competition.CompetitionProductVariant -variant_for_from_lyon: models_sport_competition.CompetitionProductVariant -variant_for_others: models_sport_competition.CompetitionProductVariant -variant_unique: models_sport_competition.CompetitionProductVariant -variant_disabled: models_sport_competition.CompetitionProductVariant -variant_old_edition: models_sport_competition.CompetitionProductVariant - -purchase: models_sport_competition.CompetitionPurchase -purchase2: models_sport_competition.CompetitionPurchase -payment: models_sport_competition.CompetitionPayment -checkout: models_sport_competition.CompetitionCheckout - - -@pytest.fixture -def users(): - return { - "admin": admin_user, - "from_lyon": user_from_lyon, - "others": user_others, - "cameraman": user_cameraman, - "pompom": user_pompom, - "fanfare": user_fanfare, - "volunteer": user_volunteer, - "multiple": user_multiple, - } - - -@pytest.fixture -def user_tokens(): - return { - "admin": admin_token, - "from_lyon": user_from_lyon_token, - "others": user_others_token, - "cameraman": user_cameraman_token, - "pompom": user_pompom_token, - "fanfare": user_fanfare_token, - "volunteer": user_volunteer_token, - "multiple": user_multiple_token, - } - - -@pytest.fixture -def variants(): - return { - "athlete": variant_for_athlete, - "cameraman": variant_for_cameraman, - "pompom": variant_for_pompom, - "fanfare": variant_for_fanfare, - "volunteer": variant_for_volunteer, - "centrale": variant_for_centrale, - "from_lyon": variant_for_from_lyon, - "others": variant_for_others, - "unique": variant_unique, - "disabled": variant_disabled, - "old_edition": variant_old_edition, - } - - -@pytest_asyncio.fixture(scope="module", autouse=True) -async def setup(): - global school_from_lyon, school_others - - school_from_lyon = models_schools.CoreSchool( - id=uuid4(), - name="Lycée de Lyon", - email_regex=".*@lyon.fr", - ) - school_others = models_schools.CoreSchool( - id=uuid4(), - name="Lycée des Autres", - email_regex=".*@autres.fr", - ) - await add_object_to_db(school_from_lyon) - await add_object_to_db(school_others) - - global active_edition, old_edition - - old_edition = models_sport_competition.CompetitionEdition( - id=uuid4(), - name="Edition 2023", - year=2023, - start_date=datetime(2023, 1, 1, tzinfo=UTC), - end_date=datetime(2023, 12, 31, tzinfo=UTC), - active=False, - inscription_enabled=False, - ) - await add_object_to_db(old_edition) - active_edition = models_sport_competition.CompetitionEdition( - id=uuid4(), - name="Edition 2024", - year=2024, - start_date=datetime(2024, 1, 1, tzinfo=UTC), - end_date=datetime(2024, 12, 31, tzinfo=UTC), - active=True, - inscription_enabled=True, - ) - await add_object_to_db(active_edition) - - global \ - admin_user, \ - user_from_lyon, \ - user_others, \ - user_cameraman, \ - user_pompom, \ - user_fanfare, \ - user_volunteer, \ - user_multiple - admin_user = await create_user_with_groups( - [GroupType.competition_admin], - email="Admin User", - ) - user_from_lyon = await create_user_with_groups( - [], - email="From Lyon User", - school_id=school_from_lyon.id, - ) - user_others = await create_user_with_groups( - [], - email="Others User", - school_id=school_others.id, - ) - user_cameraman = await create_user_with_groups( - [], - email="Cameraman User", - ) - user_pompom = await create_user_with_groups( - [], - email="Pompom User", - ) - user_fanfare = await create_user_with_groups( - [], - email="Fanfare User", - ) - user_volunteer = await create_user_with_groups( - [], - email="Volunteer User", - ) - user_multiple = await create_user_with_groups( - [], - email="Multiple Roles User", - ) - - global \ - admin_token, \ - user_from_lyon_token, \ - user_others_token, \ - user_cameraman_token, \ - user_pompom_token, \ - user_fanfare_token, \ - user_volunteer_token, \ - user_multiple_token - - admin_token = create_api_access_token(admin_user) - user_from_lyon_token = create_api_access_token(user_from_lyon) - user_others_token = create_api_access_token(user_others) - user_cameraman_token = create_api_access_token(user_cameraman) - user_pompom_token = create_api_access_token(user_pompom) - user_fanfare_token = create_api_access_token(user_fanfare) - user_volunteer_token = create_api_access_token(user_volunteer) - user_multiple_token = create_api_access_token(user_multiple) - - global \ - competition_user_admin, \ - competition_user_from_lyon, \ - competition_user_others, \ - competition_user_cameraman, \ - competition_user_pompom, \ - competition_user_fanfare, \ - competition_user_volunteer, \ - competition_user_multiple - - competition_user_admin = models_sport_competition.CompetitionUser( - user_id=admin_user.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_admin) - competition_user_from_lyon = models_sport_competition.CompetitionUser( - user_id=user_from_lyon.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_from_lyon) - competition_user_others = models_sport_competition.CompetitionUser( - user_id=user_others.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_others) - competition_user_cameraman = models_sport_competition.CompetitionUser( - user_id=user_cameraman.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_cameraman=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_cameraman) - competition_user_pompom = models_sport_competition.CompetitionUser( - user_id=user_pompom.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_pompom=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_pompom) - competition_user_fanfare = models_sport_competition.CompetitionUser( - user_id=user_fanfare.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_fanfare=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_fanfare) - competition_user_volunteer = models_sport_competition.CompetitionUser( - user_id=user_volunteer.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_volunteer=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_volunteer) - competition_user_multiple = models_sport_competition.CompetitionUser( - user_id=user_multiple.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - is_cameraman=True, - is_volunteer=True, - validated=False, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_multiple) - global ecl_extension, school_from_lyon_extension, school_others_extension - ecl_extension = models_sport_competition.SchoolExtension( - school_id=SchoolType.centrale_lyon.value, - from_lyon=True, - active=True, - inscription_enabled=True, - ) - await add_object_to_db(ecl_extension) - school_from_lyon_extension = models_sport_competition.SchoolExtension( - school_id=school_from_lyon.id, - from_lyon=True, - active=True, - inscription_enabled=True, - ) - await add_object_to_db(school_from_lyon_extension) - school_others_extension = models_sport_competition.SchoolExtension( - school_id=school_others.id, - from_lyon=False, - active=True, - inscription_enabled=True, - ) - await add_object_to_db(school_others_extension) - - global product1, product2, product_old_edition - product1 = models_sport_competition.CompetitionProduct( - id=uuid4(), - name="Product 1", - required=True, - description="Description for Product 1", - edition_id=active_edition.id, - ) - await add_object_to_db(product1) - product2 = models_sport_competition.CompetitionProduct( - id=uuid4(), - name="Product 2", - required=False, - description="Description for Product 2", - edition_id=active_edition.id, - ) - await add_object_to_db(product2) - product_old_edition = models_sport_competition.CompetitionProduct( - id=uuid4(), - name="Old Edition Product", - required=False, - description="Description for Old Edition Product", - edition_id=old_edition.id, - ) - await add_object_to_db(product_old_edition) - - global school_product1_quota - school_product1_quota = models_sport_competition.SchoolProductQuota( - school_id=school_from_lyon.id, - product_id=product1.id, - edition_id=active_edition.id, - quota=5, - ) - await add_object_to_db(school_product1_quota) - - global \ - variant_for_athlete, \ - variant_for_cameraman, \ - variant_for_pompom, \ - variant_for_fanfare, \ - variant_for_volunteer - variant_for_athlete = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Athlete Variant", - description="Variant for athletes", - price=10000, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.athlete, - ) - await add_object_to_db(variant_for_athlete) - variant_for_cameraman = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Cameraman Variant", - description="Variant for cameramen", - price=500, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.cameraman, - ) - await add_object_to_db(variant_for_cameraman) - variant_for_pompom = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Pompom Variant", - description="Variant for pompom teams", - price=300, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.pompom, - ) - await add_object_to_db(variant_for_pompom) - variant_for_fanfare = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Fanfare Variant", - description="Variant for fanfare teams", - price=400, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.fanfare, - ) - await add_object_to_db(variant_for_fanfare) - variant_for_volunteer = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Volunteer Variant", - description="Variant for volunteers", - price=200, - enabled=True, - unique=True, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.volunteer, - ) - await add_object_to_db(variant_for_volunteer) - - global variant_for_centrale, variant_for_from_lyon, variant_for_others - variant_for_centrale = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product2.id, - edition_id=active_edition.id, - name="Centrale Variant", - description="Variant for Centrale Lyon", - price=1500, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=None, - ) - await add_object_to_db(variant_for_centrale) - variant_for_from_lyon = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product2.id, - edition_id=active_edition.id, - name="From Lyon Variant", - description="Variant for schools from Lyon", - price=1200, - enabled=True, - unique=False, - school_type=ProductSchoolType.from_lyon, - public_type=None, - ) - await add_object_to_db(variant_for_from_lyon) - variant_for_others = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product2.id, - edition_id=active_edition.id, - name="Others Variant", - description="Variant for other schools", - price=1000, - enabled=True, - unique=False, - school_type=ProductSchoolType.others, - public_type=None, - ) - await add_object_to_db(variant_for_others) - - global variant_unique, variant_disabled, variant_old_edition - - variant_unique = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Unique Variant", - description="Variant that can only be purchased once", - price=2000, - enabled=True, - unique=True, - school_type=ProductSchoolType.centrale, - public_type=None, - ) - await add_object_to_db(variant_unique) - variant_disabled = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Disabled Variant", - description="Variant that is disabled", - price=1500, - enabled=False, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=None, - ) - await add_object_to_db(variant_disabled) - variant_old_edition = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product_old_edition.id, - edition_id=old_edition.id, - name="Old Edition Variant", - description="Variant for old edition products", - price=1000, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=None, - ) - await add_object_to_db(variant_old_edition) - - global purchase, purchase2, payment, checkout - purchase = models_sport_competition.CompetitionPurchase( - product_variant_id=variant_for_athlete.id, - user_id=admin_user.id, - edition_id=active_edition.id, - quantity=1, - validated=False, - purchased_on=datetime.now(UTC), - ) - await add_object_to_db(purchase) - purchase2 = models_sport_competition.CompetitionPurchase( - product_variant_id=variant_for_centrale.id, - user_id=admin_user.id, - edition_id=active_edition.id, - quantity=1, - validated=False, - purchased_on=datetime.now(UTC), - ) - await add_object_to_db(purchase2) - payment = models_sport_competition.CompetitionPayment( - id=uuid4(), - user_id=admin_user.id, - edition_id=active_edition.id, - total=2000, - created_at=datetime.now(UTC), - ) - await add_object_to_db(payment) - base_checkout = models_payment.Checkout( - id=uuid4(), - module="competition", - name="Competition Checkout", - amount=2000, - hello_asso_checkout_id=1, - secret="secret", - ) - await add_object_to_db(base_checkout) - checkout = models_sport_competition.CompetitionCheckout( - id=uuid4(), - user_id=admin_user.id, - edition_id=active_edition.id, - checkout_id=base_checkout.id, - ) - await add_object_to_db(checkout) - - -async def test_get_products( - client: TestClient, -): - response = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) > 0 - assert all("id" in item for item in data) - assert all("name" in item for item in data) - - -async def test_create_product( - client: TestClient, -): - new_product = schemas_sport_competition.ProductBase( - name="New Product", - required=False, - description="Description for New Product", - ) - - response = client.post( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - json=new_product.model_dump(), - ) - assert response.status_code == 201 - data = response.json() - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next((item for item in products_data if item["id"] == data["id"]), None) - assert product is not None - assert product["name"] == new_product.name - - -async def test_edit_product( - client: TestClient, -): - product_id = product1.id - updated_product = { - "name": "Updated Product 1", - "description": "Updated description for Product 1", - } - response = client.patch( - f"/competition/products/{product_id}", - json=updated_product, - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next( - (item for item in products_data if item["id"] == str(product_id)), - None, - ) - assert product is not None - assert product["name"] == updated_product["name"] - assert product["description"] == updated_product["description"] - - -async def test_delete_product( - client: TestClient, -): - product = models_sport_competition.CompetitionProduct( - id=uuid4(), - name="Product to Delete", - required=False, - description="Description for Product to Delete", - edition_id=active_edition.id, - ) - await add_object_to_db(product) - response = client.delete( - f"/competition/products/{product.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - assert not any(item["id"] == str(product.id) for item in products_data) - - -async def test_delete_product_with_variants( - client: TestClient, -): - response = client.delete( - f"/competition/products/{product1.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 403 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - assert any(item["id"] == str(product1.id) for item in products_data) - - -async def test_get_school_product_quotas( - client: TestClient, -): - response = client.get( - f"/competition/schools/{school_from_lyon.id}/product-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert any( - item["product_id"] == str(school_product1_quota.product_id) - and item["quota"] == school_product1_quota.quota - for item in data - ), data - - -async def test_get_product_schools_quota( - client: TestClient, -): - response = client.get( - f"/competition/products/{product1.id}/schools-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert any( - item["school_id"] == str(school_product1_quota.school_id) - and item["quota"] == school_product1_quota.quota - for item in data - ), data - - -async def test_create_school_product_quota( - client: TestClient, -): - new_quota = { - "product_id": str(product1.id), - "quota": 10, - } - response = client.post( - f"/competition/schools/{school_others.id}/product-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - json=new_quota, - ) - assert response.status_code == 201 - data = response.json() - assert data["school_id"] == str(school_others.id) - assert data["product_id"] == str(new_quota["product_id"]) - assert data["quota"] == new_quota["quota"] - - quotas = client.get( - f"/competition/schools/{school_others.id}/product-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quotas.status_code == 200 - quotas_data = quotas.json() - assert any( - item["product_id"] == str(new_quota["product_id"]) - and item["quota"] == new_quota["quota"] - for item in quotas_data - ) - - -async def test_edit_school_product_quota( - client: TestClient, -): - updated_quota = { - "quota": 15, - } - response = client.patch( - f"/competition/schools/{school_from_lyon.id}/product-quotas/{product1.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json=updated_quota, - ) - assert response.status_code == 204 - - quotas = client.get( - f"/competition/schools/{school_from_lyon.id}/product-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quotas.status_code == 200 - quotas_data = quotas.json() - quota = next( - (item for item in quotas_data if item["product_id"] == str(product1.id)), - None, - ) - assert quota is not None - assert quota["quota"] == updated_quota["quota"] - - -async def test_delete_school_product_quota( - client: TestClient, -): - response = client.delete( - f"/competition/schools/{school_from_lyon.id}/product-quotas/{product1.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - - quotas = client.get( - f"/competition/schools/{school_from_lyon.id}/product-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quotas.status_code == 200 - quotas_data = quotas.json() - assert not any( - item["product_id"] == str(school_product1_quota.product_id) - for item in quotas_data - ) - - -@pytest.mark.parametrize( - ("token", "expected_products_variants"), - [ - ("admin", ["athlete", "centrale", "unique"]), - ("from_lyon", ["from_lyon"]), - ("others", ["others"]), - ( - "cameraman", - ["cameraman", "centrale", "unique"], - ), - ("pompom", ["pompom", "centrale", "unique"]), - ("fanfare", ["fanfare", "centrale", "unique"]), - ( - "volunteer", - ["volunteer", "centrale", "unique"], - ), - ( - "multiple", - [ - "athlete", - "volunteer", - "centrale", - "unique", - ], - ), - ], -) -async def test_get_product_available( - client: TestClient, - user_tokens: dict[str, str], - variants: dict[str, models_sport_competition.CompetitionProductVariant], - token: str, - expected_products_variants: list[str], -): - response = client.get( - "/competition/products/available", - headers={"Authorization": f"Bearer {user_tokens[token]}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) == len(expected_products_variants) - assert all( - item["id"] in [str(variants[v].id) for v in expected_products_variants] - for item in data - ) - - -async def test_create_product_variants( - client: TestClient, -): - response = client.post( - f"/competition/products/{product1.id}/variants", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "New Variant", - "description": "Description for New Variant", - "product_id": str(product1.id), - "price": 1500, - "enabled": True, - "unique": False, - "school_type": ProductSchoolType.centrale.value, - "public_type": ProductPublicType.athlete.value, - }, - ) - assert response.status_code == 201 - data = response.json() - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next( - (item for item in products_data if item["id"] == str(product1.id)), - None, - ) - assert product is not None - assert "variants" in product - assert any(variant["id"] == data["id"] for variant in product["variants"]), product - - -async def test_edit_product_variant( - client: TestClient, -): - variant_to_update = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Variant to Update", - description="Description for Variant to Update", - price=1000, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.athlete, - ) - await add_object_to_db(variant_to_update) - updated_variant = { - "name": "Updated Variant", - "description": "Updated description for Variant", - "price": 1200, - "enabled": False, - "unique": True, - "school_type": ProductSchoolType.others.value, - "public_type": ProductPublicType.pompom.value, - } - response = client.patch( - f"/competition/products/variants/{variant_to_update.id}", - json=updated_variant, - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next( - (item for item in products_data if item["id"] == str(product1.id)), - None, - ) - assert product is not None - variant = next( - ( - variant - for variant in product["variants"] - if variant["id"] == str(variant_to_update.id) - ), - None, - ) - assert variant is not None - assert variant["name"] == updated_variant["name"] - assert variant["description"] == updated_variant["description"] - assert variant["price"] == updated_variant["price"] - assert variant["enabled"] is False - assert variant["unique"] is True - assert variant["school_type"] == updated_variant["school_type"] - assert variant["public_type"] == updated_variant["public_type"] - - -async def test_edit_product_variant_price_with_purchases( - client: TestClient, -): - updated_variant = {"price": 1500} - response = client.patch( - f"/competition/products/variants/{variant_for_athlete.id}", - json=updated_variant, - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 403 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next( - (item for item in products_data if item["id"] == str(product1.id)), - None, - ) - assert product is not None - variant = next( - ( - variant - for variant in product["variants"] - if variant["id"] == str(variant_for_athlete.id) - ), - None, - ) - assert variant is not None - assert variant["price"] == variant_for_athlete.price - - -async def test_delete_product_variant( - client: TestClient, -): - variant_to_delete = models_sport_competition.CompetitionProductVariant( - id=uuid4(), - product_id=product1.id, - edition_id=active_edition.id, - name="Variant to Delete", - description="Description for Variant to Delete", - price=1000, - enabled=True, - unique=False, - school_type=ProductSchoolType.centrale, - public_type=ProductPublicType.athlete, - ) - await add_object_to_db(variant_to_delete) - response = client.delete( - f"/competition/products/variants/{variant_to_delete.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next( - (item for item in products_data if item["id"] == str(product1.id)), - None, - ) - assert product is not None - assert not any( - variant["id"] == str(variant_to_delete.id) for variant in product["variants"] - ) - - -async def test_delete_product_variant_with_purchases( - client: TestClient, -): - response = client.delete( - f"/competition/products/variants/{variant_for_athlete.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 403 - - products = client.get( - "/competition/products", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert products.status_code == 200 - products_data = products.json() - product = next( - (item for item in products_data if item["id"] == str(product1.id)), - None, - ) - assert product is not None - assert any( - variant["id"] == str(variant_for_athlete.id) for variant in product["variants"] - ) - - -async def test_get_school_users_purchases( - client: TestClient, -): - response = client.get( - f"/competition/purchases/schools/{SchoolType.centrale_lyon.value}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, dict) - assert admin_user.id in data - assert isinstance(data[admin_user.id], list) - assert len(data[admin_user.id]) == 2 - - -async def get_user_purchases( - client: TestClient, -): - response = client.get( - f"/competition/purchases/users/{admin_user.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) == 1 - assert data[0]["product_variant_id"] == str(purchase.product_variant_id) - - -async def test_get_user_purchases_unauthorized( - client: TestClient, -): - response = client.get( - f"/competition/purchases/users/{user_others.id}", - headers={"Authorization": f"Bearer {user_others_token}"}, - ) - assert response.status_code == 403 - - -async def test_get_own_purchases( - client: TestClient, -): - response = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) == 2 - assert data[0]["product_variant_id"] == str(purchase.product_variant_id) - assert data[1]["product_variant_id"] == str(purchase2.product_variant_id) - - -@pytest.mark.parametrize( - ("token", "variant", "quantity", "expected_status"), - [ - ("from_lyon", "from_lyon", 1, 201), - ("others", "others", 1, 201), - ("cameraman", "cameraman", 1, 201), - ("pompom", "pompom", 1, 201), - ("fanfare", "fanfare", 1, 201), - ("volunteer", "volunteer", 2, 403), - ("volunteer", "volunteer", 1, 201), - ("multiple", "athlete", 1, 201), - ("from_lyon", "others", 1, 403), - ("others", "from_lyon", 1, 403), - ("cameraman", "athlete", 1, 403), - ("pompom", "athlete", 1, 403), - ("fanfare", "athlete", 1, 403), - ("volunteer", "athlete", 1, 403), - ("multiple", "cameraman", 1, 201), - ("multiple", "volunteer", 1, 201), - ("multiple", "fanfare", 1, 403), - ("admin", "disabled", 1, 403), - ("admin", "old_edition", 1, 403), - ], -) -async def test_create_purchase( - client: TestClient, - user_tokens: dict[str, str], - variants: dict[str, models_sport_competition.CompetitionProductVariant], - token: str, - variant: str, - quantity: int, - expected_status: int, -): - new_purchase = { - "product_variant_id": str(variants[variant].id), - "quantity": quantity, - } - response = client.post( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {user_tokens[token]}"}, - json=new_purchase, - ) - assert response.status_code == expected_status - purchases = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {user_tokens[token]}"}, - ) - purchases_data = purchases.json() - if response.status_code != 201: - assert not any( - purchase["product_variant_id"] == str(variants[variant].id) - for purchase in purchases_data - ) - else: - assert any( - purchase["product_variant_id"] == str(variants[variant].id) - for purchase in purchases_data - ) - - -async def test_delete_purchase( - client: TestClient, -): - purchase_to_delete = models_sport_competition.CompetitionPurchase( - product_variant_id=variant_for_cameraman.id, - user_id=admin_user.id, - edition_id=active_edition.id, - quantity=1, - validated=False, - purchased_on=datetime.now(UTC), - ) - await add_object_to_db(purchase_to_delete) - response = client.delete( - f"/competition/purchases/{purchase_to_delete.product_variant_id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - purchases = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - purchases_data = purchases.json() - assert not any( - purchase["product_variant_id"] == str(purchase_to_delete.product_variant_id) - for purchase in purchases_data - ) - - -async def test_delete_validated_purchase( - client: TestClient, -): - validated_purchase = models_sport_competition.CompetitionPurchase( - product_variant_id=variant_for_pompom.id, - user_id=admin_user.id, - edition_id=active_edition.id, - quantity=1, - validated=True, - purchased_on=datetime.now(UTC), - ) - await add_object_to_db(validated_purchase) - response = client.delete( - f"/competition/purchases/{validated_purchase.product_variant_id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 403 - purchases = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - purchases_data = purchases.json() - assert any( - purchase["product_variant_id"] == str(validated_purchase.product_variant_id) - for purchase in purchases_data - ) - - -async def test_get_school_users_payments( - client: TestClient, -): - response = client.get( - f"/competition/payments/schools/{SchoolType.centrale_lyon.value}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, dict) - assert admin_user.id in data - assert isinstance(data[admin_user.id], list) - assert len(data[admin_user.id]) == 1 - - -async def test_get_payments( - client: TestClient, -): - response = client.get( - f"/competition/users/{admin_user.id}/payments", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - data = response.json() - assert isinstance(data, list) - assert len(data) == 1 - assert data[0]["id"] == str(payment.id) - - -async def test_create_payment_non_validated_participant( - client: TestClient, -): - new_payment = { - "total": 3000, - } - response = client.post( - f"/competition/users/{user_others.id}/payments", - headers={"Authorization": f"Bearer {user_others_token}"}, - json=new_payment, - ) - assert response.status_code == 403 - - -async def test_create_payment( - client: TestClient, -): - async with get_TestingSessionLocal()() as db: - await cruds_sport_competition.validate_competition_user( - admin_user.id, - active_edition.id, - db, - ) - await db.commit() - - new_payment = { - "total": 9000, - } - response = client.post( - f"/competition/users/{admin_user.id}/payments", - headers={"Authorization": f"Bearer {admin_token}"}, - json=new_payment, - ) - assert response.status_code == 201 - data = response.json() - assert data["total"] == new_payment["total"] - assert "id" in data - - payments = client.get( - f"/competition/users/{admin_user.id}/payments", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - payments_data = payments.json() - assert any(payment["id"] == data["id"] for payment in payments_data) - - purchases = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - purchases_data = purchases.json() - validated_purchase = next( - ( - each_purchase - for each_purchase in purchases_data - if each_purchase["product_variant_id"] == str(purchase.product_variant_id) - ), - None, - ) - assert validated_purchase is not None - assert validated_purchase["validated"] is True - invalid_purchase = next( - ( - each_purchase - for each_purchase in purchases_data - if each_purchase["product_variant_id"] == str(purchase2.product_variant_id) - ), - None, - ) - assert invalid_purchase is not None - assert invalid_purchase["validated"] is False - - -async def test_delete_payment( - client: TestClient, -): - payment_to_delete = models_sport_competition.CompetitionPayment( - id=uuid4(), - user_id=admin_user.id, - edition_id=active_edition.id, - total=500, - created_at=datetime.now(UTC), - ) - await add_object_to_db(payment_to_delete) - async with get_TestingSessionLocal()() as db: - await cruds_sport_competition.mark_purchase_as_validated( - admin_user.id, - purchase2.product_variant_id, - True, - db, - ) - await db.commit() - - response = client.delete( - f"/competition/users/{admin_user.id}/payments/{payment_to_delete.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204 - payments = client.get( - f"/competition/users/{admin_user.id}/payments", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - payments_data = payments.json() - assert not any( - payment["id"] == str(payment_to_delete.id) for payment in payments_data - ) - - purchases = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - purchases_data = purchases.json() - invalid_purchase = next( - ( - each_purchase - for each_purchase in purchases_data - if each_purchase["product_variant_id"] == str(purchase2.product_variant_id) - ), - None, - ) - assert invalid_purchase is not None - assert invalid_purchase["validated"] is False - - -async def test_pay(client: TestClient): - response = client.post( - "/competition/pay", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 - assert response.json()["url"] == "https://some.url.fr/checkout" - - response = client.post( - "/payment/helloasso/webhook", - json={ - "eventType": "Payment", - "data": {"amount": 800, "id": 123}, - "metadata": { - "hyperion_checkout_id": str(mocked_checkout_id), - "secret": "checkoutsecret", - }, - }, - ) - assert response.status_code == 204 - - purchases = client.get( - "/competition/purchases/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - purchases_data = purchases.json() - assert all(purchase["validated"] is True for purchase in purchases_data) - - payments = client.get( - f"/competition/users/{admin_user.id}/payments", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - payments_data = payments.json() - - assert any(payment["total"] == 800 for payment in payments_data) - - -async def test_data_exporter( - client: TestClient, -): - response = client.get( - "/competition/users/data-export?included_fields=purchases&included_fields=payments&included_fields=participants", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 +from datetime import UTC, datetime +from uuid import uuid4 + +import pytest +import pytest_asyncio +from fastapi.testclient import TestClient + +from app.core.groups.groups_type import GroupType +from app.core.payment import models_payment +from app.core.schools import models_schools +from app.core.schools.schools_type import SchoolType +from app.core.users import models_users +from app.modules.sport_competition import ( + cruds_sport_competition, + models_sport_competition, + schemas_sport_competition, +) +from app.modules.sport_competition.types_sport_competition import ( + ProductPublicType, + ProductSchoolType, + SportCategory, +) +from tests.commons import ( + add_object_to_db, + create_api_access_token, + create_user_with_groups, + get_TestingSessionLocal, + mocked_checkout_id, +) + +school_from_lyon: models_schools.CoreSchool +school_others: models_schools.CoreSchool + +active_edition: models_sport_competition.CompetitionEdition +old_edition: models_sport_competition.CompetitionEdition + +admin_user: models_users.CoreUser +user_from_lyon: models_users.CoreUser +user_others: models_users.CoreUser +user_cameraman: models_users.CoreUser +user_pompom: models_users.CoreUser +user_fanfare: models_users.CoreUser +user_volunteer: models_users.CoreUser +user_multiple: models_users.CoreUser +admin_token: str +user_from_lyon_token: str +user_others_token: str +user_cameraman_token: str +user_pompom_token: str +user_fanfare_token: str +user_volunteer_token: str +user_multiple_token: str + +competition_user_admin: models_sport_competition.CompetitionUser +competition_user_from_lyon: models_sport_competition.CompetitionUser +competition_user_others: models_sport_competition.CompetitionUser +competition_user_cameraman: models_sport_competition.CompetitionUser +competition_user_pompom: models_sport_competition.CompetitionUser +competition_user_fanfare: models_sport_competition.CompetitionUser +competition_user_volunteer: models_sport_competition.CompetitionUser +competition_user_multiple: models_sport_competition.CompetitionUser + +ecl_extension: models_sport_competition.SchoolExtension +school_from_lyon_extension: models_sport_competition.SchoolExtension +school_others_extension: models_sport_competition.SchoolExtension + +product1: models_sport_competition.CompetitionProduct +product2: models_sport_competition.CompetitionProduct +product_old_edition: models_sport_competition.CompetitionProduct + +school_product1_quota: models_sport_competition.SchoolProductQuota + +variant_for_athlete: models_sport_competition.CompetitionProductVariant +variant_for_cameraman: models_sport_competition.CompetitionProductVariant +variant_for_pompom: models_sport_competition.CompetitionProductVariant +variant_for_fanfare: models_sport_competition.CompetitionProductVariant +variant_for_volunteer: models_sport_competition.CompetitionProductVariant +variant_for_centrale: models_sport_competition.CompetitionProductVariant +variant_for_from_lyon: models_sport_competition.CompetitionProductVariant +variant_for_others: models_sport_competition.CompetitionProductVariant +variant_unique: models_sport_competition.CompetitionProductVariant +variant_disabled: models_sport_competition.CompetitionProductVariant +variant_old_edition: models_sport_competition.CompetitionProductVariant + +purchase: models_sport_competition.CompetitionPurchase +purchase2: models_sport_competition.CompetitionPurchase +payment: models_sport_competition.CompetitionPayment +checkout: models_sport_competition.CompetitionCheckout + + +@pytest.fixture +def users(): + return { + "admin": admin_user, + "from_lyon": user_from_lyon, + "others": user_others, + "cameraman": user_cameraman, + "pompom": user_pompom, + "fanfare": user_fanfare, + "volunteer": user_volunteer, + "multiple": user_multiple, + } + + +@pytest.fixture +def user_tokens(): + return { + "admin": admin_token, + "from_lyon": user_from_lyon_token, + "others": user_others_token, + "cameraman": user_cameraman_token, + "pompom": user_pompom_token, + "fanfare": user_fanfare_token, + "volunteer": user_volunteer_token, + "multiple": user_multiple_token, + } + + +@pytest.fixture +def variants(): + return { + "athlete": variant_for_athlete, + "cameraman": variant_for_cameraman, + "pompom": variant_for_pompom, + "fanfare": variant_for_fanfare, + "volunteer": variant_for_volunteer, + "centrale": variant_for_centrale, + "from_lyon": variant_for_from_lyon, + "others": variant_for_others, + "unique": variant_unique, + "disabled": variant_disabled, + "old_edition": variant_old_edition, + } + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def setup(): + global school_from_lyon, school_others + + school_from_lyon = models_schools.CoreSchool( + id=uuid4(), + name="Lycée de Lyon", + email_regex=".*@lyon.fr", + ) + school_others = models_schools.CoreSchool( + id=uuid4(), + name="Lycée des Autres", + email_regex=".*@autres.fr", + ) + await add_object_to_db(school_from_lyon) + await add_object_to_db(school_others) + + global active_edition, old_edition + + old_edition = models_sport_competition.CompetitionEdition( + id=uuid4(), + name="Edition 2023", + year=2023, + start_date=datetime(2023, 1, 1, tzinfo=UTC), + end_date=datetime(2023, 12, 31, tzinfo=UTC), + active=False, + inscription_enabled=False, + ) + await add_object_to_db(old_edition) + active_edition = models_sport_competition.CompetitionEdition( + id=uuid4(), + name="Edition 2024", + year=2024, + start_date=datetime(2024, 1, 1, tzinfo=UTC), + end_date=datetime(2024, 12, 31, tzinfo=UTC), + active=True, + inscription_enabled=True, + ) + await add_object_to_db(active_edition) + + global \ + admin_user, \ + user_from_lyon, \ + user_others, \ + user_cameraman, \ + user_pompom, \ + user_fanfare, \ + user_volunteer, \ + user_multiple + admin_user = await create_user_with_groups( + [GroupType.competition_admin], + email="Admin User", + ) + user_from_lyon = await create_user_with_groups( + [], + email="From Lyon User", + school_id=school_from_lyon.id, + ) + user_others = await create_user_with_groups( + [], + email="Others User", + school_id=school_others.id, + ) + user_cameraman = await create_user_with_groups( + [], + email="Cameraman User", + ) + user_pompom = await create_user_with_groups( + [], + email="Pompom User", + ) + user_fanfare = await create_user_with_groups( + [], + email="Fanfare User", + ) + user_volunteer = await create_user_with_groups( + [], + email="Volunteer User", + ) + user_multiple = await create_user_with_groups( + [], + email="Multiple Roles User", + ) + + global \ + admin_token, \ + user_from_lyon_token, \ + user_others_token, \ + user_cameraman_token, \ + user_pompom_token, \ + user_fanfare_token, \ + user_volunteer_token, \ + user_multiple_token + + admin_token = create_api_access_token(admin_user) + user_from_lyon_token = create_api_access_token(user_from_lyon) + user_others_token = create_api_access_token(user_others) + user_cameraman_token = create_api_access_token(user_cameraman) + user_pompom_token = create_api_access_token(user_pompom) + user_fanfare_token = create_api_access_token(user_fanfare) + user_volunteer_token = create_api_access_token(user_volunteer) + user_multiple_token = create_api_access_token(user_multiple) + + global \ + competition_user_admin, \ + competition_user_from_lyon, \ + competition_user_others, \ + competition_user_cameraman, \ + competition_user_pompom, \ + competition_user_fanfare, \ + competition_user_volunteer, \ + competition_user_multiple + + competition_user_admin = models_sport_competition.CompetitionUser( + user_id=admin_user.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_admin) + competition_user_from_lyon = models_sport_competition.CompetitionUser( + user_id=user_from_lyon.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_from_lyon) + competition_user_others = models_sport_competition.CompetitionUser( + user_id=user_others.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_others) + competition_user_cameraman = models_sport_competition.CompetitionUser( + user_id=user_cameraman.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_cameraman=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_cameraman) + competition_user_pompom = models_sport_competition.CompetitionUser( + user_id=user_pompom.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_pompom=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_pompom) + competition_user_fanfare = models_sport_competition.CompetitionUser( + user_id=user_fanfare.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_fanfare=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_fanfare) + competition_user_volunteer = models_sport_competition.CompetitionUser( + user_id=user_volunteer.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_volunteer=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_volunteer) + competition_user_multiple = models_sport_competition.CompetitionUser( + user_id=user_multiple.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + is_cameraman=True, + is_volunteer=True, + validated=False, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_multiple) + global ecl_extension, school_from_lyon_extension, school_others_extension + ecl_extension = models_sport_competition.SchoolExtension( + school_id=SchoolType.centrale_lyon.value, + from_lyon=True, + active=True, + inscription_enabled=True, + ) + await add_object_to_db(ecl_extension) + school_from_lyon_extension = models_sport_competition.SchoolExtension( + school_id=school_from_lyon.id, + from_lyon=True, + active=True, + inscription_enabled=True, + ) + await add_object_to_db(school_from_lyon_extension) + school_others_extension = models_sport_competition.SchoolExtension( + school_id=school_others.id, + from_lyon=False, + active=True, + inscription_enabled=True, + ) + await add_object_to_db(school_others_extension) + + global product1, product2, product_old_edition + product1 = models_sport_competition.CompetitionProduct( + id=uuid4(), + name="Product 1", + required=True, + description="Description for Product 1", + edition_id=active_edition.id, + ) + await add_object_to_db(product1) + product2 = models_sport_competition.CompetitionProduct( + id=uuid4(), + name="Product 2", + required=False, + description="Description for Product 2", + edition_id=active_edition.id, + ) + await add_object_to_db(product2) + product_old_edition = models_sport_competition.CompetitionProduct( + id=uuid4(), + name="Old Edition Product", + required=False, + description="Description for Old Edition Product", + edition_id=old_edition.id, + ) + await add_object_to_db(product_old_edition) + + global school_product1_quota + school_product1_quota = models_sport_competition.SchoolProductQuota( + school_id=school_from_lyon.id, + product_id=product1.id, + edition_id=active_edition.id, + quota=5, + ) + await add_object_to_db(school_product1_quota) + + global \ + variant_for_athlete, \ + variant_for_cameraman, \ + variant_for_pompom, \ + variant_for_fanfare, \ + variant_for_volunteer + variant_for_athlete = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Athlete Variant", + description="Variant for athletes", + price=10000, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.athlete, + ) + await add_object_to_db(variant_for_athlete) + variant_for_cameraman = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Cameraman Variant", + description="Variant for cameramen", + price=500, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.cameraman, + ) + await add_object_to_db(variant_for_cameraman) + variant_for_pompom = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Pompom Variant", + description="Variant for pompom teams", + price=300, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.pompom, + ) + await add_object_to_db(variant_for_pompom) + variant_for_fanfare = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Fanfare Variant", + description="Variant for fanfare teams", + price=400, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.fanfare, + ) + await add_object_to_db(variant_for_fanfare) + variant_for_volunteer = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Volunteer Variant", + description="Variant for volunteers", + price=200, + enabled=True, + unique=True, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.volunteer, + ) + await add_object_to_db(variant_for_volunteer) + + global variant_for_centrale, variant_for_from_lyon, variant_for_others + variant_for_centrale = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product2.id, + edition_id=active_edition.id, + name="Centrale Variant", + description="Variant for Centrale Lyon", + price=1500, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=None, + ) + await add_object_to_db(variant_for_centrale) + variant_for_from_lyon = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product2.id, + edition_id=active_edition.id, + name="From Lyon Variant", + description="Variant for schools from Lyon", + price=1200, + enabled=True, + unique=False, + school_type=ProductSchoolType.from_lyon, + public_type=None, + ) + await add_object_to_db(variant_for_from_lyon) + variant_for_others = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product2.id, + edition_id=active_edition.id, + name="Others Variant", + description="Variant for other schools", + price=1000, + enabled=True, + unique=False, + school_type=ProductSchoolType.others, + public_type=None, + ) + await add_object_to_db(variant_for_others) + + global variant_unique, variant_disabled, variant_old_edition + + variant_unique = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Unique Variant", + description="Variant that can only be purchased once", + price=2000, + enabled=True, + unique=True, + school_type=ProductSchoolType.centrale, + public_type=None, + ) + await add_object_to_db(variant_unique) + variant_disabled = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Disabled Variant", + description="Variant that is disabled", + price=1500, + enabled=False, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=None, + ) + await add_object_to_db(variant_disabled) + variant_old_edition = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product_old_edition.id, + edition_id=old_edition.id, + name="Old Edition Variant", + description="Variant for old edition products", + price=1000, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=None, + ) + await add_object_to_db(variant_old_edition) + + global purchase, purchase2, payment, checkout + purchase = models_sport_competition.CompetitionPurchase( + product_variant_id=variant_for_athlete.id, + user_id=admin_user.id, + edition_id=active_edition.id, + quantity=1, + validated=False, + purchased_on=datetime.now(UTC), + ) + await add_object_to_db(purchase) + purchase2 = models_sport_competition.CompetitionPurchase( + product_variant_id=variant_for_centrale.id, + user_id=admin_user.id, + edition_id=active_edition.id, + quantity=1, + validated=False, + purchased_on=datetime.now(UTC), + ) + await add_object_to_db(purchase2) + payment = models_sport_competition.CompetitionPayment( + id=uuid4(), + user_id=admin_user.id, + edition_id=active_edition.id, + total=2000, + created_at=datetime.now(UTC), + ) + await add_object_to_db(payment) + base_checkout = models_payment.Checkout( + id=uuid4(), + module="competition", + name="Competition Checkout", + amount=2000, + hello_asso_checkout_id=1, + secret="secret", + ) + await add_object_to_db(base_checkout) + checkout = models_sport_competition.CompetitionCheckout( + id=uuid4(), + user_id=admin_user.id, + edition_id=active_edition.id, + checkout_id=base_checkout.id, + ) + await add_object_to_db(checkout) + + +async def test_get_products( + client: TestClient, +): + response = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) > 0 + assert all("id" in item for item in data) + assert all("name" in item for item in data) + + +async def test_create_product( + client: TestClient, +): + new_product = schemas_sport_competition.ProductBase( + name="New Product", + required=False, + description="Description for New Product", + ) + + response = client.post( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + json=new_product.model_dump(), + ) + assert response.status_code == 201 + data = response.json() + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next((item for item in products_data if item["id"] == data["id"]), None) + assert product is not None + assert product["name"] == new_product.name + + +async def test_edit_product( + client: TestClient, +): + product_id = product1.id + updated_product = { + "name": "Updated Product 1", + "description": "Updated description for Product 1", + } + response = client.patch( + f"/competition/products/{product_id}", + json=updated_product, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next( + (item for item in products_data if item["id"] == str(product_id)), + None, + ) + assert product is not None + assert product["name"] == updated_product["name"] + assert product["description"] == updated_product["description"] + + +async def test_delete_product( + client: TestClient, +): + product = models_sport_competition.CompetitionProduct( + id=uuid4(), + name="Product to Delete", + required=False, + description="Description for Product to Delete", + edition_id=active_edition.id, + ) + await add_object_to_db(product) + response = client.delete( + f"/competition/products/{product.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + assert not any(item["id"] == str(product.id) for item in products_data) + + +async def test_delete_product_with_variants( + client: TestClient, +): + response = client.delete( + f"/competition/products/{product1.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 403 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + assert any(item["id"] == str(product1.id) for item in products_data) + + +async def test_get_school_product_quotas( + client: TestClient, +): + response = client.get( + f"/competition/schools/{school_from_lyon.id}/product-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert any( + item["product_id"] == str(school_product1_quota.product_id) + and item["quota"] == school_product1_quota.quota + for item in data + ), data + + +async def test_get_product_schools_quota( + client: TestClient, +): + response = client.get( + f"/competition/products/{product1.id}/schools-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert any( + item["school_id"] == str(school_product1_quota.school_id) + and item["quota"] == school_product1_quota.quota + for item in data + ), data + + +async def test_create_school_product_quota( + client: TestClient, +): + new_quota = { + "product_id": str(product1.id), + "quota": 10, + } + response = client.post( + f"/competition/schools/{school_others.id}/product-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + json=new_quota, + ) + assert response.status_code == 201 + data = response.json() + assert data["school_id"] == str(school_others.id) + assert data["product_id"] == str(new_quota["product_id"]) + assert data["quota"] == new_quota["quota"] + + quotas = client.get( + f"/competition/schools/{school_others.id}/product-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quotas.status_code == 200 + quotas_data = quotas.json() + assert any( + item["product_id"] == str(new_quota["product_id"]) + and item["quota"] == new_quota["quota"] + for item in quotas_data + ) + + +async def test_edit_school_product_quota( + client: TestClient, +): + updated_quota = { + "quota": 15, + } + response = client.patch( + f"/competition/schools/{school_from_lyon.id}/product-quotas/{product1.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json=updated_quota, + ) + assert response.status_code == 204 + + quotas = client.get( + f"/competition/schools/{school_from_lyon.id}/product-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quotas.status_code == 200 + quotas_data = quotas.json() + quota = next( + (item for item in quotas_data if item["product_id"] == str(product1.id)), + None, + ) + assert quota is not None + assert quota["quota"] == updated_quota["quota"] + + +async def test_delete_school_product_quota( + client: TestClient, +): + response = client.delete( + f"/competition/schools/{school_from_lyon.id}/product-quotas/{product1.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + quotas = client.get( + f"/competition/schools/{school_from_lyon.id}/product-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quotas.status_code == 200 + quotas_data = quotas.json() + assert not any( + item["product_id"] == str(school_product1_quota.product_id) + for item in quotas_data + ) + + +@pytest.mark.parametrize( + ("token", "expected_products_variants"), + [ + ("admin", ["athlete", "centrale", "unique"]), + ("from_lyon", ["from_lyon"]), + ("others", ["others"]), + ( + "cameraman", + ["cameraman", "centrale", "unique"], + ), + ("pompom", ["pompom", "centrale", "unique"]), + ("fanfare", ["fanfare", "centrale", "unique"]), + ( + "volunteer", + ["volunteer", "centrale", "unique"], + ), + ( + "multiple", + [ + "athlete", + "volunteer", + "centrale", + "unique", + ], + ), + ], +) +async def test_get_product_available( + client: TestClient, + user_tokens: dict[str, str], + variants: dict[str, models_sport_competition.CompetitionProductVariant], + token: str, + expected_products_variants: list[str], +): + response = client.get( + "/competition/products/available", + headers={"Authorization": f"Bearer {user_tokens[token]}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) == len(expected_products_variants) + assert all( + item["id"] in [str(variants[v].id) for v in expected_products_variants] + for item in data + ) + + +async def test_create_product_variants( + client: TestClient, +): + response = client.post( + f"/competition/products/{product1.id}/variants", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "New Variant", + "description": "Description for New Variant", + "product_id": str(product1.id), + "price": 1500, + "enabled": True, + "unique": False, + "school_type": ProductSchoolType.centrale.value, + "public_type": ProductPublicType.athlete.value, + }, + ) + assert response.status_code == 201 + data = response.json() + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next( + (item for item in products_data if item["id"] == str(product1.id)), + None, + ) + assert product is not None + assert "variants" in product + assert any(variant["id"] == data["id"] for variant in product["variants"]), product + + +async def test_edit_product_variant( + client: TestClient, +): + variant_to_update = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Variant to Update", + description="Description for Variant to Update", + price=1000, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.athlete, + ) + await add_object_to_db(variant_to_update) + updated_variant = { + "name": "Updated Variant", + "description": "Updated description for Variant", + "price": 1200, + "enabled": False, + "unique": True, + "school_type": ProductSchoolType.others.value, + "public_type": ProductPublicType.pompom.value, + } + response = client.patch( + f"/competition/products/variants/{variant_to_update.id}", + json=updated_variant, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next( + (item for item in products_data if item["id"] == str(product1.id)), + None, + ) + assert product is not None + variant = next( + ( + variant + for variant in product["variants"] + if variant["id"] == str(variant_to_update.id) + ), + None, + ) + assert variant is not None + assert variant["name"] == updated_variant["name"] + assert variant["description"] == updated_variant["description"] + assert variant["price"] == updated_variant["price"] + assert variant["enabled"] is False + assert variant["unique"] is True + assert variant["school_type"] == updated_variant["school_type"] + assert variant["public_type"] == updated_variant["public_type"] + + +async def test_edit_product_variant_price_with_purchases( + client: TestClient, +): + updated_variant = {"price": 1500} + response = client.patch( + f"/competition/products/variants/{variant_for_athlete.id}", + json=updated_variant, + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 403 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next( + (item for item in products_data if item["id"] == str(product1.id)), + None, + ) + assert product is not None + variant = next( + ( + variant + for variant in product["variants"] + if variant["id"] == str(variant_for_athlete.id) + ), + None, + ) + assert variant is not None + assert variant["price"] == variant_for_athlete.price + + +async def test_delete_product_variant( + client: TestClient, +): + variant_to_delete = models_sport_competition.CompetitionProductVariant( + id=uuid4(), + product_id=product1.id, + edition_id=active_edition.id, + name="Variant to Delete", + description="Description for Variant to Delete", + price=1000, + enabled=True, + unique=False, + school_type=ProductSchoolType.centrale, + public_type=ProductPublicType.athlete, + ) + await add_object_to_db(variant_to_delete) + response = client.delete( + f"/competition/products/variants/{variant_to_delete.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next( + (item for item in products_data if item["id"] == str(product1.id)), + None, + ) + assert product is not None + assert not any( + variant["id"] == str(variant_to_delete.id) for variant in product["variants"] + ) + + +async def test_delete_product_variant_with_purchases( + client: TestClient, +): + response = client.delete( + f"/competition/products/variants/{variant_for_athlete.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 403 + + products = client.get( + "/competition/products", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert products.status_code == 200 + products_data = products.json() + product = next( + (item for item in products_data if item["id"] == str(product1.id)), + None, + ) + assert product is not None + assert any( + variant["id"] == str(variant_for_athlete.id) for variant in product["variants"] + ) + + +async def test_get_school_users_purchases( + client: TestClient, +): + response = client.get( + f"/competition/purchases/schools/{SchoolType.centrale_lyon.value}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, dict) + assert admin_user.id in data + assert isinstance(data[admin_user.id], list) + assert len(data[admin_user.id]) == 2 + + +async def get_user_purchases( + client: TestClient, +): + response = client.get( + f"/competition/purchases/users/{admin_user.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) == 1 + assert data[0]["product_variant_id"] == str(purchase.product_variant_id) + + +async def test_get_user_purchases_unauthorized( + client: TestClient, +): + response = client.get( + f"/competition/purchases/users/{user_others.id}", + headers={"Authorization": f"Bearer {user_others_token}"}, + ) + assert response.status_code == 403 + + +async def test_get_own_purchases( + client: TestClient, +): + response = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) == 2 + assert data[0]["product_variant_id"] == str(purchase.product_variant_id) + assert data[1]["product_variant_id"] == str(purchase2.product_variant_id) + + +@pytest.mark.parametrize( + ("token", "variant", "quantity", "expected_status"), + [ + ("from_lyon", "from_lyon", 1, 201), + ("others", "others", 1, 201), + ("cameraman", "cameraman", 1, 201), + ("pompom", "pompom", 1, 201), + ("fanfare", "fanfare", 1, 201), + ("volunteer", "volunteer", 2, 403), + ("volunteer", "volunteer", 1, 201), + ("multiple", "athlete", 1, 201), + ("from_lyon", "others", 1, 403), + ("others", "from_lyon", 1, 403), + ("cameraman", "athlete", 1, 403), + ("pompom", "athlete", 1, 403), + ("fanfare", "athlete", 1, 403), + ("volunteer", "athlete", 1, 403), + ("multiple", "cameraman", 1, 201), + ("multiple", "volunteer", 1, 201), + ("multiple", "fanfare", 1, 403), + ("admin", "disabled", 1, 403), + ("admin", "old_edition", 1, 403), + ], +) +async def test_create_purchase( + client: TestClient, + user_tokens: dict[str, str], + variants: dict[str, models_sport_competition.CompetitionProductVariant], + token: str, + variant: str, + quantity: int, + expected_status: int, +): + new_purchase = { + "product_variant_id": str(variants[variant].id), + "quantity": quantity, + } + response = client.post( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {user_tokens[token]}"}, + json=new_purchase, + ) + assert response.status_code == expected_status + purchases = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {user_tokens[token]}"}, + ) + purchases_data = purchases.json() + if response.status_code != 201: + assert not any( + purchase["product_variant_id"] == str(variants[variant].id) + for purchase in purchases_data + ) + else: + assert any( + purchase["product_variant_id"] == str(variants[variant].id) + for purchase in purchases_data + ) + + +async def test_delete_purchase( + client: TestClient, +): + purchase_to_delete = models_sport_competition.CompetitionPurchase( + product_variant_id=variant_for_cameraman.id, + user_id=admin_user.id, + edition_id=active_edition.id, + quantity=1, + validated=False, + purchased_on=datetime.now(UTC), + ) + await add_object_to_db(purchase_to_delete) + response = client.delete( + f"/competition/purchases/{purchase_to_delete.product_variant_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + purchases = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + purchases_data = purchases.json() + assert not any( + purchase["product_variant_id"] == str(purchase_to_delete.product_variant_id) + for purchase in purchases_data + ) + + +async def test_delete_validated_purchase( + client: TestClient, +): + validated_purchase = models_sport_competition.CompetitionPurchase( + product_variant_id=variant_for_pompom.id, + user_id=admin_user.id, + edition_id=active_edition.id, + quantity=1, + validated=True, + purchased_on=datetime.now(UTC), + ) + await add_object_to_db(validated_purchase) + response = client.delete( + f"/competition/purchases/{validated_purchase.product_variant_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 403 + purchases = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + purchases_data = purchases.json() + assert any( + purchase["product_variant_id"] == str(validated_purchase.product_variant_id) + for purchase in purchases_data + ) + + +async def test_get_school_users_payments( + client: TestClient, +): + response = client.get( + f"/competition/payments/schools/{SchoolType.centrale_lyon.value}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, dict) + assert admin_user.id in data + assert isinstance(data[admin_user.id], list) + assert len(data[admin_user.id]) == 1 + + +async def test_get_payments( + client: TestClient, +): + response = client.get( + f"/competition/users/{admin_user.id}/payments", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + data = response.json() + assert isinstance(data, list) + assert len(data) == 1 + assert data[0]["id"] == str(payment.id) + + +async def test_create_payment_non_validated_participant( + client: TestClient, +): + new_payment = { + "total": 3000, + } + response = client.post( + f"/competition/users/{user_others.id}/payments", + headers={"Authorization": f"Bearer {user_others_token}"}, + json=new_payment, + ) + assert response.status_code == 403 + + +async def test_create_payment( + client: TestClient, +): + async with get_TestingSessionLocal()() as db: + await cruds_sport_competition.validate_competition_user( + admin_user.id, + active_edition.id, + db, + ) + await db.commit() + + new_payment = { + "total": 9000, + } + response = client.post( + f"/competition/users/{admin_user.id}/payments", + headers={"Authorization": f"Bearer {admin_token}"}, + json=new_payment, + ) + assert response.status_code == 201 + data = response.json() + assert data["total"] == new_payment["total"] + assert "id" in data + + payments = client.get( + f"/competition/users/{admin_user.id}/payments", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + payments_data = payments.json() + assert any(payment["id"] == data["id"] for payment in payments_data) + + purchases = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + purchases_data = purchases.json() + validated_purchase = next( + ( + each_purchase + for each_purchase in purchases_data + if each_purchase["product_variant_id"] == str(purchase.product_variant_id) + ), + None, + ) + assert validated_purchase is not None + assert validated_purchase["validated"] is True + invalid_purchase = next( + ( + each_purchase + for each_purchase in purchases_data + if each_purchase["product_variant_id"] == str(purchase2.product_variant_id) + ), + None, + ) + assert invalid_purchase is not None + assert invalid_purchase["validated"] is False + + +async def test_delete_payment( + client: TestClient, +): + payment_to_delete = models_sport_competition.CompetitionPayment( + id=uuid4(), + user_id=admin_user.id, + edition_id=active_edition.id, + total=500, + created_at=datetime.now(UTC), + ) + await add_object_to_db(payment_to_delete) + async with get_TestingSessionLocal()() as db: + await cruds_sport_competition.mark_purchase_as_validated( + admin_user.id, + purchase2.product_variant_id, + True, + db, + ) + await db.commit() + + response = client.delete( + f"/competition/users/{admin_user.id}/payments/{payment_to_delete.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204 + payments = client.get( + f"/competition/users/{admin_user.id}/payments", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + payments_data = payments.json() + assert not any( + payment["id"] == str(payment_to_delete.id) for payment in payments_data + ) + + purchases = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + purchases_data = purchases.json() + invalid_purchase = next( + ( + each_purchase + for each_purchase in purchases_data + if each_purchase["product_variant_id"] == str(purchase2.product_variant_id) + ), + None, + ) + assert invalid_purchase is not None + assert invalid_purchase["validated"] is False + + +async def test_pay(client: TestClient): + response = client.post( + "/competition/pay", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 + assert response.json()["url"] == "https://some.url.fr/checkout" + + response = client.post( + "/payment/helloasso/webhook", + json={ + "eventType": "Payment", + "data": {"amount": 800, "id": 123}, + "metadata": { + "hyperion_checkout_id": str(mocked_checkout_id), + "secret": "checkoutsecret", + }, + }, + ) + assert response.status_code == 204 + + purchases = client.get( + "/competition/purchases/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + purchases_data = purchases.json() + assert all(purchase["validated"] is True for purchase in purchases_data) + + payments = client.get( + f"/competition/users/{admin_user.id}/payments", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + payments_data = payments.json() + + assert any(payment["total"] == 800 for payment in payments_data) + + +async def test_data_exporter( + client: TestClient, +): + response = client.get( + "/competition/users/data-export?included_fields=purchases&included_fields=payments&included_fields=participants", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 diff --git a/tests/sport_competition/test_sport_inscription.py b/tests/modules/sport_competition/test_sport_inscription.py similarity index 96% rename from tests/sport_competition/test_sport_inscription.py rename to tests/modules/sport_competition/test_sport_inscription.py index baf466a351..a9b83a201b 100644 --- a/tests/sport_competition/test_sport_inscription.py +++ b/tests/modules/sport_competition/test_sport_inscription.py @@ -1,3426 +1,3426 @@ -from datetime import UTC, datetime, timedelta -from uuid import UUID, uuid4 - -import pytest_asyncio -from fastapi.testclient import TestClient -from sqlalchemy import delete, update - -from app.core.groups.groups_type import GroupType -from app.core.schools import models_schools -from app.core.schools.schools_type import SchoolType -from app.core.users import models_users -from app.modules.sport_competition import models_sport_competition -from app.modules.sport_competition.schemas_sport_competition import ( - LocationBase, - MatchBase, - ParticipantInfo, - SportPodiumRankings, - TeamInfo, - TeamSportResultBase, - VolunteerShiftBase, -) -from app.modules.sport_competition.types_sport_competition import ( - CompetitionGroupType, - SportCategory, -) -from tests.commons import ( - add_object_to_db, - create_api_access_token, - create_user_with_groups, - get_TestingSessionLocal, -) - -school1: models_schools.CoreSchool -school2: models_schools.CoreSchool - -active_edition: models_sport_competition.CompetitionEdition -old_edition: models_sport_competition.CompetitionEdition - -admin_user: models_users.CoreUser -school_bds_user: models_users.CoreUser -sport_manager_user: models_users.CoreUser -user3: models_users.CoreUser -user4: models_users.CoreUser -admin_token: str -school_bds_token: str -sport_manager_token: str -user3_token: str -user4_token: str - -competition_user_admin: models_sport_competition.CompetitionUser -competition_user_school_bds: models_sport_competition.CompetitionUser -competition_user_sport_manager: models_sport_competition.CompetitionUser - -ecl_extension: models_sport_competition.SchoolExtension -school1_extension: models_sport_competition.SchoolExtension -ecl_general_quota: models_sport_competition.SchoolGeneralQuota -school_general_quota: models_sport_competition.SchoolGeneralQuota - -sport_free_quota: models_sport_competition.Sport -sport_used_quota: models_sport_competition.Sport -sport_with_team: models_sport_competition.Sport -sport_with_substitute: models_sport_competition.Sport -sport_feminine: models_sport_competition.Sport -ecl_sport_free_quota: models_sport_competition.SchoolSportQuota -ecl_sport_used_quota: models_sport_competition.SchoolSportQuota - -team_admin_user: models_sport_competition.CompetitionTeam -team1: models_sport_competition.CompetitionTeam -team2: models_sport_competition.CompetitionTeam - -participant1: models_sport_competition.CompetitionParticipant -participant2: models_sport_competition.CompetitionParticipant -participant3: models_sport_competition.CompetitionParticipant - -location: models_sport_competition.MatchLocation - -match1: models_sport_competition.Match - -podium_sport_free_quota: list[models_sport_competition.SportPodium] -podium_sport_with_team: list[models_sport_competition.SportPodium] - -volunteer_shift: models_sport_competition.VolunteerShift -volunteer_registration: models_sport_competition.VolunteerRegistration - - -async def create_competition_user( - edition_id: UUID, - school_id: UUID, - sport_category: SportCategory, -) -> tuple[models_users.CoreUser, models_sport_competition.CompetitionUser, str]: - new_user = await create_user_with_groups( - [], - school_id=school_id, - ) - new_competition_user = models_sport_competition.CompetitionUser( - user_id=new_user.id, - edition_id=edition_id, - sport_category=sport_category, - created_at=datetime.now(UTC), - validated=False, - is_athlete=True, - ) - await add_object_to_db(new_competition_user) - token = create_api_access_token(new_user) - return new_user, new_competition_user, token - - -@pytest_asyncio.fixture(scope="module", autouse=True) -async def init_objects() -> None: - global school1, school2, active_edition, old_edition - school1 = models_schools.CoreSchool( - id=uuid4(), - name="Emlyon Business School", - email_regex=r"^[\w.-]+@edu.emlyon.fr$", - ) - await add_object_to_db(school1) - school2 = models_schools.CoreSchool( - id=uuid4(), - name="Centrale Supelec", - email_regex=r"^[\w.-]+@edu.centralesupelec.fr$", - ) - await add_object_to_db(school2) - old_edition = models_sport_competition.CompetitionEdition( - id=uuid4(), - name="Edition 2023", - year=2023, - start_date=datetime(2023, 1, 1, tzinfo=UTC), - end_date=datetime(2023, 12, 31, tzinfo=UTC), - active=False, - inscription_enabled=False, - ) - await add_object_to_db(old_edition) - active_edition = models_sport_competition.CompetitionEdition( - id=uuid4(), - name="Edition 2024", - year=2024, - start_date=datetime(2024, 1, 1, tzinfo=UTC), - end_date=datetime(2024, 12, 31, tzinfo=UTC), - active=True, - inscription_enabled=True, - ) - await add_object_to_db(active_edition) - - global admin_user, school_bds_user, sport_manager_user, user3, user4 - admin_user = await create_user_with_groups( - [GroupType.competition_admin], - email="Admin User", - ) - school_bds_user = await create_user_with_groups( - [], - email="School BDS User", - school_id=school1.id, - ) - sport_manager_user = await create_user_with_groups( - [], - email="Sport Manager User", - ) - user3 = await create_user_with_groups( - [], - email="Random User", - ) - user4 = await create_user_with_groups( - [], - email="Another Random User", - ) - - global admin_token, school_bds_token, sport_manager_token, user3_token, user4_token - admin_token = create_api_access_token(admin_user) - school_bds_token = create_api_access_token(school_bds_user) - sport_manager_token = create_api_access_token(sport_manager_user) - user3_token = create_api_access_token(user3) - user4_token = create_api_access_token(user4) - - global \ - competition_user_admin, \ - competition_user_school_bds, \ - competition_user_sport_manager - competition_user_admin = models_sport_competition.CompetitionUser( - user_id=admin_user.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - is_volunteer=True, - validated=True, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_admin) - competition_user_school_bds = models_sport_competition.CompetitionUser( - user_id=school_bds_user.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - is_pompom=True, - validated=True, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_school_bds) - competition_user_sport_manager = models_sport_competition.CompetitionUser( - user_id=sport_manager_user.id, - sport_category=SportCategory.masculine, - edition_id=active_edition.id, - is_athlete=True, - is_cameraman=True, - validated=True, - created_at=datetime.now(UTC), - ) - await add_object_to_db(competition_user_sport_manager) - user1_bds_membership = models_sport_competition.CompetitionGroupMembership( - user_id=school_bds_user.id, - edition_id=active_edition.id, - group=CompetitionGroupType.schools_bds, - ) - await add_object_to_db(user1_bds_membership) - user2_sport_manager_membership = ( - models_sport_competition.CompetitionGroupMembership( - user_id=sport_manager_user.id, - edition_id=active_edition.id, - group=CompetitionGroupType.sport_manager, - ) - ) - await add_object_to_db(user2_sport_manager_membership) - user4_sport_manager_membership = ( - models_sport_competition.CompetitionGroupMembership( - user_id=user4.id, - edition_id=active_edition.id, - group=CompetitionGroupType.sport_manager, - ) - ) - await add_object_to_db(user4_sport_manager_membership) - - global ecl_extension, school1_extension - ecl_extension = models_sport_competition.SchoolExtension( - school_id=SchoolType.centrale_lyon.value, - from_lyon=True, - active=True, - inscription_enabled=False, - ) - await add_object_to_db(ecl_extension) - school1_extension = models_sport_competition.SchoolExtension( - school_id=school1.id, - from_lyon=False, - active=False, - inscription_enabled=False, - ) - await add_object_to_db(school1_extension) - - global ecl_general_quota, school_general_quota - ecl_general_quota = models_sport_competition.SchoolGeneralQuota( - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - athlete_quota=None, - cameraman_quota=None, - pompom_quota=None, - fanfare_quota=None, - athlete_cameraman_quota=None, - athlete_pompom_quota=None, - athlete_fanfare_quota=None, - non_athlete_cameraman_quota=None, - non_athlete_pompom_quota=None, - non_athlete_fanfare_quota=None, - ) - await add_object_to_db(ecl_general_quota) - school_general_quota = models_sport_competition.SchoolGeneralQuota( - school_id=school1.id, - edition_id=active_edition.id, - athlete_quota=1, - cameraman_quota=1, - pompom_quota=1, - fanfare_quota=1, - athlete_cameraman_quota=1, - athlete_pompom_quota=1, - athlete_fanfare_quota=1, - non_athlete_cameraman_quota=1, - non_athlete_pompom_quota=1, - non_athlete_fanfare_quota=1, - ) - await add_object_to_db(school_general_quota) - - global \ - sport_free_quota, \ - sport_used_quota, \ - sport_with_team, \ - sport_with_substitute, \ - sport_feminine - sport_free_quota = models_sport_competition.Sport( - id=uuid4(), - name="Free Quota Sport", - team_size=1, - substitute_max=0, - active=True, - sport_category=None, - ) - await add_object_to_db(sport_free_quota) - sport_used_quota = models_sport_competition.Sport( - id=uuid4(), - name="Used Quota Sport", - team_size=1, - substitute_max=0, - active=True, - sport_category=None, - ) - await add_object_to_db(sport_used_quota) - sport_with_team = models_sport_competition.Sport( - id=uuid4(), - name="Sport with Team", - team_size=5, - substitute_max=0, - active=True, - sport_category=None, - ) - await add_object_to_db(sport_with_team) - sport_with_substitute = models_sport_competition.Sport( - id=uuid4(), - name="Sport with Substitute", - team_size=5, - substitute_max=2, - active=True, - sport_category=None, - ) - await add_object_to_db(sport_with_substitute) - sport_feminine = models_sport_competition.Sport( - id=uuid4(), - name="Feminine Sport", - team_size=5, - substitute_max=2, - active=True, - sport_category=SportCategory.feminine, - ) - await add_object_to_db(sport_feminine) - - global ecl_sport_free_quota, ecl_sport_used_quota - ecl_sport_free_quota = models_sport_competition.SchoolSportQuota( - school_id=school1.id, - edition_id=active_edition.id, - sport_id=sport_free_quota.id, - participant_quota=2, - team_quota=1, - ) - await add_object_to_db(ecl_sport_free_quota) - ecl_sport_used_quota = models_sport_competition.SchoolSportQuota( - school_id=school1.id, - edition_id=active_edition.id, - sport_id=sport_used_quota.id, - participant_quota=0, - team_quota=1, - ) - await add_object_to_db(ecl_sport_used_quota) - - global team1, team2, team_admin_user - team1 = models_sport_competition.CompetitionTeam( - id=uuid4(), - sport_id=sport_with_team.id, - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - name="Team 1", - captain_id=sport_manager_user.id, - created_at=datetime.now(UTC), - ) - await add_object_to_db(team1) - team2 = models_sport_competition.CompetitionTeam( - id=uuid4(), - sport_id=sport_with_team.id, - school_id=school1.id, - edition_id=active_edition.id, - name="Team 2", - captain_id=school_bds_user.id, - created_at=datetime.now(UTC), - ) - await add_object_to_db(team2) - team_admin_user = models_sport_competition.CompetitionTeam( - id=uuid4(), - sport_id=sport_free_quota.id, - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - name="Admin Team", - captain_id=admin_user.id, - created_at=datetime.now(UTC), - ) - await add_object_to_db(team_admin_user) - - global participant1, participant2, participant3 - participant1 = models_sport_competition.CompetitionParticipant( - user_id=admin_user.id, - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_free_quota.id, - team_id=team_admin_user.id, - substitute=False, - license="1234567890", - certificate_file_id=None, - is_license_valid=True, - ) - await add_object_to_db(participant1) - participant2 = models_sport_competition.CompetitionParticipant( - user_id=sport_manager_user.id, - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - team_id=team1.id, - substitute=False, - license="0987654321", - certificate_file_id=None, - is_license_valid=True, - ) - await add_object_to_db(participant2) - ( - participant3_user, - _, - _, - ) = await create_competition_user( - edition_id=active_edition.id, - school_id=school1.id, - sport_category=SportCategory.masculine, - ) - participant3 = models_sport_competition.CompetitionParticipant( - user_id=participant3_user.id, - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - team_id=team1.id, - substitute=False, - license="1122334455", - certificate_file_id=None, - is_license_valid=True, - ) - await add_object_to_db(participant3) - - global location - location = models_sport_competition.MatchLocation( - id=uuid4(), - edition_id=active_edition.id, - name="Main Stadium", - address="123 Main St, City, Country", - latitude=45.764043, - longitude=4.835659, - description="Main stadium for the competition", - ) - await add_object_to_db(location) - - global match1 - match1 = models_sport_competition.Match( - id=uuid4(), - edition_id=active_edition.id, - sport_id=sport_with_team.id, - name="Match 1", - team1_id=team1.id, - team2_id=team2.id, - location_id=location.id, - date=datetime(2024, 6, 15, 15, 0, tzinfo=UTC), - score_team1=None, - score_team2=None, - winner_id=None, - ) - await add_object_to_db(match1) - - global podium_sport_free_quota, podium_sport_with_team - podium_sport_with_team = [ - models_sport_competition.SportPodium( - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - rank=1, - team_id=team_admin_user.id, - points=10, - ), - models_sport_competition.SportPodium( - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - rank=2, - team_id=team1.id, - points=5, - ), - models_sport_competition.SportPodium( - school_id=school1.id, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - rank=3, - team_id=team2.id, - points=2, - ), - ] - await add_object_to_db(podium_sport_with_team[0]) - await add_object_to_db(podium_sport_with_team[1]) - await add_object_to_db(podium_sport_with_team[2]) - podium_sport_free_quota = [ - models_sport_competition.SportPodium( - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_free_quota.id, - rank=1, - team_id=team_admin_user.id, - points=10, - ), - models_sport_competition.SportPodium( - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_free_quota.id, - rank=2, - team_id=team1.id, - points=5, - ), - models_sport_competition.SportPodium( - school_id=school1.id, - edition_id=active_edition.id, - sport_id=sport_free_quota.id, - rank=3, - team_id=team2.id, - points=2, - ), - ] - await add_object_to_db(podium_sport_free_quota[0]) - await add_object_to_db(podium_sport_free_quota[1]) - await add_object_to_db(podium_sport_free_quota[2]) - - global volunteer_shift, volunteer_registration - volunteer_shift = models_sport_competition.VolunteerShift( - id=uuid4(), - edition_id=active_edition.id, - name="Morning Shift", - description="Help with setup and registration", - value=2, - start_time=datetime(2024, 6, 15, 8, 0, tzinfo=UTC), - end_time=datetime(2024, 6, 15, 12, 0, tzinfo=UTC), - location="Main Entrance", - max_volunteers=1, - ) - await add_object_to_db(volunteer_shift) - volunteer_registration = models_sport_competition.VolunteerRegistration( - user_id=admin_user.id, - shift_id=volunteer_shift.id, - edition_id=active_edition.id, - registered_at=datetime.now(UTC), - validated=False, - ) - await add_object_to_db(volunteer_registration) - - -# region: Sports - - -async def test_get_sports( - client: TestClient, -) -> None: - response = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - editions = response.json() - assert len(editions) > 0 - - -async def test_create_sport_as_random( - client: TestClient, -) -> None: - response = client.post( - "/competition/sports", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "name": "Unauthorized Sport", - "team_size": 5, - "substitute_max": 2, - "active": True, - "sport_category": SportCategory.masculine.value, - }, - ) - assert response.status_code == 403, response.json() - - sports = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert sports.status_code == 200, sports.json() - sports_json = sports.json() - unauthorized_sport = next( - (s for s in sports_json if s["name"] == "Unauthorized Sport"), - None, - ) - assert unauthorized_sport is None - - -async def test_create_sport_as_admin( - client: TestClient, -) -> None: - response = client.post( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "New Sport", - "team_size": 5, - "substitute_max": 2, - "active": True, - "sport_category": SportCategory.masculine.value, - }, - ) - assert response.status_code == 201, response.json() - sport = response.json() - - sports = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert sports.status_code == 200, sports.json() - sports_json = sports.json() - new_sport = next( - (s for s in sports_json if s["id"] == sport["id"]), - None, - ) - assert new_sport is not None - - -async def test_create_sport_with_invalid_data( - client: TestClient, -) -> None: - response = client.post( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Invalid Sport", - "team_size": -1, # Invalid team size - "substitute_max": 2, - "active": True, - "sport_category": SportCategory.masculine.value, - }, - ) - assert response.status_code == 422, response.json() - - -async def test_create_sport_with_duplicate_name( - client: TestClient, -) -> None: - response = client.post( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": sport_free_quota.name, # Duplicate name - "team_size": 5, - "substitute_max": 2, - "active": True, - "sport_category": SportCategory.masculine.value, - }, - ) - assert response.status_code == 400, response.json() - - -async def test_patch_sport_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "name": "Unauthorized Update", - "team_size": 6, - "substitute_max": 3, - "active": True, - "sport_category": SportCategory.feminine.value, - }, - ) - assert response.status_code == 403, response.json() - - sports = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert sports.status_code == 200, sports.json() - sports_json = sports.json() - updated_sport_check = next( - (s for s in sports_json if s["id"] == str(sport_free_quota.id)), - None, - ) - assert updated_sport_check is not None - assert updated_sport_check["name"] == sport_free_quota.name - - -async def test_patch_sport_as_admin( - client: TestClient, -) -> None: - sport_to_modify = models_sport_competition.Sport( - id=uuid4(), - name="Sport to Modify", - team_size=5, - substitute_max=2, - active=True, - sport_category=SportCategory.masculine, - ) - await add_object_to_db(sport_to_modify) - response = client.patch( - f"/competition/sports/{sport_to_modify.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Updated Sport", - "team_size": 6, - "substitute_max": 3, - "active": True, - "sport_category": SportCategory.feminine.value, - }, - ) - assert response.status_code == 204, response.json() - - sports = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert sports.status_code == 200, sports.json() - sports_json = sports.json() - updated_sport_check = next( - (s for s in sports_json if s["id"] == str(sport_to_modify.id)), - None, - ) - assert updated_sport_check is not None - assert updated_sport_check["name"] == "Updated Sport" - - -async def test_patch_sport_with_duplicate_name( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": sport_used_quota.name, # Duplicate name - "team_size": 6, - "substitute_max": 3, - "active": True, - "sport_category": SportCategory.masculine.value, - }, - ) - assert response.status_code == 400, response.json() - - -async def test_delete_sport_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - sports = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert sports.status_code == 200, sports.json() - sports_json = sports.json() - deleted_sport_check = next( - (s for s in sports_json if s["id"] == str(sport_free_quota.id)), - None, - ) - assert deleted_sport_check is not None, sports.json() - - -async def test_delete_sport_active( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 400, response.json() - - -async def test_delete_sport_as_admin( - client: TestClient, -) -> None: - sport_to_delete = models_sport_competition.Sport( - id=uuid4(), - name="Sport to Delete", - team_size=5, - substitute_max=2, - active=False, - sport_category=SportCategory.masculine, - ) - await add_object_to_db(sport_to_delete) - - response = client.delete( - f"/competition/sports/{sport_to_delete.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - sports = client.get( - "/competition/sports", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert sports.status_code == 200, sports.json() - sports_json = sports.json() - deleted_sport_check = next( - (s for s in sports_json if s["id"] == str(sport_to_delete.id)), - None, - ) - assert deleted_sport_check is None - - -# endregion -# region: Editions - - -async def test_get_editions( - client: TestClient, -) -> None: - response = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - editions = response.json() - assert len(editions) > 0 - - -async def test_get_active_edition( - client: TestClient, -) -> None: - response = client.get( - "/competition/editions/active", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - edition_data = response.json() - assert edition_data["id"] == str(active_edition.id) - - -async def test_create_edition_as_admin( - client: TestClient, -) -> None: - response = client.post( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "New Competition Edition", - "year": 2025, - "start_date": "2024-01-01T00:00:00Z", - "end_date": "2024-12-31T23:59:59Z", - }, - ) - assert response.status_code == 201, response.json() - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - new_edition = next( - ( - edition - for edition in editions_json - if edition["name"] == "New Competition Edition" and edition["year"] == 2025 - ), - None, - ) - assert new_edition is not None - - -async def test_create_edition_as_random( - client: TestClient, -) -> None: - response = client.post( - "/competition/editions", - headers={"Authorization": f"Bearer {school_bds_token}"}, - json={ - "name": "Unauthorized Edition", - "year": 2025, - "start_date": "2024-01-01T00:00:00Z", - "end_date": "2024-12-31T23:59:59Z", - }, - ) - assert response.status_code == 403, response.json() - - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - unauthorized_edition = next( - ( - edition - for edition in editions_json - if edition["name"] == "Unauthorized Edition" and edition["year"] == 2025 - ), - None, - ) - assert unauthorized_edition is None - - -async def test_patch_edition_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/editions/{old_edition.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Updated Edition", - }, - ) - assert response.status_code == 204, response.json() - - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - updated_edition = next( - (edition for edition in editions_json if edition["id"] == str(old_edition.id)), - None, - ) - assert updated_edition is not None - assert updated_edition["name"] == "Updated Edition" - - -async def test_activate_edition( - client: TestClient, -) -> None: - response = client.post( - f"/competition/editions/{old_edition.id}/activate", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - activated_edition = next( - (edition for edition in editions_json if edition["id"] == str(old_edition.id)), - None, - ) - assert activated_edition is not None - assert activated_edition["active"] is True - client.post( - f"/competition/editions/{active_edition.id}/activate", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - - -async def test_enable_inscription_not_active( - client: TestClient, -) -> None: - response = client.post( - f"/competition/editions/{old_edition.id}/inscription", - headers={"Authorization": f"Bearer {admin_token}"}, - json=True, - ) - assert response.status_code == 400, response.json() - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - enabled_edition = next( - (edition for edition in editions_json if edition["id"] == str(old_edition.id)), - None, - ) - assert enabled_edition is not None - assert enabled_edition["inscription_enabled"] is False - - -async def test_enable_inscription( - client: TestClient, -) -> None: - response = client.post( - f"/competition/editions/{active_edition.id}/inscription", - headers={"Authorization": f"Bearer {admin_token}"}, - json=True, - ) - assert response.status_code == 204, response.json() - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - enabled_edition = next( - ( - edition - for edition in editions_json - if edition["id"] == str(active_edition.id) - ), - None, - ) - assert enabled_edition is not None - assert enabled_edition["inscription_enabled"] is True, enabled_edition - - -async def test_patch_edition_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/editions/{active_edition.id}", - headers={"Authorization": f"Bearer {school_bds_token}"}, - json={ - "name": "Unauthorized Edition Update", - }, - ) - assert response.status_code == 403, response.json() - - editions = client.get( - "/competition/editions", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert editions.status_code == 200 - editions_json = editions.json() - updated_edition_check = next( - ( - edition - for edition in editions_json - if edition["id"] == str(active_edition.id) - ), - None, - ) - assert updated_edition_check is not None - assert updated_edition_check["name"] == active_edition.name - - -# endregion -# region: Schools - - -async def test_get_schools( - client: TestClient, -) -> None: - response = client.get( - "/competition/schools", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - schools = response.json() - assert len(schools) > 0 - assert any(school["school_id"] == str(school1.id) for school in schools) - - -async def test_post_school_extension_as_random( - client: TestClient, -) -> None: - response = client.post( - "/competition/schools", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "school_id": str(school2.id), - "from_lyon": False, - "active": True, - "inscription_enabled": False, - }, - ) - assert response.status_code == 403, response.json() - - schools = client.get( - "/competition/schools", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert schools.status_code == 200, schools.json() - schools_json = schools.json() - unauthorized_school = next( - (s for s in schools_json if s["school_id"] == str(school2.id)), - None, - ) - assert unauthorized_school is None, schools_json - - -async def test_post_school_extension_as_admin( - client: TestClient, -) -> None: - response = client.post( - "/competition/schools", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "school_id": str(school2.id), - "from_lyon": False, - "active": True, - "inscription_enabled": True, - }, - ) - assert response.status_code == 201, response.json() - - schools = client.get( - "/competition/schools", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert schools.status_code == 200, schools.json() - schools_json = schools.json() - new_school_extension = next( - (s for s in schools_json if s["school_id"] == str(school2.id)), - None, - ) - assert new_school_extension is not None, schools_json - - -async def test_patch_school_extension_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/schools/{school1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "from_lyon": False, - "active": True, - "inscription_enabled": True, - }, - ) - assert response.status_code == 403, response.json() - - schools = client.get( - "/competition/schools", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert schools.status_code == 200, schools.json() - schools_json = schools.json() - updated_school = next( - (s for s in schools_json if s["school_id"] == str(school1.id)), - None, - ) - assert updated_school is not None - assert updated_school["from_lyon"] is False - assert updated_school["active"] is False - assert updated_school["inscription_enabled"] is False - - -async def test_patch_school_extension_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/schools/{school1.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "from_lyon": True, - "active": True, - "inscription_enabled": True, - }, - ) - assert response.status_code == 204, response.json() - - schools = client.get( - "/competition/schools", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert schools.status_code == 200, schools.json() - schools_json = schools.json() - updated_school = next( - (s for s in schools_json if s["school_id"] == str(school1.id)), - None, - ) - assert updated_school is not None - assert updated_school["from_lyon"] is True - assert updated_school["active"] is True - assert updated_school["inscription_enabled"] is True - - -# endregion -# region: Competition Users - - -async def test_get_competition_users( - client: TestClient, -) -> None: - response = client.get( - "/competition/users", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - users = response.json() - assert len(users) > 0 - assert all(user["edition_id"] == str(active_edition.id) for user in users) - - -async def test_get_competition_users_by_school( - client: TestClient, -) -> None: - response = client.get( - f"/competition/users/schools/{school1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - users = response.json() - assert len(users) > 0 - assert all(user["edition_id"] == str(active_edition.id) for user in users) - assert all(user["user"]["school_id"] == str(school1.id) for user in users) - - -async def test_get_competition_user_by_id( - client: TestClient, -) -> None: - response = client.get( - f"/competition/users/{competition_user_admin.user_id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - user = response.json() - assert user["user_id"] == str(competition_user_admin.user_id) - assert user["edition_id"] == str(competition_user_admin.edition_id) - assert competition_user_admin.sport_category is not None - assert user["sport_category"] == competition_user_admin.sport_category.value - - -async def test_post_competition_user( - client: TestClient, -) -> None: - response = client.post( - "/competition/users", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "sport_category": SportCategory.masculine.value, - "is_athlete": True, - }, - ) - assert response.status_code == 201, response.json() - user = response.json() - assert user["user_id"] == str(user3.id) - assert user["edition_id"] == str(active_edition.id) - assert user["sport_category"] == SportCategory.masculine.value - - -async def test_patch_competition_user_as_me( - client: TestClient, -) -> None: - response = client.patch( - "/competition/users/me", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "sport_category": SportCategory.feminine.value, - }, - ) - assert response.status_code == 204, response.json() - - user_response = client.get( - "/competition/users/me", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert user_response.status_code == 200, user_response.json() - user = user_response.json() - assert user["sport_category"] == SportCategory.feminine.value - - -async def test_patch_competition_user_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/users/{competition_user_admin.user_id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "sport_category": SportCategory.masculine.value, - }, - ) - assert response.status_code == 204, response.json() - - user_response = client.get( - f"/competition/users/{competition_user_admin.user_id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert user_response.status_code == 200, user_response.json() - user = user_response.json() - assert user["sport_category"] == SportCategory.masculine.value - - -# endregion -# region: Competition Groups - - -async def test_add_user_to_group_as_random( - client: TestClient, -) -> None: - response = client.post( - f"/competition/groups/{CompetitionGroupType.schools_bds.value}/users/{competition_user_admin.user_id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - user = client.get( - f"/competition/users/{competition_user_admin.user_id}/groups", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert user.status_code == 200, user.json() - user_json = user.json() - assert CompetitionGroupType.schools_bds.value not in [ - group["group"] for group in user_json - ], user_json - - -async def test_add_user_to_group_as_admin( - client: TestClient, -) -> None: - response = client.post( - f"/competition/groups/{CompetitionGroupType.schools_bds.value}/users/{competition_user_admin.user_id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 201, response.json() - - user = client.get( - "/competition/users/me/groups", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert user.status_code == 200, user.json() - user_json = user.json() - assert CompetitionGroupType.schools_bds.value in [ - group["group"] for group in user_json - ], user_json - - -async def test_remove_user_from_group_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/groups/{CompetitionGroupType.schools_bds.value}/users/{competition_user_school_bds.user_id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - user = client.get( - f"/competition/users/{competition_user_school_bds.user_id}/groups", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert user.status_code == 200, user.json() - user_json = user.json() - assert CompetitionGroupType.schools_bds.value in [ - group["group"] for group in user_json - ], user_json - - -# endregion -# region: School General Quotas - - -async def test_get_school_general_quota_as_random( - client: TestClient, -) -> None: - response = client.get( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - -async def test_get_school_general_quota( - client: TestClient, -) -> None: - response = client.get( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - school_quota_json = response.json() - assert school_quota_json["athlete_quota"] == 1, school_quota_json - assert school_quota_json["cameraman_quota"] == 1, school_quota_json - assert school_quota_json["pompom_quota"] == 1, school_quota_json - assert school_quota_json["fanfare_quota"] == 1, school_quota_json - - -async def test_get_school_general_quota_as_bds( - client: TestClient, -) -> None: - response = client.get( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {school_bds_token}"}, - ) - assert response.status_code == 200, response.json() - school_quota_json = response.json() - assert school_quota_json["athlete_quota"] == 1, school_quota_json - assert school_quota_json["cameraman_quota"] == 1, school_quota_json - assert school_quota_json["pompom_quota"] == 1, school_quota_json - assert school_quota_json["fanfare_quota"] == 1, school_quota_json - - -async def test_post_school_general_quota_as_random( - client: TestClient, -) -> None: - response = client.post( - f"/competition/schools/{school2.id}/general-quota", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "athlete_quota": 10, - "cameraman_quota": 5, - "pompom_quota": 3, - "fanfare_quota": 2, - }, - ) - assert response.status_code == 403, response.json() - - school_quota = client.get( - f"/competition/schools/{school2.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert school_quota.status_code == 404, school_quota.json() - - -async def test_post_school_general_quota_as_admin( - client: TestClient, -) -> None: - response = client.post( - f"/competition/schools/{school2.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "athlete_quota": 10, - "cameraman_quota": 5, - "pompom_quota": 3, - "fanfare_quota": 2, - }, - ) - assert response.status_code == 201, response.json() - - school_quota = client.get( - f"/competition/schools/{school2.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert school_quota.status_code == 200, school_quota.json() - school_quota_json = school_quota.json() - assert school_quota is not None, school_quota_json - assert school_quota_json["athlete_quota"] == 10, school_quota_json - assert school_quota_json["cameraman_quota"] == 5, school_quota_json - assert school_quota_json["pompom_quota"] == 3, school_quota_json - assert school_quota_json["fanfare_quota"] == 2, school_quota_json - - -async def test_patch_school_general_quota_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "athlete_quota": 5, - "cameraman_quota": 3, - "pompom_quota": 2, - "fanfare_quota": 1, - }, - ) - assert response.status_code == 403, response.json() - - school_quota = client.get( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert school_quota.status_code == 200, school_quota.json() - school_quota_json = school_quota.json() - assert school_quota_json["athlete_quota"] == 1, school_quota_json - assert school_quota_json["cameraman_quota"] == 1, school_quota_json - assert school_quota_json["pompom_quota"] == 1, school_quota_json - assert school_quota_json["fanfare_quota"] == 1, school_quota_json - - -async def test_patch_school_general_quota_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "athlete_quota": 5, - "cameraman_quota": 3, - "pompom_quota": 2, - "fanfare_quota": 1, - }, - ) - assert response.status_code == 204, response.json() - - school_quota = client.get( - f"/competition/schools/{school1.id}/general-quota", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert school_quota.status_code == 200, school_quota.json() - school_quota_json = school_quota.json() - assert school_quota_json["athlete_quota"] == 5, school_quota_json - assert school_quota_json["cameraman_quota"] == 3, school_quota_json - assert school_quota_json["pompom_quota"] == 2, school_quota_json - assert school_quota_json["fanfare_quota"] == 1, school_quota_json - - -# endregion -# region: Sport Quotas - - -async def test_get_school_sport_quota( - client: TestClient, -) -> None: - response = client.get( - f"/competition/schools/{school1.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - quotas = response.json() - assert len(quotas) > 0 - - -async def test_get_sport_quota( - client: TestClient, -) -> None: - response = client.get( - f"/competition/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - quota = response.json() - assert len(quota) > 0 - - -async def test_post_school_sport_quota_as_random( - client: TestClient, -) -> None: - response = client.post( - f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "participant_quota": 5, - "team_quota": 2, - }, - ) - assert response.status_code == 403, response.json() - - quota = client.get( - f"/competition/schools/{school2.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quota.status_code == 200, quota.json() - quota_json = quota.json() - sport_quota = next( - (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), - None, - ) - assert sport_quota is None, quota_json - - -async def test_post_school_sport_quota_as_admin( - client: TestClient, -) -> None: - response = client.post( - f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "participant_quota": 5, - "team_quota": 2, - }, - ) - assert response.status_code == 204, response.text - - quota = client.get( - f"/competition/schools/{school2.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quota.status_code == 200, quota.json() - quota_json = quota.json() - sport_quota = next( - (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), - None, - ) - assert sport_quota is not None, quota_json - assert sport_quota["participant_quota"] == 5, quota_json - assert sport_quota["team_quota"] == 2, quota_json - - -async def test_patch_school_sport_quota_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/schools/{school1.id}/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "participant_quota": 3, - "team_quota": 1, - }, - ) - assert response.status_code == 403, response.json() - - quota = client.get( - f"/competition/schools/{school1.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quota.status_code == 200, quota.json() - quota_json = quota.json() - sport_quota = next( - (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), - None, - ) - assert sport_quota is not None, quota_json - assert sport_quota["participant_quota"] == 2, quota_json - assert sport_quota["team_quota"] == 1, quota_json - - -async def test_patch_school_sport_quota_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "participant_quota": 3, - "team_quota": 1, - }, - ) - assert response.status_code == 204, response.json() - - quota = client.get( - f"/competition/schools/{school2.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quota.status_code == 200, quota.json() - quota_json = quota.json() - sport_quota = next( - (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), - None, - ) - assert sport_quota is not None, quota_json - assert sport_quota["participant_quota"] == 3, quota_json - assert sport_quota["team_quota"] == 1, quota_json - - -async def test_delete_school_sport_quota_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/schools/{school1.id}/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - quota = client.get( - f"/competition/schools/{school1.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quota.status_code == 200, quota.json() - quota_json = quota.json() - sport_quota = next( - (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), - None, - ) - assert sport_quota is not None, quota_json - - -async def test_delete_school_sport_quota_as_admin( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - quota = client.get( - f"/competition/schools/{school2.id}/sports-quotas", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert quota.status_code == 200, quota.json() - quota_json = quota.json() - sport_quota = next( - (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), - None, - ) - assert sport_quota is None, quota_json - - -# endregion -# region: Teams - - -async def test_get_user_team_as_captain( - client: TestClient, -) -> None: - response = client.get( - "/competition/teams/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - team = response.json() - assert team["id"] == str(team_admin_user.id) - - -async def test_get_sport_teams( - client: TestClient, -) -> None: - response = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - teams = response.json() - assert len(teams) == 2 - - -async def test_get_school_teams( - client: TestClient, -) -> None: - response = client.get( - f"/competition/teams/schools/{school1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - teams = response.json() - assert len(teams) == 1 - - -async def test_get_sport_team_for_school( - client: TestClient, -) -> None: - response = client.get( - f"/competition/teams/sports/{sport_with_team.id}/schools/{school1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - teams = response.json() - assert len(teams) == 1 - - -async def test_create_team_different_captain( - client: TestClient, -) -> None: - team_info = TeamInfo( - name="New Team", - school_id=school1.id, - sport_id=sport_with_team.id, - captain_id=school_bds_user.id, - ) - response = client.post( - "/competition/teams", - headers={"Authorization": f"Bearer {user3_token}"}, - json=team_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - new_team_check = next( - (t for t in teams_json if t["captain_id"] == str(school_bds_user.id)), - None, - ) - assert new_team_check is None, teams_json - - -async def test_create_team_different_school( - client: TestClient, -) -> None: - team_info = TeamInfo( - name="New Team", - school_id=school2.id, - sport_id=sport_with_team.id, - captain_id=user3.id, - ) - response = client.post( - "/competition/teams", - headers={"Authorization": f"Bearer {user3_token}"}, - json=team_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - new_team_check = next( - (t for t in teams_json if t["school_id"] == str(school2.id)), - None, - ) - assert new_team_check is None, teams_json - - -async def test_create_team_for_sport_without_team( - client: TestClient, -) -> None: - team_info = TeamInfo( - name="New Team", - school_id=SchoolType.centrale_lyon.value, - sport_id=sport_free_quota.id, - captain_id=user3.id, - ) - response = client.post( - "/competition/teams", - headers={"Authorization": f"Bearer {user3_token}"}, - json=team_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 400, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - new_team_check = next((t for t in teams_json if t["name"] == "New Team"), None) - assert new_team_check is None, teams_json - - -async def test_create_team_no_quota( - client: TestClient, -) -> None: - strict_quota = models_sport_competition.SchoolSportQuota( - school_id=school1.id, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - participant_quota=0, - team_quota=0, - ) - await add_object_to_db(strict_quota) - - team_info = TeamInfo( - name="New Team", - school_id=school1.id, - sport_id=sport_with_team.id, - captain_id=school_bds_user.id, - ) - response = client.post( - "/competition/teams", - headers={"Authorization": f"Bearer {school_bds_token}"}, - json=team_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 400, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - new_team_check = next((t for t in teams_json if t["name"] == "New Team"), None) - assert new_team_check is None, teams_json - - -async def test_create_team_used_name( - client: TestClient, -) -> None: - team_info = TeamInfo( - name=team1.name, - school_id=SchoolType.centrale_lyon.value, - sport_id=sport_with_team.id, - captain_id=user3.id, - ) - response = client.post( - "/competition/teams", - headers={"Authorization": f"Bearer {user3_token}"}, - json=team_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 400, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - new_team_check = next((t for t in teams_json if t["name"] == team1.name), None) - assert new_team_check is not None, teams_json - - -async def test_create_team( - client: TestClient, -) -> None: - team_info = TeamInfo( - name="New Team", - school_id=SchoolType.centrale_lyon.value, - sport_id=sport_with_team.id, - captain_id=user3.id, - ) - response = client.post( - "/competition/teams", - headers={"Authorization": f"Bearer {user3_token}"}, - json=team_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - team = response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - new_team_check = next((t for t in teams_json if t["id"] == team["id"]), None) - assert new_team_check is not None, teams_json - - -async def test_patch_team_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/teams/{team1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "name": "Unauthorized Team Update", - }, - ) - assert response.status_code == 403, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - updated_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) - assert updated_team_check is not None - assert updated_team_check["name"] == team1.name - - -async def test_patch_team_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/teams/{team1.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Updated Team Name", - }, - ) - assert response.status_code == 204, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - updated_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) - assert updated_team_check is not None - assert updated_team_check["name"] == "Updated Team Name" - - -async def test_patch_team_as_captain( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/teams/{team1.id}", - headers={"Authorization": f"Bearer {sport_manager_token}"}, - json={ - "name": "Captain Updated Team Name", - }, - ) - assert response.status_code == 204, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - updated_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) - assert updated_team_check is not None - assert updated_team_check["name"] == "Captain Updated Team Name" - - -async def test_delete_team_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/teams/{team1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - deleted_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) - assert deleted_team_check is not None, teams_json - - -async def test_delete_team_as_admin( - client: TestClient, -) -> None: - new_team = models_sport_competition.CompetitionTeam( - id=uuid4(), - name="Team to Delete", - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - captain_id=user3.id, - created_at=datetime.now(UTC), - ) - await add_object_to_db(new_team) - response = client.delete( - f"/competition/teams/{new_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - deleted_team_check = next( - (t for t in teams_json if t["id"] == str(new_team.id)), - None, - ) - assert deleted_team_check is None, teams_json - - -async def test_delete_team_as_captain( - client: TestClient, -) -> None: - new_team = models_sport_competition.CompetitionTeam( - id=uuid4(), - name="Team to Delete", - school_id=SchoolType.centrale_lyon.value, - edition_id=active_edition.id, - sport_id=sport_with_team.id, - captain_id=user3.id, - created_at=datetime.now(UTC), - ) - await add_object_to_db(new_team) - response = client.delete( - f"/competition/teams/{new_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 204, response.json() - - teams = client.get( - f"/competition/teams/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert teams.status_code == 200, teams.json() - teams_json = teams.json() - deleted_team_check = next( - (t for t in teams_json if t["id"] == str(new_team.id)), - None, - ) - assert deleted_team_check is None, teams_json - - -# endregion -# region: Participants - - -async def test_get_participant_me( - client: TestClient, -) -> None: - response = client.get( - "/competition/participants/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - user = response.json() - assert user["user_id"] == str(admin_user.id) - assert user["sport_id"] == str(sport_free_quota.id) - assert user["edition_id"] == str(active_edition.id) - - -async def test_get_participant_for_sport( - client: TestClient, -) -> None: - response = client.get( - f"/competition/participants/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - users = response.json() - assert len(users) > 0 - assert all(user["edition_id"] == str(active_edition.id) for user in users) - assert all(user["sport_id"] == str(sport_free_quota.id) for user in users) - - -async def test_get_participant_for_school_as_admin( - client: TestClient, -) -> None: - response = client.get( - f"/competition/participants/schools/{SchoolType.centrale_lyon.value}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200, response.json() - users = response.json() - assert len(users) > 0 - assert all(user["edition_id"] == str(active_edition.id) for user in users) - assert all( - user["school_id"] == str(SchoolType.centrale_lyon.value) for user in users - ) - - -async def test_get_participant_for_school_as_school_student( - client: TestClient, -) -> None: - response = client.get( - f"/competition/participants/schools/{SchoolType.centrale_lyon.value}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - users = response.json() - assert len(users) > 0 - assert all(user["edition_id"] == str(active_edition.id) for user in users) - assert all( - user["school_id"] == str(SchoolType.centrale_lyon.value) for user in users - ) - - -async def test_get_participant_for_school_as_random( - client: TestClient, -) -> None: - response = client.get( - f"/competition/participants/schools/{school1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - assert "Unauthorized action" in response.json()["detail"] - - -async def test_user_participate_with_invalid_category( - client: TestClient, -) -> None: - info = ParticipantInfo( - license="12345670089", - substitute=False, - ) - response = client.post( - f"/competition/sports/{sport_feminine.id}/participate", - headers={"Authorization": f"Bearer {school_bds_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - assert ( - "Sport category does not match user sport category" in response.json()["detail"] - ) - - participants = client.get( - f"/competition/participants/sports/{sport_feminine.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - assert not any( - p["user_id"] == str(school_bds_user.id) for p in participants_json - ), participants_json - - -async def test_user_participate_without_team( - client: TestClient, -) -> None: - info = ParticipantInfo( - license="12345670089", - substitute=False, - ) - response = client.post( - f"/competition/sports/{sport_with_team.id}/participate", - headers={"Authorization": f"Bearer {user3_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 400, response.json() - assert "Sport declared needs to be played in a team" in response.json()["detail"] - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( - participants_json - ) - - -async def test_user_participate_with_invalid_team_school( - client: TestClient, -) -> None: - info = ParticipantInfo( - license="12345670089", - substitute=False, - team_id=team2.id, # team2 is from a different school - ) - response = client.post( - f"/competition/sports/{sport_with_team.id}/participate", - headers={"Authorization": f"Bearer {user3_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - assert ( - "Unauthorized action, team does not belong to user school" - in response.json()["detail"] - ) - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( - participants_json - ) - - -async def test_user_participate_with_unknown_team( - client: TestClient, -) -> None: - info = ParticipantInfo( - license="12345670089", - substitute=False, - team_id=uuid4(), - ) - response = client.post( - f"/competition/sports/{sport_with_team.id}/participate", - headers={"Authorization": f"Bearer {user3_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 404, response.json() - assert "Team not found in the database" in response.json()["detail"] - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( - participants_json - ) - - -async def test_user_participate_with_maximum_team_size( - client: TestClient, -) -> None: - new_user, _, new_user_token = await create_competition_user( - edition_id=active_edition.id, - school_id=SchoolType.centrale_lyon.value, - sport_category=SportCategory.masculine, - ) - async with get_TestingSessionLocal()() as db: - await db.execute( - update(models_sport_competition.Sport) - .where( - models_sport_competition.Sport.id == sport_with_team.id, - ) - .values( - team_size=2, # Set team size to 1 for testing - ), - ) - await db.commit() - - info = ParticipantInfo( - license="12345670089", - substitute=False, - team_id=team1.id, # team1 is from the same school - ) - response = client.post( - f"/competition/sports/{sport_with_team.id}/participate", - headers={"Authorization": f"Bearer {new_user_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 400, response.json() - assert "Maximum number of players in the team reached" in response.json()["detail"] - - async with get_TestingSessionLocal()() as db: - await db.execute( - update(models_sport_competition.Sport) - .where( - models_sport_competition.Sport.id == sport_with_team.id, - ) - .values( - team_size=5, # Reset team size to a valid number - ), - ) - await db.commit() - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - assert not any(p["user_id"] == str(new_user.id) for p in participants_json), ( - participants_json - ) - - -async def test_user_participate_with_maximum_substitute_size( - client: TestClient, -) -> None: - info = ParticipantInfo( - license="12345670089", - substitute=True, - team_id=team1.id, # team1 is from the same school - ) - response = client.post( - f"/competition/sports/{sport_with_team.id}/participate", - headers={"Authorization": f"Bearer {user3_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 400, response.json() - assert ( - "Maximum number of substitutes in the team reached" in response.json()["detail"] - ) - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( - participants_json - ) - - -async def test_user_participate_with_valid_data( - client: TestClient, -) -> None: - info = ParticipantInfo( - license="12345670089", - substitute=False, - ) - response = client.post( - f"/competition/sports/{sport_free_quota.id}/participate", - headers={"Authorization": f"Bearer {user3_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - - participants = client.get( - f"/competition/participants/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - user_participation = next( - (u for u in participants_json if u["user_id"] == str(user3.id)), - None, - ) - assert user_participation is not None, participants_json - assert user_participation["edition_id"] == str(active_edition.id) - - -async def test_user_participate_with_team( - client: TestClient, -) -> None: - async with get_TestingSessionLocal()() as db: - await db.execute( - delete(models_sport_competition.CompetitionParticipant).where( - models_sport_competition.CompetitionParticipant.user_id == user3.id, - ), - ) - await db.commit() - info = ParticipantInfo( - license="12345670089", - substitute=False, - team_id=team1.id, # team1 is from the same school - ) - response = client.post( - f"/competition/sports/{sport_with_team.id}/participate", - headers={"Authorization": f"Bearer {user3_token}"}, - json=info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - user_participation = next( - (u for u in participants_json if u["user_id"] == str(user3.id)), - None, - ) - assert user_participation is not None, participants_json - assert user_participation["edition_id"] == str(active_edition.id) - - -async def test_add_user_certificate( - client: TestClient, -): - file = b"this is a test file" - file_content = { - "certificate": ("test_certificate.pdf", file, "application/pdf"), - } - response = client.post( - f"/competition/participants/sports/{sport_free_quota.id}/certificate", - headers={"Authorization": f"Bearer {admin_token}"}, - files=file_content, - ) - assert response.status_code == 204, response.json() - - participants = client.get( - f"/competition/participants/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - user_participation = next( - (u for u in participants_json if u["user_id"] == str(admin_user.id)), - None, - ) - assert user_participation is not None, participants_json - assert user_participation["certificate_file_id"] is not None, participants_json - - file_response = client.get( - f"/competition/participants/users/{admin_user.id}/certificate", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert file_response.status_code == 200, file_response.json() - assert file_response.content == file, file_response.json() - - -async def test_delete_user_certificate( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/participants/sports/{sport_free_quota.id}/certificate", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - participants = client.get( - f"/competition/participants/sports/{sport_free_quota.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - user_participation = next( - (u for u in participants_json if u["user_id"] == str(admin_user.id)), - None, - ) - assert user_participation is not None, participants_json - assert user_participation["certificate_file_id"] is None, participants_json - - -async def test_user_withdraw_participation( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/sports/{sport_with_team.id}/withdraw", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 204, response.json() - - participants = client.get( - f"/competition/participants/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert participants.status_code == 200, participants.json() - participants_json = participants.json() - user_participation = next( - (u for u in participants_json if u["user_id"] == str(user3.id)), - None, - ) - assert user_participation is None, participants_json - - -# endregion -# region: Locations - - -async def test_get_locations( - client: TestClient, -) -> None: - response = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - locations = response.json() - assert len(locations) == 1 - assert locations[0]["name"] == "Main Stadium" - - -async def test_get_location_by_id( - client: TestClient, -) -> None: - response = client.get( - f"/competition/locations/{location.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - location_json = response.json() - assert location_json["name"] == "Main Stadium" - assert len(location_json["matches"]) == 1 - - -async def test_post_location_as_random( - client: TestClient, -) -> None: - location_info = LocationBase( - name="New Location", - description="A new location for testing", - address="123 Main St", - latitude=45.0, - longitude=4.0, - ) - response = client.post( - "/competition/locations", - headers={"Authorization": f"Bearer {user3_token}"}, - json=location_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - - locations = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert locations.status_code == 200, locations.json() - locations_json = locations.json() - new_location_check = next( - (location for location in locations_json if location["name"] == "New Location"), - None, - ) - assert new_location_check is None, locations_json - - -async def test_post_location_as_admin( - client: TestClient, -) -> None: - location_info = LocationBase( - name="New Location", - description="A new location for testing", - address="123 Main St", - latitude=45.0, - longitude=4.0, - ) - response = client.post( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - json=location_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - location = response.json() - - locations = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert locations.status_code == 200, locations.json() - locations_json = locations.json() - new_location_check = next( - (loc for loc in locations_json if loc["id"] == location["id"]), - None, - ) - assert new_location_check is not None, locations_json - - -async def test_patch_location_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/locations/{location.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "name": "Unauthorized Location Update", - }, - ) - assert response.status_code == 403, response.json() - - locations = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert locations.status_code == 200, locations.json() - locations_json = locations.json() - updated_location_check = next( - (loc for loc in locations_json if loc["id"] == str(location.id)), - None, - ) - assert updated_location_check is not None - assert updated_location_check["name"] == location.name - - -async def test_patch_location_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/locations/{location.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Updated Location Name", - }, - ) - assert response.status_code == 204, response.json() - - locations = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert locations.status_code == 200, locations.json() - locations_json = locations.json() - updated_location_check = next( - (loc for loc in locations_json if loc["id"] == str(location.id)), - None, - ) - assert updated_location_check is not None - assert updated_location_check["name"] == "Updated Location Name" - - -async def delete_location_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/locations/{location.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - locations = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert locations.status_code == 200, locations.json() - locations_json = locations.json() - deleted_location_check = next( - (loc for loc in locations_json if loc["id"] == str(location.id)), - None, - ) - assert deleted_location_check is not None, locations_json - - -async def test_delete_location_as_admin( - client: TestClient, -) -> None: - new_location = models_sport_competition.MatchLocation( - id=uuid4(), - name="Location to Delete", - description="A location to delete", - address="456 Secondary St", - edition_id=active_edition.id, - latitude=46.0, - longitude=5.0, - ) - await add_object_to_db(new_location) - response = client.delete( - f"/competition/locations/{new_location.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - locations = client.get( - "/competition/locations", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert locations.status_code == 200, locations.json() - locations_json = locations.json() - deleted_location_check = next( - (loc for loc in locations_json if loc["id"] == str(new_location.id)), - None, - ) - assert deleted_location_check is None, locations_json - - -# endregion -# region: Matches - - -async def test_gest_sport_matches( - client: TestClient, -) -> None: - response = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - matches = response.json() - assert len(matches) == 1 - assert matches[0]["name"] == "Match 1" - - -async def test_gest_school_matches( - client: TestClient, -) -> None: - response = client.get( - f"/competition/matches/schools/{school1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - matches = response.json() - assert len(matches) == 1 - assert matches[0]["name"] == "Match 1" - - -async def test_create_match_as_random( - client: TestClient, -) -> None: - match_info = MatchBase( - name="New Match", - team1_id=team1.id, - team2_id=team2.id, - description="A new match for testing", - sport_id=sport_with_team.id, - location_id=location.id, - date=datetime.now(UTC), - ) - response = client.post( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json=match_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - new_match_check = next( - (match for match in matches_json if match["name"] == "New Match"), - None, - ) - assert new_match_check is None, matches_json - - -async def test_create_match_as_admin( - client: TestClient, -) -> None: - match_info = MatchBase( - name="New Match", - team1_id=team1.id, - team2_id=team2.id, - description="A new match for testing", - sport_id=sport_with_team.id, - location_id=location.id, - date=datetime(2024, 6, 15, 15, 0, 0, tzinfo=UTC), - ) - response = client.post( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json=match_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - match = response.json() - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - new_match_check = next( - (m for m in matches_json if m["id"] == match["id"]), - None, - ) - assert new_match_check is not None, matches_json - - -async def test_patch_match_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/matches/{match1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "name": "Unauthorized Match Update", - }, - ) - assert response.status_code == 403, response.json() - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - updated_match_check = next( - (m for m in matches_json if m["id"] == str(match1.id)), - None, - ) - assert updated_match_check is not None - assert updated_match_check["name"] == match1.name - - -async def test_patch_match_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/matches/{match1.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Updated Match Name", - "score_team1": 3, - "score_team2": 2, - "winner_id": str(team1.id), - }, - ) - assert response.status_code == 204, response.json() - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - updated_match_check = next( - (m for m in matches_json if m["id"] == str(match1.id)), - None, - ) - assert updated_match_check is not None - assert updated_match_check["name"] == "Updated Match Name" - assert updated_match_check["score_team1"] == 3 - assert updated_match_check["score_team2"] == 2 - assert updated_match_check["winner_id"] == str(team1.id) - - -async def test_edit_match_as_sport_manager( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/matches/{match1.id}", - headers={"Authorization": f"Bearer {user4_token}"}, - json={ - "name": "Sport Manager Updated Match Name", - "score_team1": 1, - "score_team2": 1, - "winner_id": None, - }, - ) - assert response.status_code == 204, response.json() - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - updated_match_check = next( - (m for m in matches_json if m["id"] == str(match1.id)), - None, - ) - assert updated_match_check is not None - assert updated_match_check["name"] == "Sport Manager Updated Match Name" - assert updated_match_check["score_team1"] == 1 - assert updated_match_check["score_team2"] == 1 - assert updated_match_check["winner_id"] is None - - -async def test_delete_match_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/matches/{match1.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.text - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - deleted_match_check = next( - (m for m in matches_json if m["id"] == str(match1.id)), - None, - ) - assert deleted_match_check is not None, matches_json - - -async def test_delete_match_as_admin( - client: TestClient, -) -> None: - new_match = models_sport_competition.Match( - id=uuid4(), - name="Match to Delete", - team1_id=team1.id, - team2_id=team2.id, - sport_id=sport_with_team.id, - location_id=location.id, - edition_id=active_edition.id, - date=datetime.now(UTC), - score_team1=None, - score_team2=None, - winner_id=None, - ) - await add_object_to_db(new_match) - response = client.delete( - f"/competition/matches/{new_match.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.text - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - deleted_match_check = next( - (m for m in matches_json if m["id"] == str(new_match.id)), - None, - ) - assert deleted_match_check is None, matches_json - - -async def test_delete_match_as_sport_manager( - client: TestClient, -) -> None: - new_match = models_sport_competition.Match( - id=uuid4(), - name="Match to Delete", - team1_id=team1.id, - team2_id=team2.id, - sport_id=sport_with_team.id, - location_id=location.id, - edition_id=active_edition.id, - date=datetime.now(UTC), - score_team1=None, - score_team2=None, - winner_id=None, - ) - await add_object_to_db(new_match) - response = client.delete( - f"/competition/matches/{new_match.id}", - headers={"Authorization": f"Bearer {user4_token}"}, - ) - assert response.status_code == 204, response.text - - matches = client.get( - f"/competition/matches/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert matches.status_code == 200, matches.json() - matches_json = matches.json() - deleted_match_check = next( - (m for m in matches_json if m["id"] == str(new_match.id)), - None, - ) - assert deleted_match_check is None, matches_json - - -# endregion -# region: Podiums -async def test_get_global_podiums( - client: TestClient, -) -> None: - response = client.get( - "/competition/podiums/global", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - podiums = response.json() - assert len(podiums) == 2 - centrale_score = next( - (p for p in podiums if p["school_id"] == str(SchoolType.centrale_lyon.value)), - None, - ) - assert centrale_score is not None - assert centrale_score["total_points"] == 30 - other_school_score = next( - (p for p in podiums if p["school_id"] == str(school1.id)), - None, - ) - assert other_school_score is not None - assert other_school_score["total_points"] == 4 - - -async def test_get_sport_podiums( - client: TestClient, -) -> None: - response = client.get( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 200, response.json() - podiums = response.json() - assert len(podiums) == 3 - - -async def test_post_podium_as_random( - client: TestClient, -) -> None: - podium_info = SportPodiumRankings( - rankings=[ - TeamSportResultBase( - sport_id=sport_with_team.id, - school_id=SchoolType.centrale_lyon.value, - points=15, - team_id=team1.id, - ), - ], - ) - - response = client.post( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json=podium_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - - podiums = client.get( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert podiums.status_code == 200, podiums.json() - podiums_json = podiums.json() - podium_check = next( - (p for p in podiums_json if p["points"] == 15), - None, - ) - assert podium_check is None, podiums_json - - -async def test_post_podium_as_sport_manager( - client: TestClient, -) -> None: - podium_info = SportPodiumRankings( - rankings=[ - TeamSportResultBase( - sport_id=sport_with_team.id, - school_id=SchoolType.centrale_lyon.value, - points=15, - team_id=team1.id, - ), - TeamSportResultBase( - sport_id=sport_with_team.id, - school_id=school1.id, - points=10, - team_id=team2.id, - ), - TeamSportResultBase( - sport_id=sport_with_team.id, - school_id=SchoolType.centrale_lyon.value, - points=5, - team_id=team_admin_user.id, - ), - ], - ) - - response = client.post( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user4_token}"}, - json=podium_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - - podiums = client.get( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert podiums.status_code == 200, podiums.json() - podiums_json = podiums.json() - podium_check = next( - (p for p in podiums_json if p["points"] == 15), - None, - ) - assert podium_check is not None, podiums_json - podium_check = next( - (p for p in podiums_json if p["points"] == 10), - None, - ) - assert podium_check is not None, podiums_json - podium_check = next( - (p for p in podiums_json if p["points"] == 5), - None, - ) - assert podium_check is not None, podiums_json - - -async def test_delete_podium_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - podiums = client.get( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert podiums.status_code == 200, podiums.json() - podiums_json = podiums.json() - assert len(podiums_json) == 3, podiums_json - - -async def test_delete_podium_as_sport_manager( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {user4_token}"}, - ) - assert response.status_code == 204, response.json() - - podiums = client.get( - f"/competition/podiums/sports/{sport_with_team.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert podiums.status_code == 200, podiums.json() - podiums_json = podiums.json() - assert len(podiums_json) == 0, podiums_json - - -# endregion -# region: Volunteers Shifts - - -async def test_get_volunteer_shifts( - client: TestClient, -) -> None: - response = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - - assert response.status_code == 200, response.json() - shifts = response.json() - assert len(shifts) == 1 - assert shifts[0]["name"] == "Morning Shift" - - -async def test_create_volunteer_shift_as_random( - client: TestClient, -) -> None: - shift_info = VolunteerShiftBase( - name="New Shift", - description="A new shift for testing", - value=1, - start_time=datetime.now(UTC), - end_time=datetime.now(UTC) + timedelta(hours=2), - location="Event Hall", - max_volunteers=5, - ) - response = client.post( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {user3_token}"}, - json=shift_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 403, response.json() - - shifts = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert shifts.status_code == 200, shifts.json() - shifts_json = shifts.json() - new_shift_check = next( - (shift for shift in shifts_json if shift["name"] == "New Shift"), - None, - ) - assert new_shift_check is None, shifts_json - - -async def test_create_volunteer_shift_as_admin( - client: TestClient, -) -> None: - shift_info = VolunteerShiftBase( - name="New Shift", - description="A new shift for testing", - value=1, - start_time=datetime.now(UTC), - end_time=datetime.now(UTC) + timedelta(hours=2), - location="Event Hall", - max_volunteers=5, - ) - response = client.post( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - json=shift_info.model_dump(exclude_none=True, mode="json"), - ) - assert response.status_code == 201, response.json() - shift = response.json() - - shifts = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert shifts.status_code == 200, shifts.json() - shifts_json = shifts.json() - new_shift_check = next( - (s for s in shifts_json if s["id"] == shift["id"]), - None, - ) - assert new_shift_check is not None, shifts_json - - -async def test_patch_volunteer_shift_as_random( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/volunteers/shifts/{volunteer_shift.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - json={ - "name": "Unauthorized Shift Update", - }, - ) - assert response.status_code == 403, response.json() - - shifts = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert shifts.status_code == 200, shifts.json() - shifts_json = shifts.json() - updated_shift_check = next( - (s for s in shifts_json if s["id"] == str(volunteer_shift.id)), - None, - ) - assert updated_shift_check is not None - assert updated_shift_check["name"] == volunteer_shift.name - - -async def test_patch_volunteer_shift_as_admin( - client: TestClient, -) -> None: - response = client.patch( - f"/competition/volunteers/shifts/{volunteer_shift.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - json={ - "name": "Updated Shift Name", - }, - ) - assert response.status_code == 204, response.json() - - shifts = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert shifts.status_code == 200, shifts.json() - shifts_json = shifts.json() - updated_shift_check = next( - (s for s in shifts_json if s["id"] == str(volunteer_shift.id)), - None, - ) - assert updated_shift_check is not None - assert updated_shift_check["name"] == "Updated Shift Name" - - -async def test_delete_volunteer_shift_as_random( - client: TestClient, -) -> None: - response = client.delete( - f"/competition/volunteers/shifts/{volunteer_shift.id}", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - - shifts = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert shifts.status_code == 200, shifts.json() - shifts_json = shifts.json() - deleted_shift_check = next( - (s for s in shifts_json if s["id"] == str(volunteer_shift.id)), - None, - ) - assert deleted_shift_check is not None, shifts_json - - -async def test_delete_volunteer_shift_as_admin( - client: TestClient, -) -> None: - new_shift = models_sport_competition.VolunteerShift( - id=uuid4(), - name="Shift to Delete", - description="A shift to delete", - value=1, - start_time=datetime.now(UTC), - end_time=datetime.now(UTC) + timedelta(hours=2), - location="Event Hall", - max_volunteers=5, - edition_id=active_edition.id, - ) - await add_object_to_db(new_shift) - new_registration = models_sport_competition.VolunteerRegistration( - shift_id=new_shift.id, - user_id=user3.id, - edition_id=active_edition.id, - registered_at=datetime.now(UTC), - validated=True, - ) - await add_object_to_db(new_registration) - response = client.delete( - f"/competition/volunteers/shifts/{new_shift.id}", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - shifts = client.get( - "/competition/volunteers/shifts", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert shifts.status_code == 200, shifts.json() - shifts_json = shifts.json() - deleted_shift_check = next( - (s for s in shifts_json if s["id"] == str(new_shift.id)), - None, - ) - assert deleted_shift_check is None, shifts_json - - registrations = client.get( - "/competition/volunteers/me", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert registrations.status_code == 200, registrations.json() - registrations_json = registrations.json() - deleted_registration_check = next( - (r for r in registrations_json if r["shift_id"] == str(new_shift.id)), - None, - ) - assert deleted_registration_check is None, registrations_json - - -# endregion -# region: Volunteer Registrations - - -async def test_get_own_volunteer_registrations( - client: TestClient, -) -> None: - response = client.get( - "/competition/volunteers/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - - assert response.status_code == 200, response.json() - registrations = response.json() - assert len(registrations) == 1 - assert registrations[0]["shift_id"] == str(volunteer_shift.id) - - -async def test_register_to_shift_as_non_volunteer( - client: TestClient, -) -> None: - response = client.post( - f"/competition/volunteers/shifts/{volunteer_shift.id}/register", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 403, response.json() - assert ( - "You must be registered for the competition as a volunteer to register for a volunteer shift" - in response.json()["detail"] - ) - - registrations = client.get( - "/competition/volunteers/me", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert registrations.status_code == 200, registrations.json() - registrations_json = registrations.json() - registration_check = next( - (r for r in registrations_json if r["shift_id"] == str(volunteer_shift.id)), - None, - ) - assert registration_check is None, registrations_json - - -async def test_register_already_registered_to_shift( - client: TestClient, -) -> None: - response = client.post( - f"/competition/volunteers/shifts/{volunteer_shift.id}/register", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 400, response.json() - assert ( - "You are already registered to this volunteer shift." - in response.json()["detail"] - ) - - -async def test_register_to_full_shift( - client: TestClient, -) -> None: - async with get_TestingSessionLocal()() as db: - await db.execute( - update(models_sport_competition.CompetitionUser) - .where(models_sport_competition.CompetitionUser.user_id == user3.id) - .values(is_volunteer=True), - ) - await db.commit() - - response = client.post( - f"/competition/volunteers/shifts/{volunteer_shift.id}/register", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert response.status_code == 400 - assert "This volunteer shift is full." in response.json()["detail"] - - registrations = client.get( - "/competition/volunteers/me", - headers={"Authorization": f"Bearer {user3_token}"}, - ) - assert registrations.status_code == 200, registrations.json() - registrations_json = registrations.json() - registration_check = next( - (r for r in registrations_json if r["shift_id"] == str(volunteer_shift.id)), - None, - ) - assert registration_check is None, registrations_json - - -async def test_register_to_volunteer_shift( - client: TestClient, -) -> None: - new_shift = models_sport_competition.VolunteerShift( - id=uuid4(), - name="Another Shift", - description="Another shift for testing", - value=1, - start_time=datetime.now(UTC) + timedelta(days=1), - end_time=datetime.now(UTC) + timedelta(days=1, hours=2), - location="Event Hall", - max_volunteers=5, - edition_id=active_edition.id, - ) - await add_object_to_db(new_shift) - - response = client.post( - f"/competition/volunteers/shifts/{new_shift.id}/register", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 204, response.json() - - registrations = client.get( - "/competition/volunteers/me", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert registrations.status_code == 200, registrations.json() - registrations_json = registrations.json() - registration_check = next( - (r for r in registrations_json if r["shift_id"] == str(volunteer_shift.id)), - None, - ) - assert registration_check is not None, registrations_json - - -async def test_data_exporter( - client: TestClient, -): - response = client.get( - "/competition/users/data-export?included_fields=purchases&included_fields=payments&included_fields=participants", - headers={"Authorization": f"Bearer {admin_token}"}, - ) - assert response.status_code == 200 +from datetime import UTC, datetime, timedelta +from uuid import UUID, uuid4 + +import pytest_asyncio +from fastapi.testclient import TestClient +from sqlalchemy import delete, update + +from app.core.groups.groups_type import GroupType +from app.core.schools import models_schools +from app.core.schools.schools_type import SchoolType +from app.core.users import models_users +from app.modules.sport_competition import models_sport_competition +from app.modules.sport_competition.schemas_sport_competition import ( + LocationBase, + MatchBase, + ParticipantInfo, + SportPodiumRankings, + TeamInfo, + TeamSportResultBase, + VolunteerShiftBase, +) +from app.modules.sport_competition.types_sport_competition import ( + CompetitionGroupType, + SportCategory, +) +from tests.commons import ( + add_object_to_db, + create_api_access_token, + create_user_with_groups, + get_TestingSessionLocal, +) + +school1: models_schools.CoreSchool +school2: models_schools.CoreSchool + +active_edition: models_sport_competition.CompetitionEdition +old_edition: models_sport_competition.CompetitionEdition + +admin_user: models_users.CoreUser +school_bds_user: models_users.CoreUser +sport_manager_user: models_users.CoreUser +user3: models_users.CoreUser +user4: models_users.CoreUser +admin_token: str +school_bds_token: str +sport_manager_token: str +user3_token: str +user4_token: str + +competition_user_admin: models_sport_competition.CompetitionUser +competition_user_school_bds: models_sport_competition.CompetitionUser +competition_user_sport_manager: models_sport_competition.CompetitionUser + +ecl_extension: models_sport_competition.SchoolExtension +school1_extension: models_sport_competition.SchoolExtension +ecl_general_quota: models_sport_competition.SchoolGeneralQuota +school_general_quota: models_sport_competition.SchoolGeneralQuota + +sport_free_quota: models_sport_competition.Sport +sport_used_quota: models_sport_competition.Sport +sport_with_team: models_sport_competition.Sport +sport_with_substitute: models_sport_competition.Sport +sport_feminine: models_sport_competition.Sport +ecl_sport_free_quota: models_sport_competition.SchoolSportQuota +ecl_sport_used_quota: models_sport_competition.SchoolSportQuota + +team_admin_user: models_sport_competition.CompetitionTeam +team1: models_sport_competition.CompetitionTeam +team2: models_sport_competition.CompetitionTeam + +participant1: models_sport_competition.CompetitionParticipant +participant2: models_sport_competition.CompetitionParticipant +participant3: models_sport_competition.CompetitionParticipant + +location: models_sport_competition.MatchLocation + +match1: models_sport_competition.Match + +podium_sport_free_quota: list[models_sport_competition.SportPodium] +podium_sport_with_team: list[models_sport_competition.SportPodium] + +volunteer_shift: models_sport_competition.VolunteerShift +volunteer_registration: models_sport_competition.VolunteerRegistration + + +async def create_competition_user( + edition_id: UUID, + school_id: UUID, + sport_category: SportCategory, +) -> tuple[models_users.CoreUser, models_sport_competition.CompetitionUser, str]: + new_user = await create_user_with_groups( + [], + school_id=school_id, + ) + new_competition_user = models_sport_competition.CompetitionUser( + user_id=new_user.id, + edition_id=edition_id, + sport_category=sport_category, + created_at=datetime.now(UTC), + validated=False, + is_athlete=True, + ) + await add_object_to_db(new_competition_user) + token = create_api_access_token(new_user) + return new_user, new_competition_user, token + + +@pytest_asyncio.fixture(scope="module", autouse=True) +async def init_objects() -> None: + global school1, school2, active_edition, old_edition + school1 = models_schools.CoreSchool( + id=uuid4(), + name="Emlyon Business School", + email_regex=r"^[\w.-]+@edu.emlyon.fr$", + ) + await add_object_to_db(school1) + school2 = models_schools.CoreSchool( + id=uuid4(), + name="Centrale Supelec", + email_regex=r"^[\w.-]+@edu.centralesupelec.fr$", + ) + await add_object_to_db(school2) + old_edition = models_sport_competition.CompetitionEdition( + id=uuid4(), + name="Edition 2023", + year=2023, + start_date=datetime(2023, 1, 1, tzinfo=UTC), + end_date=datetime(2023, 12, 31, tzinfo=UTC), + active=False, + inscription_enabled=False, + ) + await add_object_to_db(old_edition) + active_edition = models_sport_competition.CompetitionEdition( + id=uuid4(), + name="Edition 2024", + year=2024, + start_date=datetime(2024, 1, 1, tzinfo=UTC), + end_date=datetime(2024, 12, 31, tzinfo=UTC), + active=True, + inscription_enabled=True, + ) + await add_object_to_db(active_edition) + + global admin_user, school_bds_user, sport_manager_user, user3, user4 + admin_user = await create_user_with_groups( + [GroupType.competition_admin], + email="Admin User", + ) + school_bds_user = await create_user_with_groups( + [], + email="School BDS User", + school_id=school1.id, + ) + sport_manager_user = await create_user_with_groups( + [], + email="Sport Manager User", + ) + user3 = await create_user_with_groups( + [], + email="Random User", + ) + user4 = await create_user_with_groups( + [], + email="Another Random User", + ) + + global admin_token, school_bds_token, sport_manager_token, user3_token, user4_token + admin_token = create_api_access_token(admin_user) + school_bds_token = create_api_access_token(school_bds_user) + sport_manager_token = create_api_access_token(sport_manager_user) + user3_token = create_api_access_token(user3) + user4_token = create_api_access_token(user4) + + global \ + competition_user_admin, \ + competition_user_school_bds, \ + competition_user_sport_manager + competition_user_admin = models_sport_competition.CompetitionUser( + user_id=admin_user.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + is_volunteer=True, + validated=True, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_admin) + competition_user_school_bds = models_sport_competition.CompetitionUser( + user_id=school_bds_user.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + is_pompom=True, + validated=True, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_school_bds) + competition_user_sport_manager = models_sport_competition.CompetitionUser( + user_id=sport_manager_user.id, + sport_category=SportCategory.masculine, + edition_id=active_edition.id, + is_athlete=True, + is_cameraman=True, + validated=True, + created_at=datetime.now(UTC), + ) + await add_object_to_db(competition_user_sport_manager) + user1_bds_membership = models_sport_competition.CompetitionGroupMembership( + user_id=school_bds_user.id, + edition_id=active_edition.id, + group=CompetitionGroupType.schools_bds, + ) + await add_object_to_db(user1_bds_membership) + user2_sport_manager_membership = ( + models_sport_competition.CompetitionGroupMembership( + user_id=sport_manager_user.id, + edition_id=active_edition.id, + group=CompetitionGroupType.sport_manager, + ) + ) + await add_object_to_db(user2_sport_manager_membership) + user4_sport_manager_membership = ( + models_sport_competition.CompetitionGroupMembership( + user_id=user4.id, + edition_id=active_edition.id, + group=CompetitionGroupType.sport_manager, + ) + ) + await add_object_to_db(user4_sport_manager_membership) + + global ecl_extension, school1_extension + ecl_extension = models_sport_competition.SchoolExtension( + school_id=SchoolType.centrale_lyon.value, + from_lyon=True, + active=True, + inscription_enabled=False, + ) + await add_object_to_db(ecl_extension) + school1_extension = models_sport_competition.SchoolExtension( + school_id=school1.id, + from_lyon=False, + active=False, + inscription_enabled=False, + ) + await add_object_to_db(school1_extension) + + global ecl_general_quota, school_general_quota + ecl_general_quota = models_sport_competition.SchoolGeneralQuota( + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + athlete_quota=None, + cameraman_quota=None, + pompom_quota=None, + fanfare_quota=None, + athlete_cameraman_quota=None, + athlete_pompom_quota=None, + athlete_fanfare_quota=None, + non_athlete_cameraman_quota=None, + non_athlete_pompom_quota=None, + non_athlete_fanfare_quota=None, + ) + await add_object_to_db(ecl_general_quota) + school_general_quota = models_sport_competition.SchoolGeneralQuota( + school_id=school1.id, + edition_id=active_edition.id, + athlete_quota=1, + cameraman_quota=1, + pompom_quota=1, + fanfare_quota=1, + athlete_cameraman_quota=1, + athlete_pompom_quota=1, + athlete_fanfare_quota=1, + non_athlete_cameraman_quota=1, + non_athlete_pompom_quota=1, + non_athlete_fanfare_quota=1, + ) + await add_object_to_db(school_general_quota) + + global \ + sport_free_quota, \ + sport_used_quota, \ + sport_with_team, \ + sport_with_substitute, \ + sport_feminine + sport_free_quota = models_sport_competition.Sport( + id=uuid4(), + name="Free Quota Sport", + team_size=1, + substitute_max=0, + active=True, + sport_category=None, + ) + await add_object_to_db(sport_free_quota) + sport_used_quota = models_sport_competition.Sport( + id=uuid4(), + name="Used Quota Sport", + team_size=1, + substitute_max=0, + active=True, + sport_category=None, + ) + await add_object_to_db(sport_used_quota) + sport_with_team = models_sport_competition.Sport( + id=uuid4(), + name="Sport with Team", + team_size=5, + substitute_max=0, + active=True, + sport_category=None, + ) + await add_object_to_db(sport_with_team) + sport_with_substitute = models_sport_competition.Sport( + id=uuid4(), + name="Sport with Substitute", + team_size=5, + substitute_max=2, + active=True, + sport_category=None, + ) + await add_object_to_db(sport_with_substitute) + sport_feminine = models_sport_competition.Sport( + id=uuid4(), + name="Feminine Sport", + team_size=5, + substitute_max=2, + active=True, + sport_category=SportCategory.feminine, + ) + await add_object_to_db(sport_feminine) + + global ecl_sport_free_quota, ecl_sport_used_quota + ecl_sport_free_quota = models_sport_competition.SchoolSportQuota( + school_id=school1.id, + edition_id=active_edition.id, + sport_id=sport_free_quota.id, + participant_quota=2, + team_quota=1, + ) + await add_object_to_db(ecl_sport_free_quota) + ecl_sport_used_quota = models_sport_competition.SchoolSportQuota( + school_id=school1.id, + edition_id=active_edition.id, + sport_id=sport_used_quota.id, + participant_quota=0, + team_quota=1, + ) + await add_object_to_db(ecl_sport_used_quota) + + global team1, team2, team_admin_user + team1 = models_sport_competition.CompetitionTeam( + id=uuid4(), + sport_id=sport_with_team.id, + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + name="Team 1", + captain_id=sport_manager_user.id, + created_at=datetime.now(UTC), + ) + await add_object_to_db(team1) + team2 = models_sport_competition.CompetitionTeam( + id=uuid4(), + sport_id=sport_with_team.id, + school_id=school1.id, + edition_id=active_edition.id, + name="Team 2", + captain_id=school_bds_user.id, + created_at=datetime.now(UTC), + ) + await add_object_to_db(team2) + team_admin_user = models_sport_competition.CompetitionTeam( + id=uuid4(), + sport_id=sport_free_quota.id, + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + name="Admin Team", + captain_id=admin_user.id, + created_at=datetime.now(UTC), + ) + await add_object_to_db(team_admin_user) + + global participant1, participant2, participant3 + participant1 = models_sport_competition.CompetitionParticipant( + user_id=admin_user.id, + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_free_quota.id, + team_id=team_admin_user.id, + substitute=False, + license="1234567890", + certificate_file_id=None, + is_license_valid=True, + ) + await add_object_to_db(participant1) + participant2 = models_sport_competition.CompetitionParticipant( + user_id=sport_manager_user.id, + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + team_id=team1.id, + substitute=False, + license="0987654321", + certificate_file_id=None, + is_license_valid=True, + ) + await add_object_to_db(participant2) + ( + participant3_user, + _, + _, + ) = await create_competition_user( + edition_id=active_edition.id, + school_id=school1.id, + sport_category=SportCategory.masculine, + ) + participant3 = models_sport_competition.CompetitionParticipant( + user_id=participant3_user.id, + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + team_id=team1.id, + substitute=False, + license="1122334455", + certificate_file_id=None, + is_license_valid=True, + ) + await add_object_to_db(participant3) + + global location + location = models_sport_competition.MatchLocation( + id=uuid4(), + edition_id=active_edition.id, + name="Main Stadium", + address="123 Main St, City, Country", + latitude=45.764043, + longitude=4.835659, + description="Main stadium for the competition", + ) + await add_object_to_db(location) + + global match1 + match1 = models_sport_competition.Match( + id=uuid4(), + edition_id=active_edition.id, + sport_id=sport_with_team.id, + name="Match 1", + team1_id=team1.id, + team2_id=team2.id, + location_id=location.id, + date=datetime(2024, 6, 15, 15, 0, tzinfo=UTC), + score_team1=None, + score_team2=None, + winner_id=None, + ) + await add_object_to_db(match1) + + global podium_sport_free_quota, podium_sport_with_team + podium_sport_with_team = [ + models_sport_competition.SportPodium( + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + rank=1, + team_id=team_admin_user.id, + points=10, + ), + models_sport_competition.SportPodium( + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + rank=2, + team_id=team1.id, + points=5, + ), + models_sport_competition.SportPodium( + school_id=school1.id, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + rank=3, + team_id=team2.id, + points=2, + ), + ] + await add_object_to_db(podium_sport_with_team[0]) + await add_object_to_db(podium_sport_with_team[1]) + await add_object_to_db(podium_sport_with_team[2]) + podium_sport_free_quota = [ + models_sport_competition.SportPodium( + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_free_quota.id, + rank=1, + team_id=team_admin_user.id, + points=10, + ), + models_sport_competition.SportPodium( + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_free_quota.id, + rank=2, + team_id=team1.id, + points=5, + ), + models_sport_competition.SportPodium( + school_id=school1.id, + edition_id=active_edition.id, + sport_id=sport_free_quota.id, + rank=3, + team_id=team2.id, + points=2, + ), + ] + await add_object_to_db(podium_sport_free_quota[0]) + await add_object_to_db(podium_sport_free_quota[1]) + await add_object_to_db(podium_sport_free_quota[2]) + + global volunteer_shift, volunteer_registration + volunteer_shift = models_sport_competition.VolunteerShift( + id=uuid4(), + edition_id=active_edition.id, + name="Morning Shift", + description="Help with setup and registration", + value=2, + start_time=datetime(2024, 6, 15, 8, 0, tzinfo=UTC), + end_time=datetime(2024, 6, 15, 12, 0, tzinfo=UTC), + location="Main Entrance", + max_volunteers=1, + ) + await add_object_to_db(volunteer_shift) + volunteer_registration = models_sport_competition.VolunteerRegistration( + user_id=admin_user.id, + shift_id=volunteer_shift.id, + edition_id=active_edition.id, + registered_at=datetime.now(UTC), + validated=False, + ) + await add_object_to_db(volunteer_registration) + + +# region: Sports + + +async def test_get_sports( + client: TestClient, +) -> None: + response = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + editions = response.json() + assert len(editions) > 0 + + +async def test_create_sport_as_random( + client: TestClient, +) -> None: + response = client.post( + "/competition/sports", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "name": "Unauthorized Sport", + "team_size": 5, + "substitute_max": 2, + "active": True, + "sport_category": SportCategory.masculine.value, + }, + ) + assert response.status_code == 403, response.json() + + sports = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert sports.status_code == 200, sports.json() + sports_json = sports.json() + unauthorized_sport = next( + (s for s in sports_json if s["name"] == "Unauthorized Sport"), + None, + ) + assert unauthorized_sport is None + + +async def test_create_sport_as_admin( + client: TestClient, +) -> None: + response = client.post( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "New Sport", + "team_size": 5, + "substitute_max": 2, + "active": True, + "sport_category": SportCategory.masculine.value, + }, + ) + assert response.status_code == 201, response.json() + sport = response.json() + + sports = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert sports.status_code == 200, sports.json() + sports_json = sports.json() + new_sport = next( + (s for s in sports_json if s["id"] == sport["id"]), + None, + ) + assert new_sport is not None + + +async def test_create_sport_with_invalid_data( + client: TestClient, +) -> None: + response = client.post( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Invalid Sport", + "team_size": -1, # Invalid team size + "substitute_max": 2, + "active": True, + "sport_category": SportCategory.masculine.value, + }, + ) + assert response.status_code == 422, response.json() + + +async def test_create_sport_with_duplicate_name( + client: TestClient, +) -> None: + response = client.post( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": sport_free_quota.name, # Duplicate name + "team_size": 5, + "substitute_max": 2, + "active": True, + "sport_category": SportCategory.masculine.value, + }, + ) + assert response.status_code == 400, response.json() + + +async def test_patch_sport_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "name": "Unauthorized Update", + "team_size": 6, + "substitute_max": 3, + "active": True, + "sport_category": SportCategory.feminine.value, + }, + ) + assert response.status_code == 403, response.json() + + sports = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert sports.status_code == 200, sports.json() + sports_json = sports.json() + updated_sport_check = next( + (s for s in sports_json if s["id"] == str(sport_free_quota.id)), + None, + ) + assert updated_sport_check is not None + assert updated_sport_check["name"] == sport_free_quota.name + + +async def test_patch_sport_as_admin( + client: TestClient, +) -> None: + sport_to_modify = models_sport_competition.Sport( + id=uuid4(), + name="Sport to Modify", + team_size=5, + substitute_max=2, + active=True, + sport_category=SportCategory.masculine, + ) + await add_object_to_db(sport_to_modify) + response = client.patch( + f"/competition/sports/{sport_to_modify.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Updated Sport", + "team_size": 6, + "substitute_max": 3, + "active": True, + "sport_category": SportCategory.feminine.value, + }, + ) + assert response.status_code == 204, response.json() + + sports = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert sports.status_code == 200, sports.json() + sports_json = sports.json() + updated_sport_check = next( + (s for s in sports_json if s["id"] == str(sport_to_modify.id)), + None, + ) + assert updated_sport_check is not None + assert updated_sport_check["name"] == "Updated Sport" + + +async def test_patch_sport_with_duplicate_name( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": sport_used_quota.name, # Duplicate name + "team_size": 6, + "substitute_max": 3, + "active": True, + "sport_category": SportCategory.masculine.value, + }, + ) + assert response.status_code == 400, response.json() + + +async def test_delete_sport_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + sports = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert sports.status_code == 200, sports.json() + sports_json = sports.json() + deleted_sport_check = next( + (s for s in sports_json if s["id"] == str(sport_free_quota.id)), + None, + ) + assert deleted_sport_check is not None, sports.json() + + +async def test_delete_sport_active( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 400, response.json() + + +async def test_delete_sport_as_admin( + client: TestClient, +) -> None: + sport_to_delete = models_sport_competition.Sport( + id=uuid4(), + name="Sport to Delete", + team_size=5, + substitute_max=2, + active=False, + sport_category=SportCategory.masculine, + ) + await add_object_to_db(sport_to_delete) + + response = client.delete( + f"/competition/sports/{sport_to_delete.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + sports = client.get( + "/competition/sports", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert sports.status_code == 200, sports.json() + sports_json = sports.json() + deleted_sport_check = next( + (s for s in sports_json if s["id"] == str(sport_to_delete.id)), + None, + ) + assert deleted_sport_check is None + + +# endregion +# region: Editions + + +async def test_get_editions( + client: TestClient, +) -> None: + response = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + editions = response.json() + assert len(editions) > 0 + + +async def test_get_active_edition( + client: TestClient, +) -> None: + response = client.get( + "/competition/editions/active", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + edition_data = response.json() + assert edition_data["id"] == str(active_edition.id) + + +async def test_create_edition_as_admin( + client: TestClient, +) -> None: + response = client.post( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "New Competition Edition", + "year": 2025, + "start_date": "2024-01-01T00:00:00Z", + "end_date": "2024-12-31T23:59:59Z", + }, + ) + assert response.status_code == 201, response.json() + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + new_edition = next( + ( + edition + for edition in editions_json + if edition["name"] == "New Competition Edition" and edition["year"] == 2025 + ), + None, + ) + assert new_edition is not None + + +async def test_create_edition_as_random( + client: TestClient, +) -> None: + response = client.post( + "/competition/editions", + headers={"Authorization": f"Bearer {school_bds_token}"}, + json={ + "name": "Unauthorized Edition", + "year": 2025, + "start_date": "2024-01-01T00:00:00Z", + "end_date": "2024-12-31T23:59:59Z", + }, + ) + assert response.status_code == 403, response.json() + + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + unauthorized_edition = next( + ( + edition + for edition in editions_json + if edition["name"] == "Unauthorized Edition" and edition["year"] == 2025 + ), + None, + ) + assert unauthorized_edition is None + + +async def test_patch_edition_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/editions/{old_edition.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Updated Edition", + }, + ) + assert response.status_code == 204, response.json() + + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + updated_edition = next( + (edition for edition in editions_json if edition["id"] == str(old_edition.id)), + None, + ) + assert updated_edition is not None + assert updated_edition["name"] == "Updated Edition" + + +async def test_activate_edition( + client: TestClient, +) -> None: + response = client.post( + f"/competition/editions/{old_edition.id}/activate", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + activated_edition = next( + (edition for edition in editions_json if edition["id"] == str(old_edition.id)), + None, + ) + assert activated_edition is not None + assert activated_edition["active"] is True + client.post( + f"/competition/editions/{active_edition.id}/activate", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + +async def test_enable_inscription_not_active( + client: TestClient, +) -> None: + response = client.post( + f"/competition/editions/{old_edition.id}/inscription", + headers={"Authorization": f"Bearer {admin_token}"}, + json=True, + ) + assert response.status_code == 400, response.json() + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + enabled_edition = next( + (edition for edition in editions_json if edition["id"] == str(old_edition.id)), + None, + ) + assert enabled_edition is not None + assert enabled_edition["inscription_enabled"] is False + + +async def test_enable_inscription( + client: TestClient, +) -> None: + response = client.post( + f"/competition/editions/{active_edition.id}/inscription", + headers={"Authorization": f"Bearer {admin_token}"}, + json=True, + ) + assert response.status_code == 204, response.json() + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + enabled_edition = next( + ( + edition + for edition in editions_json + if edition["id"] == str(active_edition.id) + ), + None, + ) + assert enabled_edition is not None + assert enabled_edition["inscription_enabled"] is True, enabled_edition + + +async def test_patch_edition_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/editions/{active_edition.id}", + headers={"Authorization": f"Bearer {school_bds_token}"}, + json={ + "name": "Unauthorized Edition Update", + }, + ) + assert response.status_code == 403, response.json() + + editions = client.get( + "/competition/editions", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert editions.status_code == 200 + editions_json = editions.json() + updated_edition_check = next( + ( + edition + for edition in editions_json + if edition["id"] == str(active_edition.id) + ), + None, + ) + assert updated_edition_check is not None + assert updated_edition_check["name"] == active_edition.name + + +# endregion +# region: Schools + + +async def test_get_schools( + client: TestClient, +) -> None: + response = client.get( + "/competition/schools", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + schools = response.json() + assert len(schools) > 0 + assert any(school["school_id"] == str(school1.id) for school in schools) + + +async def test_post_school_extension_as_random( + client: TestClient, +) -> None: + response = client.post( + "/competition/schools", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "school_id": str(school2.id), + "from_lyon": False, + "active": True, + "inscription_enabled": False, + }, + ) + assert response.status_code == 403, response.json() + + schools = client.get( + "/competition/schools", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert schools.status_code == 200, schools.json() + schools_json = schools.json() + unauthorized_school = next( + (s for s in schools_json if s["school_id"] == str(school2.id)), + None, + ) + assert unauthorized_school is None, schools_json + + +async def test_post_school_extension_as_admin( + client: TestClient, +) -> None: + response = client.post( + "/competition/schools", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "school_id": str(school2.id), + "from_lyon": False, + "active": True, + "inscription_enabled": True, + }, + ) + assert response.status_code == 201, response.json() + + schools = client.get( + "/competition/schools", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert schools.status_code == 200, schools.json() + schools_json = schools.json() + new_school_extension = next( + (s for s in schools_json if s["school_id"] == str(school2.id)), + None, + ) + assert new_school_extension is not None, schools_json + + +async def test_patch_school_extension_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/schools/{school1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "from_lyon": False, + "active": True, + "inscription_enabled": True, + }, + ) + assert response.status_code == 403, response.json() + + schools = client.get( + "/competition/schools", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert schools.status_code == 200, schools.json() + schools_json = schools.json() + updated_school = next( + (s for s in schools_json if s["school_id"] == str(school1.id)), + None, + ) + assert updated_school is not None + assert updated_school["from_lyon"] is False + assert updated_school["active"] is False + assert updated_school["inscription_enabled"] is False + + +async def test_patch_school_extension_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/schools/{school1.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "from_lyon": True, + "active": True, + "inscription_enabled": True, + }, + ) + assert response.status_code == 204, response.json() + + schools = client.get( + "/competition/schools", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert schools.status_code == 200, schools.json() + schools_json = schools.json() + updated_school = next( + (s for s in schools_json if s["school_id"] == str(school1.id)), + None, + ) + assert updated_school is not None + assert updated_school["from_lyon"] is True + assert updated_school["active"] is True + assert updated_school["inscription_enabled"] is True + + +# endregion +# region: Competition Users + + +async def test_get_competition_users( + client: TestClient, +) -> None: + response = client.get( + "/competition/users", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + users = response.json() + assert len(users) > 0 + assert all(user["edition_id"] == str(active_edition.id) for user in users) + + +async def test_get_competition_users_by_school( + client: TestClient, +) -> None: + response = client.get( + f"/competition/users/schools/{school1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + users = response.json() + assert len(users) > 0 + assert all(user["edition_id"] == str(active_edition.id) for user in users) + assert all(user["user"]["school_id"] == str(school1.id) for user in users) + + +async def test_get_competition_user_by_id( + client: TestClient, +) -> None: + response = client.get( + f"/competition/users/{competition_user_admin.user_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + user = response.json() + assert user["user_id"] == str(competition_user_admin.user_id) + assert user["edition_id"] == str(competition_user_admin.edition_id) + assert competition_user_admin.sport_category is not None + assert user["sport_category"] == competition_user_admin.sport_category.value + + +async def test_post_competition_user( + client: TestClient, +) -> None: + response = client.post( + "/competition/users", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "sport_category": SportCategory.masculine.value, + "is_athlete": True, + }, + ) + assert response.status_code == 201, response.json() + user = response.json() + assert user["user_id"] == str(user3.id) + assert user["edition_id"] == str(active_edition.id) + assert user["sport_category"] == SportCategory.masculine.value + + +async def test_patch_competition_user_as_me( + client: TestClient, +) -> None: + response = client.patch( + "/competition/users/me", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "sport_category": SportCategory.feminine.value, + }, + ) + assert response.status_code == 204, response.json() + + user_response = client.get( + "/competition/users/me", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert user_response.status_code == 200, user_response.json() + user = user_response.json() + assert user["sport_category"] == SportCategory.feminine.value + + +async def test_patch_competition_user_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/users/{competition_user_admin.user_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "sport_category": SportCategory.masculine.value, + }, + ) + assert response.status_code == 204, response.json() + + user_response = client.get( + f"/competition/users/{competition_user_admin.user_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert user_response.status_code == 200, user_response.json() + user = user_response.json() + assert user["sport_category"] == SportCategory.masculine.value + + +# endregion +# region: Competition Groups + + +async def test_add_user_to_group_as_random( + client: TestClient, +) -> None: + response = client.post( + f"/competition/groups/{CompetitionGroupType.schools_bds.value}/users/{competition_user_admin.user_id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + user = client.get( + f"/competition/users/{competition_user_admin.user_id}/groups", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert user.status_code == 200, user.json() + user_json = user.json() + assert CompetitionGroupType.schools_bds.value not in [ + group["group"] for group in user_json + ], user_json + + +async def test_add_user_to_group_as_admin( + client: TestClient, +) -> None: + response = client.post( + f"/competition/groups/{CompetitionGroupType.schools_bds.value}/users/{competition_user_admin.user_id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 201, response.json() + + user = client.get( + "/competition/users/me/groups", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert user.status_code == 200, user.json() + user_json = user.json() + assert CompetitionGroupType.schools_bds.value in [ + group["group"] for group in user_json + ], user_json + + +async def test_remove_user_from_group_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/groups/{CompetitionGroupType.schools_bds.value}/users/{competition_user_school_bds.user_id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + user = client.get( + f"/competition/users/{competition_user_school_bds.user_id}/groups", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert user.status_code == 200, user.json() + user_json = user.json() + assert CompetitionGroupType.schools_bds.value in [ + group["group"] for group in user_json + ], user_json + + +# endregion +# region: School General Quotas + + +async def test_get_school_general_quota_as_random( + client: TestClient, +) -> None: + response = client.get( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + +async def test_get_school_general_quota( + client: TestClient, +) -> None: + response = client.get( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + school_quota_json = response.json() + assert school_quota_json["athlete_quota"] == 1, school_quota_json + assert school_quota_json["cameraman_quota"] == 1, school_quota_json + assert school_quota_json["pompom_quota"] == 1, school_quota_json + assert school_quota_json["fanfare_quota"] == 1, school_quota_json + + +async def test_get_school_general_quota_as_bds( + client: TestClient, +) -> None: + response = client.get( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {school_bds_token}"}, + ) + assert response.status_code == 200, response.json() + school_quota_json = response.json() + assert school_quota_json["athlete_quota"] == 1, school_quota_json + assert school_quota_json["cameraman_quota"] == 1, school_quota_json + assert school_quota_json["pompom_quota"] == 1, school_quota_json + assert school_quota_json["fanfare_quota"] == 1, school_quota_json + + +async def test_post_school_general_quota_as_random( + client: TestClient, +) -> None: + response = client.post( + f"/competition/schools/{school2.id}/general-quota", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "athlete_quota": 10, + "cameraman_quota": 5, + "pompom_quota": 3, + "fanfare_quota": 2, + }, + ) + assert response.status_code == 403, response.json() + + school_quota = client.get( + f"/competition/schools/{school2.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert school_quota.status_code == 404, school_quota.json() + + +async def test_post_school_general_quota_as_admin( + client: TestClient, +) -> None: + response = client.post( + f"/competition/schools/{school2.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "athlete_quota": 10, + "cameraman_quota": 5, + "pompom_quota": 3, + "fanfare_quota": 2, + }, + ) + assert response.status_code == 201, response.json() + + school_quota = client.get( + f"/competition/schools/{school2.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert school_quota.status_code == 200, school_quota.json() + school_quota_json = school_quota.json() + assert school_quota is not None, school_quota_json + assert school_quota_json["athlete_quota"] == 10, school_quota_json + assert school_quota_json["cameraman_quota"] == 5, school_quota_json + assert school_quota_json["pompom_quota"] == 3, school_quota_json + assert school_quota_json["fanfare_quota"] == 2, school_quota_json + + +async def test_patch_school_general_quota_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "athlete_quota": 5, + "cameraman_quota": 3, + "pompom_quota": 2, + "fanfare_quota": 1, + }, + ) + assert response.status_code == 403, response.json() + + school_quota = client.get( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert school_quota.status_code == 200, school_quota.json() + school_quota_json = school_quota.json() + assert school_quota_json["athlete_quota"] == 1, school_quota_json + assert school_quota_json["cameraman_quota"] == 1, school_quota_json + assert school_quota_json["pompom_quota"] == 1, school_quota_json + assert school_quota_json["fanfare_quota"] == 1, school_quota_json + + +async def test_patch_school_general_quota_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "athlete_quota": 5, + "cameraman_quota": 3, + "pompom_quota": 2, + "fanfare_quota": 1, + }, + ) + assert response.status_code == 204, response.json() + + school_quota = client.get( + f"/competition/schools/{school1.id}/general-quota", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert school_quota.status_code == 200, school_quota.json() + school_quota_json = school_quota.json() + assert school_quota_json["athlete_quota"] == 5, school_quota_json + assert school_quota_json["cameraman_quota"] == 3, school_quota_json + assert school_quota_json["pompom_quota"] == 2, school_quota_json + assert school_quota_json["fanfare_quota"] == 1, school_quota_json + + +# endregion +# region: Sport Quotas + + +async def test_get_school_sport_quota( + client: TestClient, +) -> None: + response = client.get( + f"/competition/schools/{school1.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + quotas = response.json() + assert len(quotas) > 0 + + +async def test_get_sport_quota( + client: TestClient, +) -> None: + response = client.get( + f"/competition/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + quota = response.json() + assert len(quota) > 0 + + +async def test_post_school_sport_quota_as_random( + client: TestClient, +) -> None: + response = client.post( + f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "participant_quota": 5, + "team_quota": 2, + }, + ) + assert response.status_code == 403, response.json() + + quota = client.get( + f"/competition/schools/{school2.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quota.status_code == 200, quota.json() + quota_json = quota.json() + sport_quota = next( + (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), + None, + ) + assert sport_quota is None, quota_json + + +async def test_post_school_sport_quota_as_admin( + client: TestClient, +) -> None: + response = client.post( + f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "participant_quota": 5, + "team_quota": 2, + }, + ) + assert response.status_code == 204, response.text + + quota = client.get( + f"/competition/schools/{school2.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quota.status_code == 200, quota.json() + quota_json = quota.json() + sport_quota = next( + (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), + None, + ) + assert sport_quota is not None, quota_json + assert sport_quota["participant_quota"] == 5, quota_json + assert sport_quota["team_quota"] == 2, quota_json + + +async def test_patch_school_sport_quota_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/schools/{school1.id}/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "participant_quota": 3, + "team_quota": 1, + }, + ) + assert response.status_code == 403, response.json() + + quota = client.get( + f"/competition/schools/{school1.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quota.status_code == 200, quota.json() + quota_json = quota.json() + sport_quota = next( + (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), + None, + ) + assert sport_quota is not None, quota_json + assert sport_quota["participant_quota"] == 2, quota_json + assert sport_quota["team_quota"] == 1, quota_json + + +async def test_patch_school_sport_quota_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "participant_quota": 3, + "team_quota": 1, + }, + ) + assert response.status_code == 204, response.json() + + quota = client.get( + f"/competition/schools/{school2.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quota.status_code == 200, quota.json() + quota_json = quota.json() + sport_quota = next( + (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), + None, + ) + assert sport_quota is not None, quota_json + assert sport_quota["participant_quota"] == 3, quota_json + assert sport_quota["team_quota"] == 1, quota_json + + +async def test_delete_school_sport_quota_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/schools/{school1.id}/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + quota = client.get( + f"/competition/schools/{school1.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quota.status_code == 200, quota.json() + quota_json = quota.json() + sport_quota = next( + (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), + None, + ) + assert sport_quota is not None, quota_json + + +async def test_delete_school_sport_quota_as_admin( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/schools/{school2.id}/sports/{sport_free_quota.id}/quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + quota = client.get( + f"/competition/schools/{school2.id}/sports-quotas", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert quota.status_code == 200, quota.json() + quota_json = quota.json() + sport_quota = next( + (q for q in quota_json if q["sport_id"] == str(sport_free_quota.id)), + None, + ) + assert sport_quota is None, quota_json + + +# endregion +# region: Teams + + +async def test_get_user_team_as_captain( + client: TestClient, +) -> None: + response = client.get( + "/competition/teams/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + team = response.json() + assert team["id"] == str(team_admin_user.id) + + +async def test_get_sport_teams( + client: TestClient, +) -> None: + response = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + teams = response.json() + assert len(teams) == 2 + + +async def test_get_school_teams( + client: TestClient, +) -> None: + response = client.get( + f"/competition/teams/schools/{school1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + teams = response.json() + assert len(teams) == 1 + + +async def test_get_sport_team_for_school( + client: TestClient, +) -> None: + response = client.get( + f"/competition/teams/sports/{sport_with_team.id}/schools/{school1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + teams = response.json() + assert len(teams) == 1 + + +async def test_create_team_different_captain( + client: TestClient, +) -> None: + team_info = TeamInfo( + name="New Team", + school_id=school1.id, + sport_id=sport_with_team.id, + captain_id=school_bds_user.id, + ) + response = client.post( + "/competition/teams", + headers={"Authorization": f"Bearer {user3_token}"}, + json=team_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + new_team_check = next( + (t for t in teams_json if t["captain_id"] == str(school_bds_user.id)), + None, + ) + assert new_team_check is None, teams_json + + +async def test_create_team_different_school( + client: TestClient, +) -> None: + team_info = TeamInfo( + name="New Team", + school_id=school2.id, + sport_id=sport_with_team.id, + captain_id=user3.id, + ) + response = client.post( + "/competition/teams", + headers={"Authorization": f"Bearer {user3_token}"}, + json=team_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + new_team_check = next( + (t for t in teams_json if t["school_id"] == str(school2.id)), + None, + ) + assert new_team_check is None, teams_json + + +async def test_create_team_for_sport_without_team( + client: TestClient, +) -> None: + team_info = TeamInfo( + name="New Team", + school_id=SchoolType.centrale_lyon.value, + sport_id=sport_free_quota.id, + captain_id=user3.id, + ) + response = client.post( + "/competition/teams", + headers={"Authorization": f"Bearer {user3_token}"}, + json=team_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 400, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + new_team_check = next((t for t in teams_json if t["name"] == "New Team"), None) + assert new_team_check is None, teams_json + + +async def test_create_team_no_quota( + client: TestClient, +) -> None: + strict_quota = models_sport_competition.SchoolSportQuota( + school_id=school1.id, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + participant_quota=0, + team_quota=0, + ) + await add_object_to_db(strict_quota) + + team_info = TeamInfo( + name="New Team", + school_id=school1.id, + sport_id=sport_with_team.id, + captain_id=school_bds_user.id, + ) + response = client.post( + "/competition/teams", + headers={"Authorization": f"Bearer {school_bds_token}"}, + json=team_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 400, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + new_team_check = next((t for t in teams_json if t["name"] == "New Team"), None) + assert new_team_check is None, teams_json + + +async def test_create_team_used_name( + client: TestClient, +) -> None: + team_info = TeamInfo( + name=team1.name, + school_id=SchoolType.centrale_lyon.value, + sport_id=sport_with_team.id, + captain_id=user3.id, + ) + response = client.post( + "/competition/teams", + headers={"Authorization": f"Bearer {user3_token}"}, + json=team_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 400, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + new_team_check = next((t for t in teams_json if t["name"] == team1.name), None) + assert new_team_check is not None, teams_json + + +async def test_create_team( + client: TestClient, +) -> None: + team_info = TeamInfo( + name="New Team", + school_id=SchoolType.centrale_lyon.value, + sport_id=sport_with_team.id, + captain_id=user3.id, + ) + response = client.post( + "/competition/teams", + headers={"Authorization": f"Bearer {user3_token}"}, + json=team_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + team = response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + new_team_check = next((t for t in teams_json if t["id"] == team["id"]), None) + assert new_team_check is not None, teams_json + + +async def test_patch_team_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/teams/{team1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "name": "Unauthorized Team Update", + }, + ) + assert response.status_code == 403, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + updated_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) + assert updated_team_check is not None + assert updated_team_check["name"] == team1.name + + +async def test_patch_team_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/teams/{team1.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Updated Team Name", + }, + ) + assert response.status_code == 204, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + updated_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) + assert updated_team_check is not None + assert updated_team_check["name"] == "Updated Team Name" + + +async def test_patch_team_as_captain( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/teams/{team1.id}", + headers={"Authorization": f"Bearer {sport_manager_token}"}, + json={ + "name": "Captain Updated Team Name", + }, + ) + assert response.status_code == 204, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + updated_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) + assert updated_team_check is not None + assert updated_team_check["name"] == "Captain Updated Team Name" + + +async def test_delete_team_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/teams/{team1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + deleted_team_check = next((t for t in teams_json if t["id"] == str(team1.id)), None) + assert deleted_team_check is not None, teams_json + + +async def test_delete_team_as_admin( + client: TestClient, +) -> None: + new_team = models_sport_competition.CompetitionTeam( + id=uuid4(), + name="Team to Delete", + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + captain_id=user3.id, + created_at=datetime.now(UTC), + ) + await add_object_to_db(new_team) + response = client.delete( + f"/competition/teams/{new_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + deleted_team_check = next( + (t for t in teams_json if t["id"] == str(new_team.id)), + None, + ) + assert deleted_team_check is None, teams_json + + +async def test_delete_team_as_captain( + client: TestClient, +) -> None: + new_team = models_sport_competition.CompetitionTeam( + id=uuid4(), + name="Team to Delete", + school_id=SchoolType.centrale_lyon.value, + edition_id=active_edition.id, + sport_id=sport_with_team.id, + captain_id=user3.id, + created_at=datetime.now(UTC), + ) + await add_object_to_db(new_team) + response = client.delete( + f"/competition/teams/{new_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 204, response.json() + + teams = client.get( + f"/competition/teams/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert teams.status_code == 200, teams.json() + teams_json = teams.json() + deleted_team_check = next( + (t for t in teams_json if t["id"] == str(new_team.id)), + None, + ) + assert deleted_team_check is None, teams_json + + +# endregion +# region: Participants + + +async def test_get_participant_me( + client: TestClient, +) -> None: + response = client.get( + "/competition/participants/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + user = response.json() + assert user["user_id"] == str(admin_user.id) + assert user["sport_id"] == str(sport_free_quota.id) + assert user["edition_id"] == str(active_edition.id) + + +async def test_get_participant_for_sport( + client: TestClient, +) -> None: + response = client.get( + f"/competition/participants/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + users = response.json() + assert len(users) > 0 + assert all(user["edition_id"] == str(active_edition.id) for user in users) + assert all(user["sport_id"] == str(sport_free_quota.id) for user in users) + + +async def test_get_participant_for_school_as_admin( + client: TestClient, +) -> None: + response = client.get( + f"/competition/participants/schools/{SchoolType.centrale_lyon.value}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200, response.json() + users = response.json() + assert len(users) > 0 + assert all(user["edition_id"] == str(active_edition.id) for user in users) + assert all( + user["school_id"] == str(SchoolType.centrale_lyon.value) for user in users + ) + + +async def test_get_participant_for_school_as_school_student( + client: TestClient, +) -> None: + response = client.get( + f"/competition/participants/schools/{SchoolType.centrale_lyon.value}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + users = response.json() + assert len(users) > 0 + assert all(user["edition_id"] == str(active_edition.id) for user in users) + assert all( + user["school_id"] == str(SchoolType.centrale_lyon.value) for user in users + ) + + +async def test_get_participant_for_school_as_random( + client: TestClient, +) -> None: + response = client.get( + f"/competition/participants/schools/{school1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + assert "Unauthorized action" in response.json()["detail"] + + +async def test_user_participate_with_invalid_category( + client: TestClient, +) -> None: + info = ParticipantInfo( + license="12345670089", + substitute=False, + ) + response = client.post( + f"/competition/sports/{sport_feminine.id}/participate", + headers={"Authorization": f"Bearer {school_bds_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + assert ( + "Sport category does not match user sport category" in response.json()["detail"] + ) + + participants = client.get( + f"/competition/participants/sports/{sport_feminine.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + assert not any( + p["user_id"] == str(school_bds_user.id) for p in participants_json + ), participants_json + + +async def test_user_participate_without_team( + client: TestClient, +) -> None: + info = ParticipantInfo( + license="12345670089", + substitute=False, + ) + response = client.post( + f"/competition/sports/{sport_with_team.id}/participate", + headers={"Authorization": f"Bearer {user3_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 400, response.json() + assert "Sport declared needs to be played in a team" in response.json()["detail"] + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( + participants_json + ) + + +async def test_user_participate_with_invalid_team_school( + client: TestClient, +) -> None: + info = ParticipantInfo( + license="12345670089", + substitute=False, + team_id=team2.id, # team2 is from a different school + ) + response = client.post( + f"/competition/sports/{sport_with_team.id}/participate", + headers={"Authorization": f"Bearer {user3_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + assert ( + "Unauthorized action, team does not belong to user school" + in response.json()["detail"] + ) + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( + participants_json + ) + + +async def test_user_participate_with_unknown_team( + client: TestClient, +) -> None: + info = ParticipantInfo( + license="12345670089", + substitute=False, + team_id=uuid4(), + ) + response = client.post( + f"/competition/sports/{sport_with_team.id}/participate", + headers={"Authorization": f"Bearer {user3_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 404, response.json() + assert "Team not found in the database" in response.json()["detail"] + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( + participants_json + ) + + +async def test_user_participate_with_maximum_team_size( + client: TestClient, +) -> None: + new_user, _, new_user_token = await create_competition_user( + edition_id=active_edition.id, + school_id=SchoolType.centrale_lyon.value, + sport_category=SportCategory.masculine, + ) + async with get_TestingSessionLocal()() as db: + await db.execute( + update(models_sport_competition.Sport) + .where( + models_sport_competition.Sport.id == sport_with_team.id, + ) + .values( + team_size=2, # Set team size to 1 for testing + ), + ) + await db.commit() + + info = ParticipantInfo( + license="12345670089", + substitute=False, + team_id=team1.id, # team1 is from the same school + ) + response = client.post( + f"/competition/sports/{sport_with_team.id}/participate", + headers={"Authorization": f"Bearer {new_user_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 400, response.json() + assert "Maximum number of players in the team reached" in response.json()["detail"] + + async with get_TestingSessionLocal()() as db: + await db.execute( + update(models_sport_competition.Sport) + .where( + models_sport_competition.Sport.id == sport_with_team.id, + ) + .values( + team_size=5, # Reset team size to a valid number + ), + ) + await db.commit() + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + assert not any(p["user_id"] == str(new_user.id) for p in participants_json), ( + participants_json + ) + + +async def test_user_participate_with_maximum_substitute_size( + client: TestClient, +) -> None: + info = ParticipantInfo( + license="12345670089", + substitute=True, + team_id=team1.id, # team1 is from the same school + ) + response = client.post( + f"/competition/sports/{sport_with_team.id}/participate", + headers={"Authorization": f"Bearer {user3_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 400, response.json() + assert ( + "Maximum number of substitutes in the team reached" in response.json()["detail"] + ) + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + assert not any(p["user_id"] == str(user3.id) for p in participants_json), ( + participants_json + ) + + +async def test_user_participate_with_valid_data( + client: TestClient, +) -> None: + info = ParticipantInfo( + license="12345670089", + substitute=False, + ) + response = client.post( + f"/competition/sports/{sport_free_quota.id}/participate", + headers={"Authorization": f"Bearer {user3_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + + participants = client.get( + f"/competition/participants/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + user_participation = next( + (u for u in participants_json if u["user_id"] == str(user3.id)), + None, + ) + assert user_participation is not None, participants_json + assert user_participation["edition_id"] == str(active_edition.id) + + +async def test_user_participate_with_team( + client: TestClient, +) -> None: + async with get_TestingSessionLocal()() as db: + await db.execute( + delete(models_sport_competition.CompetitionParticipant).where( + models_sport_competition.CompetitionParticipant.user_id == user3.id, + ), + ) + await db.commit() + info = ParticipantInfo( + license="12345670089", + substitute=False, + team_id=team1.id, # team1 is from the same school + ) + response = client.post( + f"/competition/sports/{sport_with_team.id}/participate", + headers={"Authorization": f"Bearer {user3_token}"}, + json=info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + user_participation = next( + (u for u in participants_json if u["user_id"] == str(user3.id)), + None, + ) + assert user_participation is not None, participants_json + assert user_participation["edition_id"] == str(active_edition.id) + + +async def test_add_user_certificate( + client: TestClient, +): + file = b"this is a test file" + file_content = { + "certificate": ("test_certificate.pdf", file, "application/pdf"), + } + response = client.post( + f"/competition/participants/sports/{sport_free_quota.id}/certificate", + headers={"Authorization": f"Bearer {admin_token}"}, + files=file_content, + ) + assert response.status_code == 204, response.json() + + participants = client.get( + f"/competition/participants/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + user_participation = next( + (u for u in participants_json if u["user_id"] == str(admin_user.id)), + None, + ) + assert user_participation is not None, participants_json + assert user_participation["certificate_file_id"] is not None, participants_json + + file_response = client.get( + f"/competition/participants/users/{admin_user.id}/certificate", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert file_response.status_code == 200, file_response.json() + assert file_response.content == file, file_response.json() + + +async def test_delete_user_certificate( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/participants/sports/{sport_free_quota.id}/certificate", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + participants = client.get( + f"/competition/participants/sports/{sport_free_quota.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + user_participation = next( + (u for u in participants_json if u["user_id"] == str(admin_user.id)), + None, + ) + assert user_participation is not None, participants_json + assert user_participation["certificate_file_id"] is None, participants_json + + +async def test_user_withdraw_participation( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/sports/{sport_with_team.id}/withdraw", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 204, response.json() + + participants = client.get( + f"/competition/participants/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert participants.status_code == 200, participants.json() + participants_json = participants.json() + user_participation = next( + (u for u in participants_json if u["user_id"] == str(user3.id)), + None, + ) + assert user_participation is None, participants_json + + +# endregion +# region: Locations + + +async def test_get_locations( + client: TestClient, +) -> None: + response = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + locations = response.json() + assert len(locations) == 1 + assert locations[0]["name"] == "Main Stadium" + + +async def test_get_location_by_id( + client: TestClient, +) -> None: + response = client.get( + f"/competition/locations/{location.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + location_json = response.json() + assert location_json["name"] == "Main Stadium" + assert len(location_json["matches"]) == 1 + + +async def test_post_location_as_random( + client: TestClient, +) -> None: + location_info = LocationBase( + name="New Location", + description="A new location for testing", + address="123 Main St", + latitude=45.0, + longitude=4.0, + ) + response = client.post( + "/competition/locations", + headers={"Authorization": f"Bearer {user3_token}"}, + json=location_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + + locations = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert locations.status_code == 200, locations.json() + locations_json = locations.json() + new_location_check = next( + (location for location in locations_json if location["name"] == "New Location"), + None, + ) + assert new_location_check is None, locations_json + + +async def test_post_location_as_admin( + client: TestClient, +) -> None: + location_info = LocationBase( + name="New Location", + description="A new location for testing", + address="123 Main St", + latitude=45.0, + longitude=4.0, + ) + response = client.post( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + json=location_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + location = response.json() + + locations = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert locations.status_code == 200, locations.json() + locations_json = locations.json() + new_location_check = next( + (loc for loc in locations_json if loc["id"] == location["id"]), + None, + ) + assert new_location_check is not None, locations_json + + +async def test_patch_location_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/locations/{location.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "name": "Unauthorized Location Update", + }, + ) + assert response.status_code == 403, response.json() + + locations = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert locations.status_code == 200, locations.json() + locations_json = locations.json() + updated_location_check = next( + (loc for loc in locations_json if loc["id"] == str(location.id)), + None, + ) + assert updated_location_check is not None + assert updated_location_check["name"] == location.name + + +async def test_patch_location_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/locations/{location.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Updated Location Name", + }, + ) + assert response.status_code == 204, response.json() + + locations = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert locations.status_code == 200, locations.json() + locations_json = locations.json() + updated_location_check = next( + (loc for loc in locations_json if loc["id"] == str(location.id)), + None, + ) + assert updated_location_check is not None + assert updated_location_check["name"] == "Updated Location Name" + + +async def delete_location_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/locations/{location.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + locations = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert locations.status_code == 200, locations.json() + locations_json = locations.json() + deleted_location_check = next( + (loc for loc in locations_json if loc["id"] == str(location.id)), + None, + ) + assert deleted_location_check is not None, locations_json + + +async def test_delete_location_as_admin( + client: TestClient, +) -> None: + new_location = models_sport_competition.MatchLocation( + id=uuid4(), + name="Location to Delete", + description="A location to delete", + address="456 Secondary St", + edition_id=active_edition.id, + latitude=46.0, + longitude=5.0, + ) + await add_object_to_db(new_location) + response = client.delete( + f"/competition/locations/{new_location.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + locations = client.get( + "/competition/locations", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert locations.status_code == 200, locations.json() + locations_json = locations.json() + deleted_location_check = next( + (loc for loc in locations_json if loc["id"] == str(new_location.id)), + None, + ) + assert deleted_location_check is None, locations_json + + +# endregion +# region: Matches + + +async def test_gest_sport_matches( + client: TestClient, +) -> None: + response = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + matches = response.json() + assert len(matches) == 1 + assert matches[0]["name"] == "Match 1" + + +async def test_gest_school_matches( + client: TestClient, +) -> None: + response = client.get( + f"/competition/matches/schools/{school1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + matches = response.json() + assert len(matches) == 1 + assert matches[0]["name"] == "Match 1" + + +async def test_create_match_as_random( + client: TestClient, +) -> None: + match_info = MatchBase( + name="New Match", + team1_id=team1.id, + team2_id=team2.id, + description="A new match for testing", + sport_id=sport_with_team.id, + location_id=location.id, + date=datetime.now(UTC), + ) + response = client.post( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json=match_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + new_match_check = next( + (match for match in matches_json if match["name"] == "New Match"), + None, + ) + assert new_match_check is None, matches_json + + +async def test_create_match_as_admin( + client: TestClient, +) -> None: + match_info = MatchBase( + name="New Match", + team1_id=team1.id, + team2_id=team2.id, + description="A new match for testing", + sport_id=sport_with_team.id, + location_id=location.id, + date=datetime(2024, 6, 15, 15, 0, 0, tzinfo=UTC), + ) + response = client.post( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json=match_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + match = response.json() + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + new_match_check = next( + (m for m in matches_json if m["id"] == match["id"]), + None, + ) + assert new_match_check is not None, matches_json + + +async def test_patch_match_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/matches/{match1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "name": "Unauthorized Match Update", + }, + ) + assert response.status_code == 403, response.json() + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + updated_match_check = next( + (m for m in matches_json if m["id"] == str(match1.id)), + None, + ) + assert updated_match_check is not None + assert updated_match_check["name"] == match1.name + + +async def test_patch_match_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/matches/{match1.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Updated Match Name", + "score_team1": 3, + "score_team2": 2, + "winner_id": str(team1.id), + }, + ) + assert response.status_code == 204, response.json() + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + updated_match_check = next( + (m for m in matches_json if m["id"] == str(match1.id)), + None, + ) + assert updated_match_check is not None + assert updated_match_check["name"] == "Updated Match Name" + assert updated_match_check["score_team1"] == 3 + assert updated_match_check["score_team2"] == 2 + assert updated_match_check["winner_id"] == str(team1.id) + + +async def test_edit_match_as_sport_manager( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/matches/{match1.id}", + headers={"Authorization": f"Bearer {user4_token}"}, + json={ + "name": "Sport Manager Updated Match Name", + "score_team1": 1, + "score_team2": 1, + "winner_id": None, + }, + ) + assert response.status_code == 204, response.json() + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + updated_match_check = next( + (m for m in matches_json if m["id"] == str(match1.id)), + None, + ) + assert updated_match_check is not None + assert updated_match_check["name"] == "Sport Manager Updated Match Name" + assert updated_match_check["score_team1"] == 1 + assert updated_match_check["score_team2"] == 1 + assert updated_match_check["winner_id"] is None + + +async def test_delete_match_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/matches/{match1.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.text + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + deleted_match_check = next( + (m for m in matches_json if m["id"] == str(match1.id)), + None, + ) + assert deleted_match_check is not None, matches_json + + +async def test_delete_match_as_admin( + client: TestClient, +) -> None: + new_match = models_sport_competition.Match( + id=uuid4(), + name="Match to Delete", + team1_id=team1.id, + team2_id=team2.id, + sport_id=sport_with_team.id, + location_id=location.id, + edition_id=active_edition.id, + date=datetime.now(UTC), + score_team1=None, + score_team2=None, + winner_id=None, + ) + await add_object_to_db(new_match) + response = client.delete( + f"/competition/matches/{new_match.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.text + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + deleted_match_check = next( + (m for m in matches_json if m["id"] == str(new_match.id)), + None, + ) + assert deleted_match_check is None, matches_json + + +async def test_delete_match_as_sport_manager( + client: TestClient, +) -> None: + new_match = models_sport_competition.Match( + id=uuid4(), + name="Match to Delete", + team1_id=team1.id, + team2_id=team2.id, + sport_id=sport_with_team.id, + location_id=location.id, + edition_id=active_edition.id, + date=datetime.now(UTC), + score_team1=None, + score_team2=None, + winner_id=None, + ) + await add_object_to_db(new_match) + response = client.delete( + f"/competition/matches/{new_match.id}", + headers={"Authorization": f"Bearer {user4_token}"}, + ) + assert response.status_code == 204, response.text + + matches = client.get( + f"/competition/matches/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert matches.status_code == 200, matches.json() + matches_json = matches.json() + deleted_match_check = next( + (m for m in matches_json if m["id"] == str(new_match.id)), + None, + ) + assert deleted_match_check is None, matches_json + + +# endregion +# region: Podiums +async def test_get_global_podiums( + client: TestClient, +) -> None: + response = client.get( + "/competition/podiums/global", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + podiums = response.json() + assert len(podiums) == 2 + centrale_score = next( + (p for p in podiums if p["school_id"] == str(SchoolType.centrale_lyon.value)), + None, + ) + assert centrale_score is not None + assert centrale_score["total_points"] == 30 + other_school_score = next( + (p for p in podiums if p["school_id"] == str(school1.id)), + None, + ) + assert other_school_score is not None + assert other_school_score["total_points"] == 4 + + +async def test_get_sport_podiums( + client: TestClient, +) -> None: + response = client.get( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 200, response.json() + podiums = response.json() + assert len(podiums) == 3 + + +async def test_post_podium_as_random( + client: TestClient, +) -> None: + podium_info = SportPodiumRankings( + rankings=[ + TeamSportResultBase( + sport_id=sport_with_team.id, + school_id=SchoolType.centrale_lyon.value, + points=15, + team_id=team1.id, + ), + ], + ) + + response = client.post( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json=podium_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + + podiums = client.get( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert podiums.status_code == 200, podiums.json() + podiums_json = podiums.json() + podium_check = next( + (p for p in podiums_json if p["points"] == 15), + None, + ) + assert podium_check is None, podiums_json + + +async def test_post_podium_as_sport_manager( + client: TestClient, +) -> None: + podium_info = SportPodiumRankings( + rankings=[ + TeamSportResultBase( + sport_id=sport_with_team.id, + school_id=SchoolType.centrale_lyon.value, + points=15, + team_id=team1.id, + ), + TeamSportResultBase( + sport_id=sport_with_team.id, + school_id=school1.id, + points=10, + team_id=team2.id, + ), + TeamSportResultBase( + sport_id=sport_with_team.id, + school_id=SchoolType.centrale_lyon.value, + points=5, + team_id=team_admin_user.id, + ), + ], + ) + + response = client.post( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user4_token}"}, + json=podium_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + + podiums = client.get( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert podiums.status_code == 200, podiums.json() + podiums_json = podiums.json() + podium_check = next( + (p for p in podiums_json if p["points"] == 15), + None, + ) + assert podium_check is not None, podiums_json + podium_check = next( + (p for p in podiums_json if p["points"] == 10), + None, + ) + assert podium_check is not None, podiums_json + podium_check = next( + (p for p in podiums_json if p["points"] == 5), + None, + ) + assert podium_check is not None, podiums_json + + +async def test_delete_podium_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + podiums = client.get( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert podiums.status_code == 200, podiums.json() + podiums_json = podiums.json() + assert len(podiums_json) == 3, podiums_json + + +async def test_delete_podium_as_sport_manager( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {user4_token}"}, + ) + assert response.status_code == 204, response.json() + + podiums = client.get( + f"/competition/podiums/sports/{sport_with_team.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert podiums.status_code == 200, podiums.json() + podiums_json = podiums.json() + assert len(podiums_json) == 0, podiums_json + + +# endregion +# region: Volunteers Shifts + + +async def test_get_volunteer_shifts( + client: TestClient, +) -> None: + response = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + + assert response.status_code == 200, response.json() + shifts = response.json() + assert len(shifts) == 1 + assert shifts[0]["name"] == "Morning Shift" + + +async def test_create_volunteer_shift_as_random( + client: TestClient, +) -> None: + shift_info = VolunteerShiftBase( + name="New Shift", + description="A new shift for testing", + value=1, + start_time=datetime.now(UTC), + end_time=datetime.now(UTC) + timedelta(hours=2), + location="Event Hall", + max_volunteers=5, + ) + response = client.post( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {user3_token}"}, + json=shift_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 403, response.json() + + shifts = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert shifts.status_code == 200, shifts.json() + shifts_json = shifts.json() + new_shift_check = next( + (shift for shift in shifts_json if shift["name"] == "New Shift"), + None, + ) + assert new_shift_check is None, shifts_json + + +async def test_create_volunteer_shift_as_admin( + client: TestClient, +) -> None: + shift_info = VolunteerShiftBase( + name="New Shift", + description="A new shift for testing", + value=1, + start_time=datetime.now(UTC), + end_time=datetime.now(UTC) + timedelta(hours=2), + location="Event Hall", + max_volunteers=5, + ) + response = client.post( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + json=shift_info.model_dump(exclude_none=True, mode="json"), + ) + assert response.status_code == 201, response.json() + shift = response.json() + + shifts = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert shifts.status_code == 200, shifts.json() + shifts_json = shifts.json() + new_shift_check = next( + (s for s in shifts_json if s["id"] == shift["id"]), + None, + ) + assert new_shift_check is not None, shifts_json + + +async def test_patch_volunteer_shift_as_random( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/volunteers/shifts/{volunteer_shift.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + json={ + "name": "Unauthorized Shift Update", + }, + ) + assert response.status_code == 403, response.json() + + shifts = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert shifts.status_code == 200, shifts.json() + shifts_json = shifts.json() + updated_shift_check = next( + (s for s in shifts_json if s["id"] == str(volunteer_shift.id)), + None, + ) + assert updated_shift_check is not None + assert updated_shift_check["name"] == volunteer_shift.name + + +async def test_patch_volunteer_shift_as_admin( + client: TestClient, +) -> None: + response = client.patch( + f"/competition/volunteers/shifts/{volunteer_shift.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + json={ + "name": "Updated Shift Name", + }, + ) + assert response.status_code == 204, response.json() + + shifts = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert shifts.status_code == 200, shifts.json() + shifts_json = shifts.json() + updated_shift_check = next( + (s for s in shifts_json if s["id"] == str(volunteer_shift.id)), + None, + ) + assert updated_shift_check is not None + assert updated_shift_check["name"] == "Updated Shift Name" + + +async def test_delete_volunteer_shift_as_random( + client: TestClient, +) -> None: + response = client.delete( + f"/competition/volunteers/shifts/{volunteer_shift.id}", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + + shifts = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert shifts.status_code == 200, shifts.json() + shifts_json = shifts.json() + deleted_shift_check = next( + (s for s in shifts_json if s["id"] == str(volunteer_shift.id)), + None, + ) + assert deleted_shift_check is not None, shifts_json + + +async def test_delete_volunteer_shift_as_admin( + client: TestClient, +) -> None: + new_shift = models_sport_competition.VolunteerShift( + id=uuid4(), + name="Shift to Delete", + description="A shift to delete", + value=1, + start_time=datetime.now(UTC), + end_time=datetime.now(UTC) + timedelta(hours=2), + location="Event Hall", + max_volunteers=5, + edition_id=active_edition.id, + ) + await add_object_to_db(new_shift) + new_registration = models_sport_competition.VolunteerRegistration( + shift_id=new_shift.id, + user_id=user3.id, + edition_id=active_edition.id, + registered_at=datetime.now(UTC), + validated=True, + ) + await add_object_to_db(new_registration) + response = client.delete( + f"/competition/volunteers/shifts/{new_shift.id}", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + shifts = client.get( + "/competition/volunteers/shifts", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert shifts.status_code == 200, shifts.json() + shifts_json = shifts.json() + deleted_shift_check = next( + (s for s in shifts_json if s["id"] == str(new_shift.id)), + None, + ) + assert deleted_shift_check is None, shifts_json + + registrations = client.get( + "/competition/volunteers/me", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert registrations.status_code == 200, registrations.json() + registrations_json = registrations.json() + deleted_registration_check = next( + (r for r in registrations_json if r["shift_id"] == str(new_shift.id)), + None, + ) + assert deleted_registration_check is None, registrations_json + + +# endregion +# region: Volunteer Registrations + + +async def test_get_own_volunteer_registrations( + client: TestClient, +) -> None: + response = client.get( + "/competition/volunteers/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + + assert response.status_code == 200, response.json() + registrations = response.json() + assert len(registrations) == 1 + assert registrations[0]["shift_id"] == str(volunteer_shift.id) + + +async def test_register_to_shift_as_non_volunteer( + client: TestClient, +) -> None: + response = client.post( + f"/competition/volunteers/shifts/{volunteer_shift.id}/register", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 403, response.json() + assert ( + "You must be registered for the competition as a volunteer to register for a volunteer shift" + in response.json()["detail"] + ) + + registrations = client.get( + "/competition/volunteers/me", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert registrations.status_code == 200, registrations.json() + registrations_json = registrations.json() + registration_check = next( + (r for r in registrations_json if r["shift_id"] == str(volunteer_shift.id)), + None, + ) + assert registration_check is None, registrations_json + + +async def test_register_already_registered_to_shift( + client: TestClient, +) -> None: + response = client.post( + f"/competition/volunteers/shifts/{volunteer_shift.id}/register", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 400, response.json() + assert ( + "You are already registered to this volunteer shift." + in response.json()["detail"] + ) + + +async def test_register_to_full_shift( + client: TestClient, +) -> None: + async with get_TestingSessionLocal()() as db: + await db.execute( + update(models_sport_competition.CompetitionUser) + .where(models_sport_competition.CompetitionUser.user_id == user3.id) + .values(is_volunteer=True), + ) + await db.commit() + + response = client.post( + f"/competition/volunteers/shifts/{volunteer_shift.id}/register", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert response.status_code == 400 + assert "This volunteer shift is full." in response.json()["detail"] + + registrations = client.get( + "/competition/volunteers/me", + headers={"Authorization": f"Bearer {user3_token}"}, + ) + assert registrations.status_code == 200, registrations.json() + registrations_json = registrations.json() + registration_check = next( + (r for r in registrations_json if r["shift_id"] == str(volunteer_shift.id)), + None, + ) + assert registration_check is None, registrations_json + + +async def test_register_to_volunteer_shift( + client: TestClient, +) -> None: + new_shift = models_sport_competition.VolunteerShift( + id=uuid4(), + name="Another Shift", + description="Another shift for testing", + value=1, + start_time=datetime.now(UTC) + timedelta(days=1), + end_time=datetime.now(UTC) + timedelta(days=1, hours=2), + location="Event Hall", + max_volunteers=5, + edition_id=active_edition.id, + ) + await add_object_to_db(new_shift) + + response = client.post( + f"/competition/volunteers/shifts/{new_shift.id}/register", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 204, response.json() + + registrations = client.get( + "/competition/volunteers/me", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert registrations.status_code == 200, registrations.json() + registrations_json = registrations.json() + registration_check = next( + (r for r in registrations_json if r["shift_id"] == str(volunteer_shift.id)), + None, + ) + assert registration_check is not None, registrations_json + + +async def test_data_exporter( + client: TestClient, +): + response = client.get( + "/competition/users/data-export?included_fields=purchases&included_fields=payments&included_fields=participants", + headers={"Authorization": f"Bearer {admin_token}"}, + ) + assert response.status_code == 200 diff --git a/tests/sport_competition/test_validation.py b/tests/modules/sport_competition/test_validation.py similarity index 100% rename from tests/sport_competition/test_validation.py rename to tests/modules/sport_competition/test_validation.py diff --git a/tests/test_PH.py b/tests/modules/test_PH.py similarity index 100% rename from tests/test_PH.py rename to tests/modules/test_PH.py diff --git a/tests/test_advert.py b/tests/modules/test_advert.py similarity index 100% rename from tests/test_advert.py rename to tests/modules/test_advert.py diff --git a/tests/test_amap.py b/tests/modules/test_amap.py similarity index 100% rename from tests/test_amap.py rename to tests/modules/test_amap.py diff --git a/tests/test_booking.py b/tests/modules/test_booking.py similarity index 100% rename from tests/test_booking.py rename to tests/modules/test_booking.py diff --git a/tests/test_calendar.py b/tests/modules/test_calendar.py similarity index 100% rename from tests/test_calendar.py rename to tests/modules/test_calendar.py diff --git a/tests/test_campaign.py b/tests/modules/test_campaign.py similarity index 100% rename from tests/test_campaign.py rename to tests/modules/test_campaign.py diff --git a/tests/test_cdr.py b/tests/modules/test_cdr.py similarity index 100% rename from tests/test_cdr.py rename to tests/modules/test_cdr.py diff --git a/tests/test_cdr_result.py b/tests/modules/test_cdr_result.py similarity index 100% rename from tests/test_cdr_result.py rename to tests/modules/test_cdr_result.py diff --git a/tests/test_cinema.py b/tests/modules/test_cinema.py similarity index 100% rename from tests/test_cinema.py rename to tests/modules/test_cinema.py diff --git a/tests/test_flappybird.py b/tests/modules/test_flappybird.py similarity index 100% rename from tests/test_flappybird.py rename to tests/modules/test_flappybird.py diff --git a/tests/test_loan.py b/tests/modules/test_loan.py similarity index 100% rename from tests/test_loan.py rename to tests/modules/test_loan.py diff --git a/tests/test_myeclpay.py b/tests/modules/test_myeclpay.py similarity index 100% rename from tests/test_myeclpay.py rename to tests/modules/test_myeclpay.py diff --git a/tests/test_payment.py b/tests/modules/test_payment.py similarity index 100% rename from tests/test_payment.py rename to tests/modules/test_payment.py diff --git a/tests/test_phonebook.py b/tests/modules/test_phonebook.py similarity index 100% rename from tests/test_phonebook.py rename to tests/modules/test_phonebook.py diff --git a/tests/test_raffle.py b/tests/modules/test_raffle.py similarity index 100% rename from tests/test_raffle.py rename to tests/modules/test_raffle.py diff --git a/tests/test_raid.py b/tests/modules/test_raid.py similarity index 100% rename from tests/test_raid.py rename to tests/modules/test_raid.py diff --git a/tests/test_recommendation.py b/tests/modules/test_recommendation.py similarity index 100% rename from tests/test_recommendation.py rename to tests/modules/test_recommendation.py diff --git a/tests/test_seed_library.py b/tests/modules/test_seed_library.py similarity index 100% rename from tests/test_seed_library.py rename to tests/modules/test_seed_library.py diff --git a/tests/core/test_factories.py b/tests/test_factories.py similarity index 100% rename from tests/core/test_factories.py rename to tests/test_factories.py diff --git a/tests/core/test_migrations.py b/tests/test_migrations.py similarity index 100% rename from tests/core/test_migrations.py rename to tests/test_migrations.py diff --git a/tests_script.py b/tests_script.py index 7d24dd3767..f8f9e637a4 100644 --- a/tests_script.py +++ b/tests_script.py @@ -44,14 +44,28 @@ def is_module_scope_only(changed_files): ) -def get_modules_tests_patterns(modules, coverage=True, run_all=False): +def get_modules_tests_patterns(modules): """Run pytest with coverage on core + modified modules.""" patterns = [] for mod in modules: - path = f"tests/test_{mod}*.py" - if Path(path).exists(): - patterns.append(path) - else: + # Check for tests/modules/test_mod*.py pattern + path1 = f"tests/modules/test_{mod}*.py" + # Check for tests/modules/mod/ directory pattern + path2 = f"tests/modules/{mod}/" + + found_tests = False + + # Check if direct test files exist + if list(Path(".").glob(path1)): + patterns.append(path1) + found_tests = True + + # Check if module directory with tests exists + if Path(path2).exists() and Path(path2).is_dir(): + patterns.append(path2) + found_tests = True + + if not found_tests: logger.warning(f"No tests found for module: {mod}") return patterns @@ -83,9 +97,7 @@ def run_tests(modules, changed_files, coverage=True, run_all=False): logger.info("Running all tests.") return sys.exit(subprocess.call(base_cmd)) # noqa: S603 - module_patterns = get_modules_tests_patterns( - modules, coverage=coverage, run_all=run_all, - ) + module_patterns = get_modules_tests_patterns(modules) if not module_patterns: logger.warning("No tests found for the changed modules.") else: From 6cc6bff45bf27b6fda9e7fce09b6d1f7d839a707 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:49:08 +0100 Subject: [PATCH 07/18] Fix --- .github/workflows/test.yml | 4 ++-- tests_script.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e0647f5357..88ee49b61e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -84,11 +84,11 @@ jobs: # Run unit tests, run them all when push to main branch - name: Run all unit tests - run: python test_script.py --cov --all + run: python tests_script.py --cov --all if: github.event_name == 'push' - name: Run unit tests for changed files - run: python test_script.py --cov + run: python tests_script.py --cov if: github.event_name == 'pull_request' - name: Upload coverage reports to Codecov diff --git a/tests_script.py b/tests_script.py index f8f9e637a4..a008d5e52d 100644 --- a/tests_script.py +++ b/tests_script.py @@ -56,7 +56,7 @@ def get_modules_tests_patterns(modules): found_tests = False # Check if direct test files exist - if list(Path(".").glob(path1)): + if list(Path().glob(path1)): patterns.append(path1) found_tests = True From 4fc1cf063fa71faa5860b77e492a62a68a9e022c Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 13:51:12 +0100 Subject: [PATCH 08/18] Missing namespace declaration --- tests/modules/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/modules/__init__.py diff --git a/tests/modules/__init__.py b/tests/modules/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From bb534d66d4da372d1707f104557dc830a8080d6b Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 14:54:00 +0100 Subject: [PATCH 09/18] Move myeclpay tests to core tests --- tests/{modules => core}/test_myeclpay.py | 0 tests/{modules => core}/test_payment.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename tests/{modules => core}/test_myeclpay.py (100%) rename tests/{modules => core}/test_payment.py (99%) diff --git a/tests/modules/test_myeclpay.py b/tests/core/test_myeclpay.py similarity index 100% rename from tests/modules/test_myeclpay.py rename to tests/core/test_myeclpay.py diff --git a/tests/modules/test_payment.py b/tests/core/test_payment.py similarity index 99% rename from tests/modules/test_payment.py rename to tests/core/test_payment.py index 88cd84264a..b670855c0a 100644 --- a/tests/modules/test_payment.py +++ b/tests/core/test_payment.py @@ -296,7 +296,7 @@ async def test_webhook_payment_callback( ) -> None: # We patch the callback to be able to check if it was called mocked_callback = mocker.patch( - "tests.test_payment.callback", + "tests.core.test_payment.callback", ) # We patch the module_list to inject our custom test module @@ -337,7 +337,7 @@ async def test_webhook_payment_callback_fail( ) -> None: # We patch the callback to be able to check if it was called mocked_callback = mocker.patch( - "tests.test_payment.callback", + "tests.core.test_payment.callback", side_effect=ValueError("Test error"), ) From 022685a74ea361bb8524049d14acfe792bafb948 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:02:18 +0100 Subject: [PATCH 10/18] Detect tests modification as module scope --- tests_script.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests_script.py b/tests_script.py index a008d5e52d..b4ede06052 100644 --- a/tests_script.py +++ b/tests_script.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) # We ignore .md files and GitHub workflows and app/modules to detect the scope of the changes -IGNORE_PATHS_START = ("app/modules/", ".github/") +IGNORE_PATHS_START = ("app/modules/", "tests/modules/", ".github/") IGNORE_EXTENSIONS = (".md",) @@ -30,9 +30,18 @@ def detect_modules(changed_files): modules = set() for f in changed_files: logger.info(f"Changed file: {f}") + # We want to detect changes in app/modules//... if f.startswith("app/modules/"): module = f.split("/")[2] modules.add(module) + # Or the modification of tests in tests/modules//... or tests/modules/test_*.py + elif f.startswith("tests/modules/"): + parts = f.split("/") + if parts[2].startswith("test_"): + module = parts[2][5:].split(".")[0].split("_")[0] + else: + module = parts[2] + modules.add(module) return sorted(modules) From 8bc5edbd943458b8dc487016bc7a32af9d410cfa Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:14:45 +0100 Subject: [PATCH 11/18] Some improvements / fixes --- tests_script.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/tests_script.py b/tests_script.py index b4ede06052..fcb02a3f1a 100644 --- a/tests_script.py +++ b/tests_script.py @@ -26,7 +26,7 @@ def get_changed_files(): def detect_modules(changed_files): - """DDetect impacted modules based on file paths.""" + """Detect impacted modules based on file paths.""" modules = set() for f in changed_files: logger.info(f"Changed file: {f}") @@ -104,6 +104,7 @@ def run_tests(modules, changed_files, coverage=True, run_all=False): if run_all: logger.info("Running all tests.") + base_cmd += ["tests/"] return sys.exit(subprocess.call(base_cmd)) # noqa: S603 module_patterns = get_modules_tests_patterns(modules) @@ -113,30 +114,32 @@ def run_tests(modules, changed_files, coverage=True, run_all=False): logger.info(f"Impacted modules tests: {', '.join(module_patterns)}") base_cmd += module_patterns - get_other_tests = get_other_tests_patterns(changed_files) - if get_other_tests: - logger.info(f"Additional tests to run: {', '.join(get_other_tests)}") - base_cmd += get_other_tests + other_tests = get_other_tests_patterns(changed_files) + if other_tests: + logger.info(f"Additional tests to run: {', '.join(other_tests)}") + base_cmd += other_tests logger.info(f"Running tests with command: {' '.join(base_cmd)}") sys.exit(subprocess.call(base_cmd)) # noqa: S603 if __name__ == "__main__": - changed_files = get_changed_files() - modules = detect_modules(changed_files) - scope_only = is_module_scope_only(changed_files) - - # Detect arg --cov + # Detect arg --cov and --all coverage = "--cov" in sys.argv run_all = "--all" in sys.argv + changed_files = get_changed_files() + # First detect if the --all flag is set or if there are no changed files outside module scope + scope_only = not run_all and is_module_scope_only(changed_files) + # First we check if changes are module-scoped only # If so, we run tests only for those modules - if scope_only and not run_all: + if scope_only: logger.info("Changes are module-scoped only.") + modules = detect_modules(changed_files) run_tests(modules, changed_files, coverage=coverage) # Else else: logger.info("Changes affect broader scope, running all tests.") + modules = [] run_tests(modules, changed_files, coverage=coverage, run_all=True) From cb7f9eb515b4d26af23b7a1b1ce9c8d17804a9e9 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:23:43 +0100 Subject: [PATCH 12/18] Regex --- tests_script.py | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/tests_script.py b/tests_script.py index fcb02a3f1a..34f6a21098 100644 --- a/tests_script.py +++ b/tests_script.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 import logging +import re import subprocess import sys from pathlib import Path @@ -28,20 +29,25 @@ def get_changed_files(): def detect_modules(changed_files): """Detect impacted modules based on file paths.""" modules = set() + + # Regex patterns for module detection + app_module_pattern = re.compile(r"^app/modules/([^/]+)/") + test_module_pattern = re.compile(r"^tests/modules/(?:test_)?([^/_.]+)") + for f in changed_files: logger.info(f"Changed file: {f}") - # We want to detect changes in app/modules//... - if f.startswith("app/modules/"): - module = f.split("/")[2] - modules.add(module) - # Or the modification of tests in tests/modules//... or tests/modules/test_*.py - elif f.startswith("tests/modules/"): - parts = f.split("/") - if parts[2].startswith("test_"): - module = parts[2][5:].split(".")[0].split("_")[0] - else: - module = parts[2] - modules.add(module) + + # Check for app/modules//... + app_match = app_module_pattern.match(f) + if app_match: + modules.add(app_match.group(1)) + continue + + # Check for tests/modules//... or tests/modules/test_*.py + test_match = test_module_pattern.match(f) + if test_match: + modules.add(test_match.group(1)) + return sorted(modules) From b9266556445e93915b206fa55922ad649473dc69 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:30:01 +0100 Subject: [PATCH 13/18] Add a comment --- tests_script.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests_script.py b/tests_script.py index 34f6a21098..af2563c175 100644 --- a/tests_script.py +++ b/tests_script.py @@ -17,6 +17,7 @@ def get_changed_files(): """Enumerate files changed compared to main branch.""" try: + # We use git diff to get the list of changed files with Three dots (...) to compare with the base commit of the PR in the main branch diff = subprocess.check_output( # noqa: S603 ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 text=True, From cf081b06eea9bd380cb53ce8534ca9e629702cf0 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:31:10 +0100 Subject: [PATCH 14/18] Use any instead of list --- tests_script.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests_script.py b/tests_script.py index af2563c175..514895d8b5 100644 --- a/tests_script.py +++ b/tests_script.py @@ -72,7 +72,7 @@ def get_modules_tests_patterns(modules): found_tests = False # Check if direct test files exist - if list(Path().glob(path1)): + if any(Path().glob(path1)): patterns.append(path1) found_tests = True From d63413e1df95375105f1d966ef9ea719f53bf7ff Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Wed, 19 Nov 2025 00:35:04 +0100 Subject: [PATCH 15/18] Rename test_PH.py to test_ph.py Lowercase --- tests/modules/{test_PH.py => test_ph.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/modules/{test_PH.py => test_ph.py} (100%) diff --git a/tests/modules/test_PH.py b/tests/modules/test_ph.py similarity index 100% rename from tests/modules/test_PH.py rename to tests/modules/test_ph.py From 9a79bc469b5692fe7621a144d1e5b8bdb3db94a2 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:11:03 +0100 Subject: [PATCH 16/18] Fixes --- tests_script.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/tests_script.py b/tests_script.py index 514895d8b5..7bb909a943 100644 --- a/tests_script.py +++ b/tests_script.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python3 import logging import re import subprocess @@ -10,21 +9,18 @@ logger = logging.getLogger(__name__) # We ignore .md files and GitHub workflows and app/modules to detect the scope of the changes -IGNORE_PATHS_START = ("app/modules/", "tests/modules/", ".github/") +IGNORE_PATHS_START = ("app/modules/", "tests/modules/", ".github/", ".vscode/") IGNORE_EXTENSIONS = (".md",) def get_changed_files(): """Enumerate files changed compared to main branch.""" - try: - # We use git diff to get the list of changed files with Three dots (...) to compare with the base commit of the PR in the main branch - diff = subprocess.check_output( # noqa: S603 - ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 - text=True, - ).strip() - return diff.splitlines() - except subprocess.CalledProcessError: - return [] + # We use git diff to get the list of changed files with three dots (...) to compare with the base commit of the PR in the main branch + diff = subprocess.check_output( # noqa: S603 + ["git", "diff", "--name-only", "origin/main..."], # noqa: S607 + text=True, + ).strip() + return diff.splitlines() def detect_modules(changed_files): From e04d731e4a911de84fd8d769f8721f0c8b5ba3e7 Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:13:07 +0100 Subject: [PATCH 17/18] Removed edited on PR --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 88ee49b61e..c4ab2b602d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,7 +3,7 @@ name: Test on: pull_request: - types: [opened, edited, ready_for_review, synchronize] + types: [opened, ready_for_review, synchronize] push: branches: - main From 94aa24768f156be71a1d9d89525fe75ea0f98bfc Mon Sep 17 00:00:00 2001 From: Warix <39554785+warix8@users.noreply.github.com> Date: Thu, 20 Nov 2025 12:19:42 +0100 Subject: [PATCH 18/18] Use sub dir for cdr --- tests/modules/cdr/__init__.py | 0 tests/modules/{ => cdr}/test_cdr.py | 0 tests/modules/{ => cdr}/test_cdr_result.py | 0 tests_script.py | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 tests/modules/cdr/__init__.py rename tests/modules/{ => cdr}/test_cdr.py (100%) rename tests/modules/{ => cdr}/test_cdr_result.py (100%) diff --git a/tests/modules/cdr/__init__.py b/tests/modules/cdr/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/modules/test_cdr.py b/tests/modules/cdr/test_cdr.py similarity index 100% rename from tests/modules/test_cdr.py rename to tests/modules/cdr/test_cdr.py diff --git a/tests/modules/test_cdr_result.py b/tests/modules/cdr/test_cdr_result.py similarity index 100% rename from tests/modules/test_cdr_result.py rename to tests/modules/cdr/test_cdr_result.py diff --git a/tests_script.py b/tests_script.py index 7bb909a943..3a75e3b51b 100644 --- a/tests_script.py +++ b/tests_script.py @@ -29,7 +29,7 @@ def detect_modules(changed_files): # Regex patterns for module detection app_module_pattern = re.compile(r"^app/modules/([^/]+)/") - test_module_pattern = re.compile(r"^tests/modules/(?:test_)?([^/_.]+)") + test_module_pattern = re.compile(r"^tests/modules/(?:test_)?([^/.]+)") for f in changed_files: logger.info(f"Changed file: {f}")