From 1ab11b1cbe178655ae604c76e94cf4ef16ebeb45 Mon Sep 17 00:00:00 2001 From: Tudor Amariei Date: Tue, 17 Sep 2024 10:54:59 +0300 Subject: [PATCH] Add more endpoints and checks - add new organization and user endpoints - get a user's profile - get an NGO user's organization's profile - get the applications of an organization's user - get the list of apps - get organization information - get user(s) information - checks for users making use of these endpoints - check if a user has an application (self and other users) - check if an organization has an application --- pyproject.toml | 5 +- src/ngohub/core.py | 187 +++++++++- src/ngohub/network.py | 19 +- tests/__init__.py | 0 tests/conftest.py | 58 ++- tests/schemas.py | 82 ----- tests/test_end_to_end/__init__.py | 0 tests/test_end_to_end/schemas.py | 338 ++++++++++++++++++ .../test_nomenclatures.py | 2 +- tests/test_end_to_end/test_organization.py | 130 +++++++ tests/{ => test_end_to_end}/test_status.py | 2 +- tests/test_end_to_end/test_user.py | 21 ++ 12 files changed, 746 insertions(+), 98 deletions(-) create mode 100644 tests/__init__.py delete mode 100644 tests/schemas.py create mode 100644 tests/test_end_to_end/__init__.py create mode 100644 tests/test_end_to_end/schemas.py rename tests/{ => test_end_to_end}/test_nomenclatures.py (98%) create mode 100644 tests/test_end_to_end/test_organization.py rename tests/{ => test_end_to_end}/test_status.py (88%) create mode 100644 tests/test_end_to_end/test_user.py diff --git a/pyproject.toml b/pyproject.toml index 548c5ce..a927f5f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,6 @@ addopts = "-ra -q" testpaths = [ "tests", ] -env_files = [ - ".env", - ".env.test", +markers = [ + "investigation_needed: mark test as needing investigation (deselect with '-m \"not investigation_needed\"')", ] diff --git a/src/ngohub/core.py b/src/ngohub/core.py index f0c7931..1e42032 100644 --- a/src/ngohub/core.py +++ b/src/ngohub/core.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod -from typing import Any, Dict, List +from copy import deepcopy +from typing import Any, Dict, List, Union +from ngohub.exceptions import HubHTTPException, MissingUserException from ngohub.network import HTTPClient, HTTPClientResponse @@ -11,12 +13,13 @@ class BaseHub(ABC): @abstractmethod def __init__(self, api_base_url: str) -> None: - pass + self.api_base_url: str = api_base_url or "" class NGOHub(BaseHub): def __init__(self, api_base_url: str) -> None: - self.api_base_url: str = api_base_url or "" + super().__init__(api_base_url) + self.client: HTTPClient = HTTPClient(self.api_base_url) def is_healthy(self) -> bool: @@ -96,3 +99,181 @@ def get_beneficiaries_nomenclatures(self): def get_issuers_nomenclatures(self): return self._get_nomenclature("issuers") + + # User related methods + def get_profile(self, user_token: str) -> Dict[str, Any]: + response: HTTPClientResponse = self.client.api_get("/profile/", token=user_token) + + return response.to_dict() + + # Organization related methods + def get_organization_profile(self, ngo_token: str) -> Dict[str, Any]: + response: HTTPClientResponse = self.client.api_get("/organization-profile/", token=ngo_token) + + return response.to_dict() + + def get_user_organization_applications(self, ngo_token: str) -> List[Dict[str, Any]]: + response: HTTPClientResponse = self.client.api_get("/organization/applications/", token=ngo_token) + + return list(response.to_dict()) + + def check_user_organization_has_application(self, ngo_token: str, login_link: str) -> Dict[str, Any]: + organization_applications: List[Dict[str, Any]] = self.get_user_organization_applications(ngo_token) + + for app in organization_applications: + if app["loginLink"].startswith(login_link) and app["status"] == "active" and app["ongStatus"] == "active": + return app + + return {} + + # Admin related methods + def get_application_list(self, admin_token: str) -> List[Dict[str, Any]]: + response: HTTPClientResponse = self.client.api_get("/application/list/", token=admin_token) + + return list(response.to_dict()) + + def get_organization(self, admin_token: str, organization_id: int) -> Dict[str, Any]: + response: HTTPClientResponse = self.client.api_get(f"/organization/{organization_id}/", token=admin_token) + + return response.to_dict() + + def get_organization_applications(self, admin_token: str, organization_id: int) -> List[Dict[str, Any]]: + response: HTTPClientResponse = self.client.api_get( + f"/application/organization/{organization_id}/", token=admin_token + ) + + return list(response.to_dict()) + + def get_user(self, admin_token: str, user_id: int) -> Dict[str, Any]: + try: + response: HTTPClientResponse = self.client.api_get(f"/user/{user_id}/", token=admin_token) + except HubHTTPException as e: + if e.status_code == 404: + raise MissingUserException(f"User with ID {user_id} not found") + + raise e + + return response.to_dict() + + def get_users( + self, + admin_token: str, + organization_id: int, + limit: int = 1000, + page: int = 1, + search: str = None, + order_by: str = None, + order_direction: str = None, + start: str = None, + end: str = None, + status: str = None, + available_apps_ids: List[int] = None, + ) -> Dict[str, Any]: + request_url: str = f"/user?organization_id={organization_id}&limit={limit}&page={page}" + if search: + request_url += f"&search={search}" + if order_by: + request_url += f"&orderBy={order_by}" + if order_direction and order_direction.upper() in ["ASC", "DESC"]: + request_url += f"&orderDirection={order_direction.upper()}" + if start: + request_url += f"&start={start}" + if end: + request_url += f"&end={end}" + if status and status.lower() in ["active", "pending", "restricted"]: + request_url += f"&status={status}" + if available_apps_ids: + for app_id in available_apps_ids: + request_url += f"&availableAppsIds={app_id}" + + response: HTTPClientResponse = self.client.api_get(request_url, token=admin_token) + + return response.to_dict() + + def check_organization_has_application( + self, admin_token: str, organization_id: int, login_link: str + ) -> Dict[str, Any]: + organization_applications: List[Dict[str, Any]] = self.get_organization_applications( + admin_token, organization_id + ) + + for application in organization_applications: + if ( + application["loginLink"].startswith(login_link) + and application["status"] == "active" + and application["ongStatus"] == "active" + ): + return application + + return {} + + def _check_user_has_application(self, admin_token, organization_id, user_id, response) -> Dict[str, Any]: + continue_searching: bool = True + page: int = 1 + + response: Dict[str, Any] = deepcopy(response) + searched_application_id = response["application"]["id"] + + while continue_searching: + organization_users: Dict[str, Union[Dict, List]] = self.get_users( + admin_token=admin_token, organization_id=organization_id, page=page + ) + + response_metadata = organization_users.get("meta", {}) + response_users = organization_users.get("items", []) + + if response_metadata["totalPages"] <= response_metadata["currentPage"]: + continue_searching = False + else: + page += 1 + + for user in response_users: + if user["id"] == int(user_id): + response["user"] = user + user_applications: List[Dict[str, Any]] = user["availableAppsIDs"] + + if searched_application_id in user_applications: + response["access"] = True + return response + + response["access"] = False + return response + + return response + + def check_organization_user_has_application( + self, + admin_token: str, + organization_id: int, + user_id: int, + login_link: str, + ) -> Dict[str, Any]: + + response: Dict[str, Any] = {"user": None, "application": None, "access": None} + + organization_application: Dict = self.check_organization_has_application( + admin_token, organization_id, login_link + ) + if not organization_application: + return response + + response["application"] = organization_application + + try: + user_information: Dict = self.get_user(admin_token, user_id) + except MissingUserException: + return response + + if not user_information or user_information.get("organizationId") != int(organization_id): + return response + + response["user"] = user_information + + if user_information.get("role") == "admin": + response["access"] = True + + return response + + response = self._check_user_has_application(admin_token, organization_id, user_id, response) + + return response diff --git a/src/ngohub/network.py b/src/ngohub/network.py index b6b583d..c2ac47a 100644 --- a/src/ngohub/network.py +++ b/src/ngohub/network.py @@ -6,7 +6,7 @@ from http.client import HTTPResponse, HTTPSConnection from typing import Dict, Optional -from ngohub.exceptions import HubDecodeException, HubHTTPException +from ngohub.exceptions import HubBadRequestException, HubDecodeException, HubHTTPException logger = logging.getLogger(__name__) @@ -80,16 +80,27 @@ def _api_request( try: conn.request(method=request_method, url=path, body=encoded_params, headers=headers) except socket.gaierror as e: - raise HubHTTPException(f"Failed to make request to '{path}': {e}") + raise HubBadRequestException( + message=f"Failed to make request to '{path}': {e}", + path=path, + ) try: response: HTTPResponse = conn.getresponse() except ConnectionError as e: - raise HubHTTPException(f"Failed to get response from '{path}': {e}") + raise HubBadRequestException( + message=f"Failed to get response from '{path}': {e}", + path=path, + ) if response.status != http.HTTPStatus.OK: logger.info(path) - raise HubHTTPException(f"{response.status} while retrieving '{path}'. Reason: {response.reason}") + raise HubHTTPException( + message=f"{response.status} while retrieving '{path}'. Reason: {response.reason}", + status_code=response.status, + path=path, + reason=response.reason, + ) return HTTPClientResponse(response) diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py index 960868d..acb797a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,16 +2,66 @@ import pytest from dotenv import find_dotenv, load_dotenv +from pycognito import Cognito def pytest_configure(): - load_dotenv(find_dotenv(filename=".env.test")) + if not os.environ.get("SKIP_TEST_ENV_FILE"): + load_dotenv(find_dotenv(filename=".env.test")) - pytest.ngohub_api_url = os.environ.get("NGO_HUB_API_URL") + pytest.ngohub_api_url = os.environ["NGO_HUB_API_URL"] + pytest.app_login_link = os.environ["APP_LOGIN_LINK"] -def ngohub_url(): - return os.environ.get("NGO_HUB_API_URL") + pytest.ngo_id = os.environ["NGO_ID"] + pytest.ngo_admin_id = os.environ["NGO_ADMIN_ID"] + pytest.ngo_user_id = os.environ["NGO_USER_ID"] + pytest.ngo_user_id_no_app = os.environ["NGO_USER_ID_NO_APP"] + + pytest.ngo_other_id = os.environ["NGO_OTHER_ID"] + pytest.ngo_other_admin_id = os.environ["NGO_OTHER_ADMIN_ID"] + pytest.ngo_other_user_id = os.environ["NGO_OTHER_USER_ID"] + + pytest.admin_username = os.environ["NGOHUB_API_ACCOUNT"] + pytest.ngo_admin_username = os.environ["NGOHUB_NGO_ACCOUNT"] + + pytest.admin_token = _authenticate_with_ngohub( + user_pool_id=os.environ["AWS_COGNITO_USER_POOL_ID"], + client_id=os.environ["AWS_COGNITO_CLIENT_ID"], + client_secret=os.environ["AWS_COGNITO_CLIENT_SECRET"], + user_pool_region=os.environ["AWS_COGNITO_REGION"], + username=os.environ["NGOHUB_API_ACCOUNT"], + api_key=os.environ["NGOHUB_API_KEY"], + ) + + pytest.ngo_admin_token = _authenticate_with_ngohub( + user_pool_id=os.environ["AWS_COGNITO_USER_POOL_ID"], + client_id=os.environ["AWS_COGNITO_CLIENT_ID"], + client_secret=os.environ["AWS_COGNITO_CLIENT_SECRET"], + user_pool_region=os.environ["AWS_COGNITO_REGION"], + username=os.environ["NGOHUB_NGO_ACCOUNT"], + api_key=os.environ["NGOHUB_NGO_KEY"], + ) + + +def _authenticate_with_ngohub( + user_pool_id: str, + client_id: str, + client_secret: str, + user_pool_region: str, + username: str, + api_key: str, +) -> str: + u = Cognito( + user_pool_id=user_pool_id, + client_id=client_id, + client_secret=client_secret, + username=username, + user_pool_region=user_pool_region, + ) + u.authenticate(password=api_key) + + return u.id_token class FakeHTTPSConnection: diff --git a/tests/schemas.py b/tests/schemas.py deleted file mode 100644 index f0a0143..0000000 --- a/tests/schemas.py +++ /dev/null @@ -1,82 +0,0 @@ -from schema import Regex, Schema - -VERSION_REVISION_SCHEMA = Schema( - { - "version": Regex(r"\d+\.\d+\.\d+"), - "revision": Regex(r"[0-9a-f]{40}"), - } -) - -NOMENCLATURE_CITIES_SCHEMA = Schema( - [ - { - "id": int, - "name": str, - "countyId": int, - "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - "county": { - "id": int, - "name": str, - "abbreviation": str, - "regionId": int, - "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - }, - } - ] -) - -NOMENCLATURE_COUNTIES_SCHEMA = Schema( - [ - { - "id": int, - "name": str, - "abbreviation": str, - "regionId": int, - "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - } - ] -) - -NOMENCLATURE_ISSUERS_SCHEMA = NOMENCLATURE_SKILLS_SCHEMA = NOMENCLATURE_FACULTIES_SCHEMA = ( - NOMENCLATURE_REGIONS_SCHEMA -) = NOMENCLATURE_DOMAINS_SCHEMA = Schema( - [ - { - "id": int, - "name": str, - "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - } - ] -) - -NOMENCLATURE_FEDERATIONS_SCHEMA = NOMENCLATURE_COALITIONS_SCHEMA = Schema( - [ - { - "id": int, - "name": str, - "abbreviation": str, - "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), - } - ] -) - -NOMENCLATURE_PRACTICE_DOMAINS_SCHEMA = NOMENCLATURE_SERVICE_DOMAINS_SCHEMA = Schema( - [ - { - "id": int, - "name": str, - "group": str, - } - ] -) - -NOMENCLATURE_BENEFIARIES_SCHEMA = Schema( - [ - { - "id": int, - "name": str, - } - ] -) diff --git a/tests/test_end_to_end/__init__.py b/tests/test_end_to_end/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_end_to_end/schemas.py b/tests/test_end_to_end/schemas.py new file mode 100644 index 0000000..c8ef1ec --- /dev/null +++ b/tests/test_end_to_end/schemas.py @@ -0,0 +1,338 @@ +from schema import Or, Regex, Schema + +VERSION_REVISION_SCHEMA = Schema( + { + "version": Regex(r"\d+\.\d+\.\d+"), + "revision": Regex(r"[0-9a-f]{40}"), + } +) + +NOMENCLATURE_CITIES_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + "countyId": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "county": { + "id": int, + "name": str, + "abbreviation": str, + "regionId": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + }, + } + ] +) + +NOMENCLATURE_COUNTIES_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + "abbreviation": str, + "regionId": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + } + ] +) + +NOMENCLATURE_ISSUERS_SCHEMA = NOMENCLATURE_SKILLS_SCHEMA = NOMENCLATURE_FACULTIES_SCHEMA = ( + NOMENCLATURE_REGIONS_SCHEMA +) = NOMENCLATURE_DOMAINS_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + } + ] +) + +NOMENCLATURE_FEDERATIONS_SCHEMA = NOMENCLATURE_COALITIONS_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + "abbreviation": str, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + } + ] +) + +NOMENCLATURE_PRACTICE_DOMAINS_SCHEMA = NOMENCLATURE_SERVICE_DOMAINS_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + "group": str, + } + ] +) + +NOMENCLATURE_BENEFIARIES_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + } + ] +) + +ORGANIZATION_SCHEMA = ORGANIZATION_PROFILE_SCHEMA = Schema( + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "status": str, + "organizationGeneral": { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "name": str, + "alias": str, + "type": str, + "email": str, + "phone": str, + "yearCreated": int, + "cui": str, + "associationRegistryNumber": None, + "associationRegistryPart": None, + "associationRegistrySection": None, + "associationRegistryIssuerId": None, + "nationalRegistryNumber": None, + "rafNumber": str, + "shortDescription": str, + "description": str, + "address": str, + "logo": str, + "website": str, + "facebook": str, + "instagram": str, + "twitter": str, + "linkedin": str, + "tiktok": str, + "donationWebsite": str, + "redirectLink": str, + "donationSMS": str, + "donationKeyword": str, + "contact": { + "email": str, + "phone": str, + "fullName": str, + }, + "organizationAddress": None, + "city": { + "id": int, + "name": str, + "countyId": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + }, + "county": { + "id": int, + "name": str, + "abbreviation": str, + "regionId": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + }, + "organizationCity": None, + "organizationCounty": None, + "associationRegistryIssuer": None, + }, + "organizationActivity": { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "area": str, + "isPartOfFederation": bool, + "isPartOfCoalition": bool, + "isPartOfInternationalOrganization": bool, + "internationalOrganizationName": None, + "isSocialServiceViable": bool, + "offersGrants": bool, + "isPublicIntrestOrganization": bool, + "hasBranches": bool, + "federations": [], + "coalitions": [], + "domains": [ + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "name": str, + } + ], + "cities": [], + "branches": [], + "regions": [], + }, + "organizationLegal": { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "others": None, + "organizationStatute": str, + "nonPoliticalAffiliationFile": str, + "balanceSheetFile": str, + "legalReprezentative": { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "fullName": str, + "email": str, + "phone": str, + "role": str, + }, + "directors": [ + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "fullName": str, + "email": str, + "phone": Or(None, str), # noqa + "role": Or(None, str), # noqa + } + ], + }, + "organizationFinancial": [ + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "type": str, + "numberOfEmployees": Or(None, int), # noqa2 + "year": int, + "total": Or(None, int), # noqa + "status": str, + "reportStatus": str, + "synched_anaf": bool, + "data": Or(None, str), # noqa + } + ], + "organizationReport": { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "reports": [ + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "report": None, + "numberOfVolunteers": int, + "numberOfContractors": int, + "year": int, + "status": str, + } + ], + "partners": [ + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "year": int, + "numberOfPartners": int, + "status": str, + "path": None, + } + ], + "investors": [ + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "year": int, + "numberOfInvestors": int, + "status": str, + "path": None, + } + ], + }, + } +) + +APPLICATION_LIST_SCHEMA = Schema( + [ + { + "id": int, + "name": str, + } + ] +) + +ORGANIZATION_APPLICATIONS_SCHEMA = Schema( + [ + { + "id": int, + "logo": Or(None, str), # noqa + "name": str, + "shortDescription": str, + "loginLink": str, + "website": str, + "status": str, + "ongStatus": Or(None, str), # noqa + "createdOn": Or(None, Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z")), # noqa + "type": str, + "applicationLabel": Or(None, str), # noqa + } + ] +) + +ORGANIZATION_USERS_SCHEMA = Schema( + { + "items": [ + { + "id": int, + "name": str, + "email": str, + "phone": str, + "status": Or("active", "restricted", "pending"), + "availableApps": [ + { + "id": int, + "name": str, + "type": Or("independent", "simple", "standalone"), + }, + ], + "availableAppsIDs": [int], + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "organizationId": int, + "organizationAlias": str, + }, + ], + "meta": { + "itemCount": int, + "totalItems": int, + "itemsPerPage": int, + "totalPages": int, + "currentPage": int, + }, + } +) + + +PROFILE_SCHEMA = Schema( + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "cognitoId": str, + "name": str, + "email": str, + "phone": str, + "role": str, + "status": str, + "organization": Or( + None, + { + "id": int, + "createdOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "updatedOn": Regex(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z"), + "status": str, + }, + ), + } +) diff --git a/tests/test_nomenclatures.py b/tests/test_end_to_end/test_nomenclatures.py similarity index 98% rename from tests/test_nomenclatures.py rename to tests/test_end_to_end/test_nomenclatures.py index 0e45bba..a6ee203 100644 --- a/tests/test_nomenclatures.py +++ b/tests/test_end_to_end/test_nomenclatures.py @@ -1,7 +1,7 @@ import pytest from ngohub.core import NGOHub -from tests.schemas import ( +from tests.test_end_to_end.schemas import ( NOMENCLATURE_BENEFIARIES_SCHEMA, NOMENCLATURE_CITIES_SCHEMA, NOMENCLATURE_COALITIONS_SCHEMA, diff --git a/tests/test_end_to_end/test_organization.py b/tests/test_end_to_end/test_organization.py new file mode 100644 index 0000000..5eeb9e3 --- /dev/null +++ b/tests/test_end_to_end/test_organization.py @@ -0,0 +1,130 @@ +import pytest + +from ngohub import NGOHub +from ngohub.exceptions import HubHTTPException +from tests.test_end_to_end.schemas import ( + APPLICATION_LIST_SCHEMA, + ORGANIZATION_APPLICATIONS_SCHEMA, + ORGANIZATION_PROFILE_SCHEMA, + ORGANIZATION_SCHEMA, +) + + +@pytest.mark.investigation_needed +def test_organization_profile(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.get_organization_profile(ngo_token=pytest.ngo_admin_token) + + assert ORGANIZATION_PROFILE_SCHEMA.validate(response) + + +def test_organization_profile_returns_401(): + hub = NGOHub(pytest.ngohub_api_url) + + with pytest.raises(HubHTTPException): + hub.get_organization_profile(ngo_token="invalid_token") + + +def test_organization(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.get_organization(admin_token=pytest.admin_token, organization_id=10) + + assert ORGANIZATION_SCHEMA.validate(response) + + +def test_application_list(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.get_application_list(admin_token=pytest.admin_token) + + assert APPLICATION_LIST_SCHEMA.validate(response) + + +def test_organization_applications(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.get_organization_applications(admin_token=pytest.admin_token, organization_id=pytest.ngo_id) + + assert ORGANIZATION_APPLICATIONS_SCHEMA.validate(response) + + +def test_check_missing_organization_returns_empty(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.check_organization_user_has_application( + admin_token=pytest.admin_token, + organization_id=0, + user_id=0, + login_link=pytest.app_login_link, + ) + + assert response["user"] is None + assert response["application"] is None + assert response["access"] is None + + +def test_check_missing_user_returns_empty(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.check_organization_user_has_application( + admin_token=pytest.admin_token, + organization_id=pytest.ngo_id, + user_id=0, + login_link=pytest.app_login_link, + ) + + assert response["user"] is None + assert response["application"] is not None + assert response["access"] is None + + +def test_check_organization_wrong_admin_does_not_have_application(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.check_organization_user_has_application( + admin_token=pytest.admin_token, + organization_id=pytest.ngo_id, + user_id=pytest.ngo_other_admin_id, + login_link=pytest.app_login_link, + ) + + assert response["user"] is None + assert response["application"] is not None + assert response["access"] is None + + +def test_check_organization_admin_has_application(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.check_organization_user_has_application( + admin_token=pytest.admin_token, + organization_id=pytest.ngo_id, + user_id=pytest.ngo_admin_id, + login_link=pytest.app_login_link, + ) + + assert response["user"] is not None + assert response["application"] is not None + assert response["access"] is True + + +def test_check_organization_user_without_app_doesnt_have_application(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.check_organization_user_has_application( + admin_token=pytest.admin_token, + organization_id=pytest.ngo_id, + user_id=pytest.ngo_user_id_no_app, + login_link=pytest.app_login_link, + ) + + assert response["user"] is not None + assert response["application"] is not None + assert response["access"] is False + + +def test_check_organization_user_with_app_has_application(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.check_organization_user_has_application( + admin_token=pytest.admin_token, + organization_id=pytest.ngo_id, + user_id=pytest.ngo_user_id, + login_link=pytest.app_login_link, + ) + + assert response["user"] is not None + assert response["application"] is not None + assert response["access"] is True diff --git a/tests/test_status.py b/tests/test_end_to_end/test_status.py similarity index 88% rename from tests/test_status.py rename to tests/test_end_to_end/test_status.py index e3f6977..72982b6 100644 --- a/tests/test_status.py +++ b/tests/test_end_to_end/test_status.py @@ -1,7 +1,7 @@ import pytest from ngohub.core import NGOHub -from tests.schemas import VERSION_REVISION_SCHEMA +from tests.test_end_to_end.schemas import VERSION_REVISION_SCHEMA def test_health_returns_ok(): diff --git a/tests/test_end_to_end/test_user.py b/tests/test_end_to_end/test_user.py new file mode 100644 index 0000000..e2a111e --- /dev/null +++ b/tests/test_end_to_end/test_user.py @@ -0,0 +1,21 @@ +import pytest + +from ngohub import NGOHub +from tests.test_end_to_end.schemas import PROFILE_SCHEMA + + +@pytest.mark.investigation_needed +def test_user_returns_profile(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.get_profile(user_token=pytest.ngo_admin_token) + + assert PROFILE_SCHEMA.validate(response) + assert response["email"] == pytest.ngo_admin_username + + +def test_admin_returns_profile(): + hub = NGOHub(pytest.ngohub_api_url) + response = hub.get_profile(user_token=pytest.admin_token) + + assert PROFILE_SCHEMA.validate(response) + assert response["email"] == pytest.admin_username