From 9f6cf21e23d0affbb0b5d0bcc23e28282e35bcc9 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Tue, 25 Jul 2023 16:16:55 +0000 Subject: [PATCH 01/10] Add initial setup for registration workflows --- .../workflows/registration.py | 83 +++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 course_attendance_service/course_attendance_service/workflows/registration.py diff --git a/course_attendance_service/course_attendance_service/workflows/registration.py b/course_attendance_service/course_attendance_service/workflows/registration.py new file mode 100644 index 00000000..1be0f98d --- /dev/null +++ b/course_attendance_service/course_attendance_service/workflows/registration.py @@ -0,0 +1,83 @@ +""" +Make a RegistrationWorkflows class that contains methods calling an abstract RegistrantRepo interface, +which has a ZoomRegistrantRepo implementation. +Have automated tests in place for each workflow method and each ZoomRegistrantRepo method, +that doesn't call the Zoom API (i.e. goal is to have fast tests) +""" + + +from abc import ABC, abstractmethod +from typing import Any, Dict, List, NamedTuple, Union + +###### Tests + + +def test_total_number_of_registrants(): + # GIVEN: a workshop + workshops = { + "workshop1": [Registrant(), Registrant(), Registrant()], + "workshop2": [Registrant(), Registrant()], + } + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for the number registrants + registrants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registrants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + # THEN: correct number of registrants is returned + assert registrants_report1.total_registrants == 3 + assert registrants_report2.total_registrants == 2 + + +###### Entities + + +class Registrant(NamedTuple): + pass + # name: str + # affiliation: Union[str, List[str]] + # status: str + + +###### Workflows + + +class RegistrantsRepo(ABC): + @abstractmethod + def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: + return 1 + + +class RegistrantsReport(NamedTuple): + registrants: List[Registrant] + + @property + def total_registrants(self): + return len(self.registrants) + + +class RegistrationWorkflows: + def __init__(self, registrants_repo: RegistrantsRepo): + self.registrants_repo = registrants_repo + + def get_registrants_report(self, workshop_id): + registrants = self.registrants_repo.get_list_of_registrants( + workshop_id=workshop_id + ) + return RegistrantsReport(registrants) + + +###### Repositories + + +class InMemoryRegistrantsRepo(RegistrantsRepo): + def __init__(self, workshops: Dict[Any, List[Registrant]]): + self.workshops = workshops + + def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: + return self.workshops[workshop_id] From ec6088b5ef6af60a9ae9b4eb0991a8957f598037 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Tue, 25 Jul 2023 16:20:08 +0000 Subject: [PATCH 02/10] Remove redundancies and add more info to registrants --- .../workflows/registration.py | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration.py b/course_attendance_service/course_attendance_service/workflows/registration.py index 1be0f98d..fc798dfe 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration.py +++ b/course_attendance_service/course_attendance_service/workflows/registration.py @@ -1,11 +1,3 @@ -""" -Make a RegistrationWorkflows class that contains methods calling an abstract RegistrantRepo interface, -which has a ZoomRegistrantRepo implementation. -Have automated tests in place for each workflow method and each ZoomRegistrantRepo method, -that doesn't call the Zoom API (i.e. goal is to have fast tests) -""" - - from abc import ABC, abstractmethod from typing import Any, Dict, List, NamedTuple, Union @@ -15,8 +7,15 @@ def test_total_number_of_registrants(): # GIVEN: a workshop workshops = { - "workshop1": [Registrant(), Registrant(), Registrant()], - "workshop2": [Registrant(), Registrant()], + "workshop1": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="Denied"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="Pending"), + ], + "workshop2": [ + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="Pending"), + ], } registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) @@ -38,10 +37,9 @@ def test_total_number_of_registrants(): class Registrant(NamedTuple): - pass - # name: str - # affiliation: Union[str, List[str]] - # status: str + name: str + affiliation: Union[str, List[str]] + status: str ###### Workflows From 07cbc1159f317cbae901275a0472460172f00399 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Tue, 25 Jul 2023 16:37:16 +0000 Subject: [PATCH 03/10] Add more methods and tests --- .../workflows/registration.py | 118 +++++++++++++++++- 1 file changed, 114 insertions(+), 4 deletions(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration.py b/course_attendance_service/course_attendance_service/workflows/registration.py index fc798dfe..6ca38a2b 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration.py +++ b/course_attendance_service/course_attendance_service/workflows/registration.py @@ -8,9 +8,9 @@ def test_total_number_of_registrants(): # GIVEN: a workshop workshops = { "workshop1": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="Denied"), + Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="Pending"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), ], "workshop2": [ Registrant(name="Sang", affiliation="UKB", status="approved"), @@ -33,6 +33,96 @@ def test_total_number_of_registrants(): assert registrants_report2.total_registrants == 2 +def test_number_of_approved_registrants_is_correct(): + # GIVEN: a workshop + workshops = { + "workshop1": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + ], + "workshop2": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="approved"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + ], + } + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for the number of approved particpants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + # THEN: the correct number of participants is returned + assert registratants_report1.number_of_approved_registrants == 1 + assert registratants_report2.number_of_approved_registrants == 2 + + +def test_number_of_denied_registrants_is_correct(): + # GIVEN: a workshop + workshops = { + "workshop1": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + ], + "workshop2": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="approved"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + ], + } + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for the number of denied particpants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + # THEN: the correct number of participants is returned + assert registratants_report1.number_of_denied_registrants == 1 + assert registratants_report2.number_of_denied_registrants == 0 + + +def test_number_of_pending_registrants_is_correct(): + # GIVEN: a workshop + workshops = { + "workshop1": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + ], + "workshop2": [ + Registrant(name="Mo", affiliation="Uni Tuebingen", status="approved"), + Registrant(name="Sang", affiliation="UKB", status="approved"), + Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + ], + } + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for the number of pending particpants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + # THEN: the correct number of participants is returned + assert registratants_report1.number_of_pending_registrants == 1 + assert registratants_report2.number_of_pending_registrants == 1 + + ###### Entities @@ -48,16 +138,36 @@ class Registrant(NamedTuple): class RegistrantsRepo(ABC): @abstractmethod def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: - return 1 + pass class RegistrantsReport(NamedTuple): registrants: List[Registrant] @property - def total_registrants(self): + def total_registrants(self) -> int: return len(self.registrants) + def get_number_of_registrants_for_a_specific_status(self, status: str) -> int: + return sum( + [ + 1 if registrant.status.lower() == status.lower() else 0 + for registrant in self.registrants + ] + ) + + @property + def number_of_approved_registrants(self) -> int: + return self.get_number_of_registrants_for_a_specific_status(status="approved") + + @property + def number_of_denied_registrants(self) -> int: + return self.get_number_of_registrants_for_a_specific_status(status="denied") + + @property + def number_of_pending_registrants(self) -> int: + return self.get_number_of_registrants_for_a_specific_status(status="pending") + class RegistrationWorkflows: def __init__(self, registrants_repo: RegistrantsRepo): From efa0048f745b8ac5822781d1a0a76f13400f3cf4 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Thu, 27 Jul 2023 14:38:50 +0000 Subject: [PATCH 04/10] Reorganize and add Zoom repo and api --- .../workflows/registration/__init__.py | 0 .../workflows/registration/api_zoom.py | 62 ++++++++++++++ .../workflows/registration/repo_inmemory.py | 10 +++ .../workflows/registration/repo_zoom.py | 64 +++++++++++++++ .../test_workflows.py} | 80 ++----------------- .../workflows/registration/workflows.py | 60 ++++++++++++++ 6 files changed, 201 insertions(+), 75 deletions(-) create mode 100644 course_attendance_service/course_attendance_service/workflows/registration/__init__.py create mode 100644 course_attendance_service/course_attendance_service/workflows/registration/api_zoom.py create mode 100644 course_attendance_service/course_attendance_service/workflows/registration/repo_inmemory.py create mode 100644 course_attendance_service/course_attendance_service/workflows/registration/repo_zoom.py rename course_attendance_service/course_attendance_service/workflows/{registration.py => registration/test_workflows.py} (69%) create mode 100644 course_attendance_service/course_attendance_service/workflows/registration/workflows.py diff --git a/course_attendance_service/course_attendance_service/workflows/registration/__init__.py b/course_attendance_service/course_attendance_service/workflows/registration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/course_attendance_service/course_attendance_service/workflows/registration/api_zoom.py b/course_attendance_service/course_attendance_service/workflows/registration/api_zoom.py new file mode 100644 index 00000000..7e06f321 --- /dev/null +++ b/course_attendance_service/course_attendance_service/workflows/registration/api_zoom.py @@ -0,0 +1,62 @@ +from typing import Any, Dict, List, Literal, TypedDict, Union +import requests + + +class ZoomRestApi: + def __init__(self, api_server: str = None): + self.api_server = ( + api_server if api_server is not None else "https://api.zoom.us/v2" + ) + + @staticmethod + def get_json_from_zoom_request(url, access_token: str, params: Dict = None): + headers = {"Authorization": f"Bearer {access_token}"} + response = requests.get(url=url, headers=headers, params=params, timeout=10) + response.raise_for_status() + return response.json() + + def get_registrants( + self, + access_token: str, + workshop_id: Union[str, int], + status="approved", + page_size=100, + ): + url = self.api_server + f"/meetings/{workshop_id}/registrants" + params = {"status": status, "page_size": page_size} + response_json = self.get_json_from_zoom_request( + url=url, access_token=access_token, params=params + ) + data: ZoomGetRegistrantsResponseData = response_json + return data["registrants"] + + +class ZoomRegistrantsData(TypedDict): + id: str + first_name: str + last_name: str + email: str + address: str + city: str + country: str + zip: Any + state: str + phone: Union[str, int] + industry: str + org: Any + job_title: str + purchasing_time_frame: Any + role_in_purchase_process: str + no_of_employee: int + comments: str + custom_questions: List[Dict] + status: Literal["approved", "denied", "pending"] + create_time: str + join_url: str + + +class ZoomGetRegistrantsResponseData(TypedDict): + page_size: int + total_records: int + next_page_token: str + registrants: List[ZoomRegistrantsData] diff --git a/course_attendance_service/course_attendance_service/workflows/registration/repo_inmemory.py b/course_attendance_service/course_attendance_service/workflows/registration/repo_inmemory.py new file mode 100644 index 00000000..64028c20 --- /dev/null +++ b/course_attendance_service/course_attendance_service/workflows/registration/repo_inmemory.py @@ -0,0 +1,10 @@ +from typing import Dict, Any, List +from .workflows import Registrant, RegistrantsRepo + + +class InMemoryRegistrantsRepo(RegistrantsRepo): + def __init__(self, workshops: Dict[Any, List[Registrant]]): + self.workshops = workshops + + def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: + return self.workshops[workshop_id] diff --git a/course_attendance_service/course_attendance_service/workflows/registration/repo_zoom.py b/course_attendance_service/course_attendance_service/workflows/registration/repo_zoom.py new file mode 100644 index 00000000..50151cb0 --- /dev/null +++ b/course_attendance_service/course_attendance_service/workflows/registration/repo_zoom.py @@ -0,0 +1,64 @@ +# %% +from typing import List, Union + +from .api_zoom import ZoomRestApi, ZoomRegistrantsData +from .workflows import Registrant, RegistrantsRepo + + +class ZoomRegistrantsRepo(RegistrantsRepo): + def __init__(self, zoom_api: ZoomRestApi) -> None: + self.zoom_api = zoom_api + self.access_token = "TOP-SECRET" + + @staticmethod + def turn_zoom_registrants_into_standard_form( + zoom_registrant: ZoomRegistrantsData, + ) -> Registrant: + user_id = zoom_registrant["id"] + name = f"{zoom_registrant['first_name']} {zoom_registrant['last_name']}" + affiliation = zoom_registrant["custom_questions"][0]["value"] + email = zoom_registrant["email"] + status = zoom_registrant["status"] + registrant = Registrant( + user_id=user_id, + name=name, + affiliation=affiliation, + email=email, + status=status, + ) + return registrant + + def get_list_of_registrants_for_specific_status( + self, + workshop_id: Union[str, int], + status: str, + ) -> List[Registrant]: + zoom_registrants = self.zoom_api.get_registrants( + access_token=self.access_token, + workshop_id=workshop_id, + status=status, + ) + registrants = [] + for zoom_registrant in zoom_registrants: + registrant = self.turn_zoom_registrants_into_standard_form(zoom_registrant) + registrants.append(registrant) + return registrants + + def get_list_of_registrants(self, workshop_id: Union[str, int]) -> List[Registrant]: + all_registrants = [] + for status in ["approved", "denied", "pending"]: + registrants = self.get_list_of_registrants_for_specific_status( + workshop_id=workshop_id, status=status + ) + if len(registrants) > 0: + all_registrants.extend(registrants) + return all_registrants + + def remove_registrant(self): + raise NotImplementedError + + def add_registrant(self): + raise NotImplementedError + + def update_registrant_status(self): + raise NotImplementedError diff --git a/course_attendance_service/course_attendance_service/workflows/registration.py b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py similarity index 69% rename from course_attendance_service/course_attendance_service/workflows/registration.py rename to course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py index 6ca38a2b..1d229779 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py @@ -1,7 +1,5 @@ -from abc import ABC, abstractmethod -from typing import Any, Dict, List, NamedTuple, Union - -###### Tests +from .repo_inmemory import InMemoryRegistrantsRepo +from .workflows import RegistrationWorkflows, Registrant def test_total_number_of_registrants(): @@ -33,7 +31,7 @@ def test_total_number_of_registrants(): assert registrants_report2.total_registrants == 2 -def test_number_of_approved_registrants_is_correct(): +def test_number_of_approved_registrants(): # GIVEN: a workshop workshops = { "workshop1": [ @@ -63,7 +61,7 @@ def test_number_of_approved_registrants_is_correct(): assert registratants_report2.number_of_approved_registrants == 2 -def test_number_of_denied_registrants_is_correct(): +def test_number_of_denied_registrants(): # GIVEN: a workshop workshops = { "workshop1": [ @@ -93,7 +91,7 @@ def test_number_of_denied_registrants_is_correct(): assert registratants_report2.number_of_denied_registrants == 0 -def test_number_of_pending_registrants_is_correct(): +def test_number_of_pending_registrants(): # GIVEN: a workshop workshops = { "workshop1": [ @@ -121,71 +119,3 @@ def test_number_of_pending_registrants_is_correct(): # THEN: the correct number of participants is returned assert registratants_report1.number_of_pending_registrants == 1 assert registratants_report2.number_of_pending_registrants == 1 - - -###### Entities - - -class Registrant(NamedTuple): - name: str - affiliation: Union[str, List[str]] - status: str - - -###### Workflows - - -class RegistrantsRepo(ABC): - @abstractmethod - def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: - pass - - -class RegistrantsReport(NamedTuple): - registrants: List[Registrant] - - @property - def total_registrants(self) -> int: - return len(self.registrants) - - def get_number_of_registrants_for_a_specific_status(self, status: str) -> int: - return sum( - [ - 1 if registrant.status.lower() == status.lower() else 0 - for registrant in self.registrants - ] - ) - - @property - def number_of_approved_registrants(self) -> int: - return self.get_number_of_registrants_for_a_specific_status(status="approved") - - @property - def number_of_denied_registrants(self) -> int: - return self.get_number_of_registrants_for_a_specific_status(status="denied") - - @property - def number_of_pending_registrants(self) -> int: - return self.get_number_of_registrants_for_a_specific_status(status="pending") - - -class RegistrationWorkflows: - def __init__(self, registrants_repo: RegistrantsRepo): - self.registrants_repo = registrants_repo - - def get_registrants_report(self, workshop_id): - registrants = self.registrants_repo.get_list_of_registrants( - workshop_id=workshop_id - ) - return RegistrantsReport(registrants) - - -###### Repositories - - -class InMemoryRegistrantsRepo(RegistrantsRepo): - def __init__(self, workshops: Dict[Any, List[Registrant]]): - self.workshops = workshops - - def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: - return self.workshops[workshop_id] diff --git a/course_attendance_service/course_attendance_service/workflows/registration/workflows.py b/course_attendance_service/course_attendance_service/workflows/registration/workflows.py new file mode 100644 index 00000000..8c538578 --- /dev/null +++ b/course_attendance_service/course_attendance_service/workflows/registration/workflows.py @@ -0,0 +1,60 @@ +from abc import ABC, abstractmethod +from typing import Any, Dict, List, NamedTuple, Union + +###### Entities + + +class Registrant(NamedTuple): + user_id: str + name: str + affiliation: Union[str, List[str]] + email: str + status: str + + +###### Workflows + + +class RegistrantsRepo(ABC): + @abstractmethod + def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: + pass + + +class RegistrantsReport(NamedTuple): + registrants: List[Registrant] + + @property + def total_registrants(self) -> int: + return len(self.registrants) + + def get_number_of_registrants_for_a_specific_status(self, status: str) -> int: + return sum( + [ + 1 if registrant.status.lower() == status.lower() else 0 + for registrant in self.registrants + ] + ) + + @property + def number_of_approved_registrants(self) -> int: + return self.get_number_of_registrants_for_a_specific_status(status="approved") + + @property + def number_of_denied_registrants(self) -> int: + return self.get_number_of_registrants_for_a_specific_status(status="denied") + + @property + def number_of_pending_registrants(self) -> int: + return self.get_number_of_registrants_for_a_specific_status(status="pending") + + +class RegistrationWorkflows: + def __init__(self, registrants_repo: RegistrantsRepo): + self.registrants_repo = registrants_repo + + def get_registrants_report(self, workshop_id): + registrants = self.registrants_repo.get_list_of_registrants( + workshop_id=workshop_id + ) + return RegistrantsReport(registrants) From 0d9a49f6dca70428deee4ed2d12eba8c28ddd0f7 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Thu, 27 Jul 2023 14:40:28 +0000 Subject: [PATCH 05/10] Add tests for the Zoom repo --- .../workflows/registration/test_repo_zoom.py | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py diff --git a/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py b/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py new file mode 100644 index 00000000..2b931f21 --- /dev/null +++ b/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py @@ -0,0 +1,71 @@ +from unittest.mock import Mock + +from .api_zoom import ZoomRestApi, ZoomRegistrantsData +from .repo_zoom import ZoomRegistrantsRepo +from .workflows import Registrant + + +def get_registrants(access_token=None, workshop_id=None, status=None): + workshops = { + "abc": { + "page_size": 30, + "total_records": 1, + "next_page_token": "", + "registrants": { + "approved": [ + ZoomRegistrantsData( + id="some_random_chars", + first_name="Mo", + last_name="Bashiri", + custom_questions=[{"value": "iBehave"}], + email="mo@email.com", + status="approved", + ) + ], + "denied": [], + "pending": [], + }, + }, + } + return workshops[workshop_id]["registrants"][status] + + +def test_repo_can_get_list_of_registrants_from_zoom_api(): + # GIVEN: the zoom api and a workhop + zoom_api = Mock(ZoomRestApi) + zoom_api.get_registrants = get_registrants + workshop_id = "abc" + + repo = ZoomRegistrantsRepo(zoom_api=zoom_api) + + # WHEN: asked for the list of registrants + observed_registrants = repo.get_list_of_registrants(workshop_id=workshop_id) + expected_registrants = [ + Registrant( + user_id="some_random_chars", + name="Mo Bashiri", + affiliation="iBehave", + email="mo@email.com", + status="approved", + ) + ] + + # THEN: the correct list of registrants is returned by the repo + assert observed_registrants == expected_registrants + + +def test_repo_can_get_correct_number_of_registrants_from_zoom_api(): + # GIVEN: the zoom api and a workhop + zoom_api = Mock(ZoomRestApi) + zoom_api.get_registrants = get_registrants + workshop_id = "abc" + + repo = ZoomRegistrantsRepo(zoom_api=zoom_api) + + # WHEN: asked for the registrants + registrants = repo.get_list_of_registrants(workshop_id=workshop_id) + observed_number = len(registrants) + expected_number = 1 + + # THEN: the number of registrants is correct + assert observed_number == expected_number From 85de8f2ea3a3a5c72e9afbd925d3ea9645e0c958 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Thu, 27 Jul 2023 15:19:43 +0000 Subject: [PATCH 06/10] Decouple workflow test from entities --- .../workflows/registration/test_workflows.py | 76 +++++++------------ 1 file changed, 26 insertions(+), 50 deletions(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py index 1d229779..d16aff4f 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py @@ -1,20 +1,32 @@ +from typing import NamedTuple +import pytest from .repo_inmemory import InMemoryRegistrantsRepo -from .workflows import RegistrationWorkflows, Registrant +from .workflows import RegistrationWorkflows -def test_total_number_of_registrants(): - # GIVEN: a workshop - workshops = { +class MockRegistrant(NamedTuple): + status: str + + +@pytest.fixture +def workshops(): + return { "workshop1": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), + MockRegistrant(status="denied"), + MockRegistrant(status="approved"), + MockRegistrant(status="pending"), ], "workshop2": [ - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="Pending"), + MockRegistrant(status="approved"), + MockRegistrant(status="approved"), + MockRegistrant(status="pending"), + MockRegistrant(status="denied"), ], } + + +def test_total_number_of_registrants(workshops): + # GIVEN: a workshop registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) @@ -28,23 +40,11 @@ def test_total_number_of_registrants(): # THEN: correct number of registrants is returned assert registrants_report1.total_registrants == 3 - assert registrants_report2.total_registrants == 2 + assert registrants_report2.total_registrants == 4 -def test_number_of_approved_registrants(): +def test_number_of_approved_registrants(workshops): # GIVEN: a workshop - workshops = { - "workshop1": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), - ], - "workshop2": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="approved"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), - ], - } registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) @@ -61,20 +61,8 @@ def test_number_of_approved_registrants(): assert registratants_report2.number_of_approved_registrants == 2 -def test_number_of_denied_registrants(): +def test_number_of_denied_registrants(workshops): # GIVEN: a workshop - workshops = { - "workshop1": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), - ], - "workshop2": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="approved"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), - ], - } registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) @@ -88,23 +76,11 @@ def test_number_of_denied_registrants(): # THEN: the correct number of participants is returned assert registratants_report1.number_of_denied_registrants == 1 - assert registratants_report2.number_of_denied_registrants == 0 + assert registratants_report2.number_of_denied_registrants == 1 -def test_number_of_pending_registrants(): +def test_number_of_pending_registrants(workshops): # GIVEN: a workshop - workshops = { - "workshop1": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="denied"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), - ], - "workshop2": [ - Registrant(name="Mo", affiliation="Uni Tuebingen", status="approved"), - Registrant(name="Sang", affiliation="UKB", status="approved"), - Registrant(name="Nick", affiliation="Uni Bonn", status="pending"), - ], - } registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) From 6d4c88364e9115378b980082c96d47dcf8edbf90 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Fri, 28 Jul 2023 06:53:00 +0000 Subject: [PATCH 07/10] Add new methods --- .../workflows/registration/test_workflows.py | 106 +++++++++++++++++- .../workflows/registration/workflows.py | 37 ++++-- 2 files changed, 132 insertions(+), 11 deletions(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py index d16aff4f..2b669e14 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py @@ -39,8 +39,8 @@ def test_total_number_of_registrants(workshops): ) # THEN: correct number of registrants is returned - assert registrants_report1.total_registrants == 3 - assert registrants_report2.total_registrants == 4 + assert registrants_report1.total_number_of_registrants == 3 + assert registrants_report2.total_number_of_registrants == 4 def test_number_of_approved_registrants(workshops): @@ -95,3 +95,105 @@ def test_number_of_pending_registrants(workshops): # THEN: the correct number of participants is returned assert registratants_report1.number_of_pending_registrants == 1 assert registratants_report2.number_of_pending_registrants == 1 + + +def test_registrants_are_correct(workshops): + # GIVEN: a workshop + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for a list of registrants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + expected_outcome1 = [ + MockRegistrant(status="denied"), + MockRegistrant(status="approved"), + MockRegistrant(status="pending"), + ] + expected_outcome2 = [ + MockRegistrant(status="approved"), + MockRegistrant(status="approved"), + MockRegistrant(status="pending"), + MockRegistrant(status="denied"), + ] + # THEN: correct registrants are returned + assert registratants_report1.registrants == expected_outcome1 + assert registratants_report2.registrants == expected_outcome2 + + +def test_approved_registrants_are_correct(workshops): + # GIVEN: a workshop + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for a list of approved registrants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + expected_outcome1 = [ + MockRegistrant(status="approved"), + ] + expected_outcome2 = [ + MockRegistrant(status="approved"), + MockRegistrant(status="approved"), + ] + # THEN: correct registrants are returned + assert registratants_report1.approved_registrants == expected_outcome1 + assert registratants_report2.approved_registrants == expected_outcome2 + + +def test_denied_registrants_are_correct(workshops): + # GIVEN: a workshop + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for a list of registrants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + expected_outcome1 = [ + MockRegistrant(status="denied"), + ] + expected_outcome2 = [ + MockRegistrant(status="denied"), + ] + # THEN: correct registrants are returned + assert registratants_report1.denied_registrants == expected_outcome1 + assert registratants_report2.denied_registrants == expected_outcome2 + + +def test_pending_registrants_are_correct(workshops): + # GIVEN: a workshop + registrants_repo = InMemoryRegistrantsRepo(workshops) + registration_workflows = RegistrationWorkflows(registrants_repo) + + # WHEN: asked for a list of registrants + registratants_report1 = registration_workflows.get_registrants_report( + workshop_id="workshop1" + ) + registratants_report2 = registration_workflows.get_registrants_report( + workshop_id="workshop2" + ) + + expected_outcome1 = [ + MockRegistrant(status="pending"), + ] + expected_outcome2 = [ + MockRegistrant(status="pending"), + ] + # THEN: correct registrants are returned + assert registratants_report1.pending_registrants == expected_outcome1 + assert registratants_report2.pending_registrants == expected_outcome2 diff --git a/course_attendance_service/course_attendance_service/workflows/registration/workflows.py b/course_attendance_service/course_attendance_service/workflows/registration/workflows.py index 8c538578..971ccd37 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration/workflows.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/workflows.py @@ -24,17 +24,20 @@ def get_list_of_registrants(self, workshop_id: Any) -> List[Registrant]: class RegistrantsReport(NamedTuple): registrants: List[Registrant] - @property - def total_registrants(self) -> int: - return len(self.registrants) + def get_registrants_for_a_specific_status(self, status: str) -> int: + status_specific_registrants = [ + registrant + for registrant in self.registrants + if registrant.status.lower() == status.lower() + ] + return status_specific_registrants def get_number_of_registrants_for_a_specific_status(self, status: str) -> int: - return sum( - [ - 1 if registrant.status.lower() == status.lower() else 0 - for registrant in self.registrants - ] - ) + return len(self.get_registrants_for_a_specific_status(status=status)) + + @property + def total_number_of_registrants(self) -> int: + return len(self.registrants) @property def number_of_approved_registrants(self) -> int: @@ -48,6 +51,22 @@ def number_of_denied_registrants(self) -> int: def number_of_pending_registrants(self) -> int: return self.get_number_of_registrants_for_a_specific_status(status="pending") + @property + def all_registrants(self) -> List[Registrant]: + return self.registrants + + @property + def approved_registrants(self) -> List[Registrant]: + return self.get_registrants_for_a_specific_status(status="approved") + + @property + def denied_registrants(self) -> List[Registrant]: + return self.get_registrants_for_a_specific_status(status="denied") + + @property + def pending_registrants(self) -> List[Registrant]: + return self.get_registrants_for_a_specific_status(status="pending") + class RegistrationWorkflows: def __init__(self, registrants_repo: RegistrantsRepo): From fd49e15586b91671b7bd4def0cb229bd1e66d2cb Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Fri, 28 Jul 2023 08:30:58 +0000 Subject: [PATCH 08/10] Change tests to test for behavior instead of identity Co-authored-by: Nicholas A. Del Grosso --- .../workflows/registration/test_workflows.py | 110 ++++++++---------- 1 file changed, 48 insertions(+), 62 deletions(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py index 2b669e14..cacd241d 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/test_workflows.py @@ -1,26 +1,23 @@ from typing import NamedTuple +from unittest.mock import Mock import pytest from .repo_inmemory import InMemoryRegistrantsRepo -from .workflows import RegistrationWorkflows - - -class MockRegistrant(NamedTuple): - status: str +from .workflows import RegistrationWorkflows, Registrant @pytest.fixture def workshops(): return { "workshop1": [ - MockRegistrant(status="denied"), - MockRegistrant(status="approved"), - MockRegistrant(status="pending"), + Mock(Registrant, status="denied"), + Mock(Registrant, status="approved"), + Mock(Registrant, status="pending"), ], "workshop2": [ - MockRegistrant(status="approved"), - MockRegistrant(status="approved"), - MockRegistrant(status="pending"), - MockRegistrant(status="denied"), + Mock(Registrant, status="approved"), + Mock(Registrant, status="approved"), + Mock(Registrant, status="pending"), + Mock(Registrant, status="denied"), ], } @@ -99,31 +96,26 @@ def test_number_of_pending_registrants(workshops): def test_registrants_are_correct(workshops): # GIVEN: a workshop + # WHEN: asked for a list of registrants + # THEN: correct registrants are returned + registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) - - # WHEN: asked for a list of registrants + registratants_report1 = registration_workflows.get_registrants_report( workshop_id="workshop1" ) + expected_outcome1 = ["denied", "approved", "pending"] + observed_outcome1 = [registrant.status for registrant in registratants_report1.registrants] + assert observed_outcome1 == expected_outcome1 + + registratants_report2 = registration_workflows.get_registrants_report( workshop_id="workshop2" ) - - expected_outcome1 = [ - MockRegistrant(status="denied"), - MockRegistrant(status="approved"), - MockRegistrant(status="pending"), - ] - expected_outcome2 = [ - MockRegistrant(status="approved"), - MockRegistrant(status="approved"), - MockRegistrant(status="pending"), - MockRegistrant(status="denied"), - ] - # THEN: correct registrants are returned - assert registratants_report1.registrants == expected_outcome1 - assert registratants_report2.registrants == expected_outcome2 + expected_outcome2 = ["approved", "approved", "pending", "denied"] + observed_outcome2 = [registrant.status for registrant in registratants_report2.registrants] + assert observed_outcome2 == expected_outcome2 def test_approved_registrants_are_correct(workshops): @@ -135,65 +127,59 @@ def test_approved_registrants_are_correct(workshops): registratants_report1 = registration_workflows.get_registrants_report( workshop_id="workshop1" ) + expected_outcome1 = ["approved"] + observed_outcome1 = [registrant.status for registrant in registratants_report1.registrants if registrant.status == "approved"] + assert observed_outcome1 == expected_outcome1 + registratants_report2 = registration_workflows.get_registrants_report( workshop_id="workshop2" ) - - expected_outcome1 = [ - MockRegistrant(status="approved"), - ] - expected_outcome2 = [ - MockRegistrant(status="approved"), - MockRegistrant(status="approved"), - ] - # THEN: correct registrants are returned - assert registratants_report1.approved_registrants == expected_outcome1 - assert registratants_report2.approved_registrants == expected_outcome2 + expected_outcome2 = ["approved", "approved"] + observed_outcome2 = [registrant.status for registrant in registratants_report2.registrants if registrant.status == "approved"] + assert observed_outcome2 == expected_outcome2 def test_denied_registrants_are_correct(workshops): # GIVEN: a workshop + # WHEN: asked for a list of registrants + # THEN: correct registrants are returned + registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) - # WHEN: asked for a list of registrants registratants_report1 = registration_workflows.get_registrants_report( workshop_id="workshop1" ) + expected_outcome1 = ["denied"] + observed_outcome1 = [registrant.status for registrant in registratants_report1.registrants if registrant.status == "denied"] + assert observed_outcome1 == expected_outcome1 + + registratants_report2 = registration_workflows.get_registrants_report( workshop_id="workshop2" ) - - expected_outcome1 = [ - MockRegistrant(status="denied"), - ] - expected_outcome2 = [ - MockRegistrant(status="denied"), - ] - # THEN: correct registrants are returned - assert registratants_report1.denied_registrants == expected_outcome1 - assert registratants_report2.denied_registrants == expected_outcome2 + expected_outcome2 = ["denied"] + observed_outcome2 = [registrant.status for registrant in registratants_report2.registrants if registrant.status == "denied"] + assert observed_outcome2 == expected_outcome2 def test_pending_registrants_are_correct(workshops): # GIVEN: a workshop + # WHEN: asked for a list of registrants + registrants_repo = InMemoryRegistrantsRepo(workshops) registration_workflows = RegistrationWorkflows(registrants_repo) - # WHEN: asked for a list of registrants registratants_report1 = registration_workflows.get_registrants_report( workshop_id="workshop1" ) + expected_outcome1 = ["pending"] + observed_outcome1 = [registrant.status for registrant in registratants_report1.registrants if registrant.status == "pending"] + assert observed_outcome1 == expected_outcome1 + registratants_report2 = registration_workflows.get_registrants_report( workshop_id="workshop2" ) - - expected_outcome1 = [ - MockRegistrant(status="pending"), - ] - expected_outcome2 = [ - MockRegistrant(status="pending"), - ] - # THEN: correct registrants are returned - assert registratants_report1.pending_registrants == expected_outcome1 - assert registratants_report2.pending_registrants == expected_outcome2 + expected_outcome2 = ["pending"] + observed_outcome2 = [registrant.status for registrant in registratants_report2.registrants if registrant.status == "pending"] + assert observed_outcome2 == expected_outcome2 From 0a86867814ca0594ec3264a89c05e8f63e68819d Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Fri, 28 Jul 2023 09:24:18 +0000 Subject: [PATCH 09/10] Remove redundant imports --- .../workflows/registration/workflows.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration/workflows.py b/course_attendance_service/course_attendance_service/workflows/registration/workflows.py index 971ccd37..64d9362f 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration/workflows.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/workflows.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Any, Dict, List, NamedTuple, Union +from typing import Any, List, NamedTuple, Union ###### Entities From 6fedb555a8eadcc114f51aa8cd87daad8002a469 Mon Sep 17 00:00:00 2001 From: Mohammad Bashiri Date: Fri, 28 Jul 2023 09:24:45 +0000 Subject: [PATCH 10/10] Change tests to contain everything they need in the test function --- .../workflows/registration/test_repo_zoom.py | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py b/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py index 2b931f21..77968dc1 100644 --- a/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py +++ b/course_attendance_service/course_attendance_service/workflows/registration/test_repo_zoom.py @@ -5,35 +5,24 @@ from .workflows import Registrant -def get_registrants(access_token=None, workshop_id=None, status=None): - workshops = { - "abc": { - "page_size": 30, - "total_records": 1, - "next_page_token": "", - "registrants": { - "approved": [ - ZoomRegistrantsData( - id="some_random_chars", - first_name="Mo", - last_name="Bashiri", - custom_questions=[{"value": "iBehave"}], - email="mo@email.com", - status="approved", - ) - ], - "denied": [], - "pending": [], - }, - }, - } - return workshops[workshop_id]["registrants"][status] - - def test_repo_can_get_list_of_registrants_from_zoom_api(): # GIVEN: the zoom api and a workhop zoom_api = Mock(ZoomRestApi) - zoom_api.get_registrants = get_registrants + zoom_api.get_registrants = lambda status, **kwargs: { + 'approved': [ + ZoomRegistrantsData( + id="some_random_chars", + first_name="Mo", + last_name="Bashiri", + custom_questions=[{"value": "iBehave"}], + email="mo@email.com", + status="approved", + ), + ], + 'denied': [], + 'pending': [], + }[status] + workshop_id = "abc" repo = ZoomRegistrantsRepo(zoom_api=zoom_api) @@ -57,7 +46,21 @@ def test_repo_can_get_list_of_registrants_from_zoom_api(): def test_repo_can_get_correct_number_of_registrants_from_zoom_api(): # GIVEN: the zoom api and a workhop zoom_api = Mock(ZoomRestApi) - zoom_api.get_registrants = get_registrants + zoom_api.get_registrants = lambda status, **kwargs: { + 'approved': [ + ZoomRegistrantsData( + id="some_random_chars", + first_name="Mo", + last_name="Bashiri", + custom_questions=[{"value": "iBehave"}], + email="mo@email.com", + status="approved", + ), + ], + 'denied': [], + 'pending': [], + }[status] + workshop_id = "abc" repo = ZoomRegistrantsRepo(zoom_api=zoom_api)