class AddressesAPI(API): """API methods for addresses.""" API: API = API() URL: str = API.build_api_url(path=f'/ondemand/{AGENCY}/addresses') def create_address(self, address_data: dict, url: str) -> dict: """Add an address using the API, then surface the address ID for teardown. This method does not utilize the base self.URL attribute as it requires a url to be passed from the creation of the address dictionary. This is due to the separation between Web and Admin address creation. :param address_data: A dictionary of address data for API consumption. :param url: An application URL yielded from the Address Factory. """ if 'url' in address_data: del address_data['url'] return self.API.create_request(data=address_data, expected_response=[200], url=url) def delete_address(self, address: dict, rider_address: bool) -> None: """Add an address using the API, then surface the address ID for teardown. :param address: The address intended for deletion. :param rider_address: Boolean for whether the address is web or not. """ address_id: int = address['address_id'] if rider_address: self.URL = self.API.build_api_url('/me/rider/addresses') delete_url: str = f'{self.URL}/{address_id}' self.API.delete_request(expected_response=[200], url=delete_url)
class RidesAPI(API): """API methods for rides.""" API: API = API() URL = API.build_api_url(path=f'/ondemand/{AGENCY}/rides') def cancel_ride(self, ride: dict) -> None: """Cancel a ride using the API. :param ride: A ride yielded by the API. """ self.change_ride_status(ride, 'Canceled') def change_ride_status(self, ride: dict, status: str) -> None: """Manipulate a ride's status using the API. :param ride: A ride yielded from the API. :param status: A specified status. """ groomed_status: str = status.lower().replace(' ', '_') ride_id: int = ride['ride_id'] status_data: dict = {'status': f'{groomed_status}'} url: str = f'{self.URL}/{ride_id}' self.API.change_ride_status_request(data=status_data, expected_response=[200], url=url) def complete_ride(self, ride: dict) -> None: """Complete a ride using the API. :param ride: A ride yielded by the API. """ self.change_ride_status(ride=ride, status='In Progress') self.change_ride_status(ride=ride, status='Complete') def create_ride(self, ride_data: dict) -> dict: """Add a ride using the API, then surface the ride ID for teardown. :param ride_data: A dictionary of ride data for API consumption. """ return self.API.create_request(data=ride_data, expected_response=[200], url=self.URL) def set_no_show(self, ride: dict) -> None: """Set a ride to 'No Show' status. :param ride: A ride yielded by the API. """ self.change_ride_status(ride=ride, status='No Show') def start_ride(self, ride: dict) -> None: """Start a ride using the API. :param ride: A ride yielded by the API. """ self.change_ride_status(ride=ride, status='In Progress')
class GroupsAPI(API): """API methods for groups.""" API: API = API() URL: str = API.build_api_url(path=f'/ondemand/{AGENCY}/groups') def create_group(self, group_data: dict) -> dict: """Add a group using the API, then surface the group ID for teardown. :param group_data: A dictionary of group data for API consumption. """ return self.API.create_request(data=group_data, expected_response=[200], url=self.URL) def delete_group(self, group: dict) -> None: """Delete a group which has been successfully created. :param group: A group generated by a factory. """ group_id: str = group['group_id'] delete_url: str = f'{self.URL}/{group_id}' self.API.delete_request(expected_response=[200], url=delete_url) def add_user_to_group(self, group: dict, user: dict) -> dict: """Associate a user with a group. Note- both group and user must already exist for association to take place. :param group: A group generated by a factory. :param user: A user generated by a factory. """ group_id: int = group['group_id'] username: str = user['username'] url: str = f'{self.URL}/{group_id}/users/{username}' return self.API.create_request(data=dict(), expected_response=[200], url=url) def remove_user_from_group(self, group: dict, user: dict) -> None: """Disassociate a user with a group. :param group: A group generated by a factory. :param user: A user generated by a factory. """ group_id: int = group['group_id'] username: str = user['username'] url: str = f'{self.URL}/{group_id}/users/{username}' self.API.delete_request(expected_response=[200], url=url)
def test_create_address__failure__invalid_input(self) -> None: """Check that the create_address method fails with invalid input.""" with pytest.raises(TypeError): self.addresses_api.create_address( address_data=1111111, url=API.build_api_url('/testing'), # type: ignore )
class Params: """Optional params which change factory output when True. :param rider_address: Generate an address for the OnDemand Web application. :example usage: AddressFactory.create(rider_address=True) """ rider_address = Trait(url=API.build_api_url('/me/rider/addresses'))
class AddressFactory(Factory): """Create a new address for OnDemand testing. This is a factory which can be configured by passing in optional parameters for address customization via the nested class method. Multiple addresses may be created using the factory by calling a batch method. Optional parameters must match fields defined in the Address data class. :example usage: API: AddressFactory.create(name='Lake Park Plaza') API Web Address: AddressFactory.create(name='Lake Park Plaza', rider_address=True) Non-API: AddressFactory.build(name='Lake Park Plaza') """ class Meta: model = Address class Params: """Optional params which change factory output when True. :param rider_address: Generate an address for the OnDemand Web application. :example usage: AddressFactory.create(rider_address=True) """ rider_address = Trait(url=API.build_api_url('/me/rider/addresses')) _api: AddressesAPI = AddressesAPI() url: str = API.build_api_url(f'/ondemand/{AGENCY}/addresses') name: str = LazyAttribute( lambda _: f'{fake.sentence(nb_words=3)}-{process_time()}') @classmethod def _build(cls, model_class: Callable, **kwargs: Any) -> dict: """Override the default _build method to build a new Address. :param model_class: The factory model used for generation. :param kwargs: Additional arguments being passed for generation. """ return model_class(**kwargs).__dict__ @classmethod def _create(cls, model_class: Callable, **kwargs: Any) -> dict: """Override the default _create method to post against the Addresses API. :param app: The application under test. :param model_class: The factory model used for generation. :param kwargs: Additional arguments being passed for generation. """ _address_dict: dict = model_class(**kwargs).__dict__ return cls._api.create_address(address_data=_address_dict, url=_address_dict['url'])
def test_create_address__remove_url_key(self) -> None: """Check that the create_address method removes the 'url' key.""" address_data: dict = AddressFactory.build() url: str = API.build_api_url(path=f'/ondemand/{AGENCY}/addresses') address: dict = self.addresses_api.create_address( address_data=address_data, url=url) assert 'url' not in address self.addresses_api.delete_address(address=address, rider_address=False)
def test_create_address__success(self) -> None: """Check that an Address may be created.""" address_data: dict = AddressFactory.build() url: str = API.build_api_url(path=f'/ondemand/{AGENCY}/addresses') try: address: dict = self.addresses_api.create_address( address_data=address_data, url=url) self.addresses_api.delete_address(address=address, rider_address=False) except HTTPError: pytest.fail('Test failed due to HTTPError.', pytrace=True)
class ServicesAPI(API): """API methods for services.""" API: API = API() URL = API.build_api_url(path=f'/ondemand/{AGENCY}/services') def archive_service(self, service: dict) -> None: """Archive an expired service which has been successfully created. This endpoint calls True when both a 200 or 503 have been received. The reasoning for this is to ensure that services have been archived correctly (503 response) and if not, to archive them (200 response). :param service: The service intended for archive. """ service_id: str = service['service_id'] archive_url: str = f'{self.URL}/{service_id}' self.API.delete_request(expected_response=[200, 503], url=archive_url) def create_service(self, service_data: dict) -> dict: """Create a service using the API, then surface the service ID for teardown. :param service_data: A dictionary of service data for API consumption. """ return self.API.create_request(data=service_data, expected_response=[200], url=self.URL) def delete_service(self, service: dict) -> None: """Delete a service which has been successfully created. :param service: The service intended for deletion. """ service_id: int = service['service_id'] destroy_url: str = f'{self.URL}/{service_id}/destroy' self.API.delete_request(expected_response=[200], url=destroy_url)
class VehiclesAPI(API): """API methods for vehicles.""" API: API = API() URL = API.build_api_url(path=f'/ondemand/{AGENCY}/vehicles') def create_vehicle(self, vehicle_data: dict) -> dict: """Add a vehicle using the API, then surface the vehicle ID for teardown. :param vehicle_data: A dictionary of vehicle data for API consumption. """ return self.API.create_request(data=vehicle_data, expected_response=[200], url=self.URL) def delete_vehicle(self, vehicle: dict) -> None: """Delete a vehicle which has been successfully created. :param vehicle: The unique vehicle generated when a vehicle is created. """ vehicle_id: int = vehicle['vehicle_id'] delete_url: str = f'{self.URL}/{vehicle_id}' self.API.delete_request(expected_response=[200], url=delete_url)
def change_ride_status(self, ride: dict, status: str) -> None: """Manipulate a recurring ride's status using the API. :param ride: A recurring ride yielded by the API. :param status: A specified status. """ groomed_status: str = status.lower().replace(' ', '_') ride_id: int = ride['ride']['ride_id'] status_data: dict = {'status': f'{groomed_status}'} url: str = API.build_api_url( path=f'/ondemand/{AGENCY}/rides/{ride_id}') self.API.change_ride_status_request(data=status_data, expected_response=[200], url=url)
class RegionsAPI(API): """API methods for regions.""" API: API = API() URL = API.build_api_url(path=f'/ondemand/{AGENCY}/regions') def create_region(self, region_data: dict) -> dict: """Add a region using the API, then surface the region ID for teardown. :param region_data: A dictionary of region data for API consumption. """ return self.API.create_request(data=region_data, expected_response=[200], url=self.URL) def delete_region(self, region: dict) -> None: """Delete a region which has been successfully created. :param region: The unique region ID generated when a region is created. """ region_id: int = region['region_id'] delete_url: str = f'{self.URL}/{region_id}' self.API.delete_request(expected_response=[200], url=delete_url)
def valid_address(self) -> Address: """Create a valid Address.""" url: str = API.build_api_url(f'/ondemand/test_agency/addresses') address: Address = Address(name='Test Address', url=url) yield address
def login(request: fixture, selenium: fixture) -> None: """Login via submitting a login form or auth token. Runs a check to determine if an auth token exists. If one exists, the framework will add the auth token as a cookie. If not, the framework will input valid login credentials, then capture the auth token for later tests. :param request: Fixture for requesting Pytest configuration data. :param selenium: An instance of Selenium Web Driver. """ login_api = API() login_page = Login(selenium) if 'no_auth' not in request.keywords: # # Check for specific pytest marks for permissions, then login accordingly # if 'role_agent' in request.keywords: login_api.capture_token(username=USERS.Agent.USERNAME, superuser=False) elif 'role_driver' in request.keywords: login_api.capture_token(username=USERS.Driver.USERNAME, superuser=False) elif 'role_dispatcher' in request.keywords: login_api.capture_token(username=USERS.Dispatcher.USERNAME, superuser=False) elif 'role_rider' in request.keywords: login_api.capture_token(username=USERS.Rider.USERNAME, superuser=False) else: login_api.capture_token(username=USERS.Admin.USERNAME, superuser=False) login_page.visit() login_page.add_auth_token()
class RecurringRidesAPI(API): """API methods for recurring rides.""" API: API = API() URL: str = API.build_api_url( path=f'/ondemand/{AGENCY}/rides/subscriptions') def cancel_booked_ride(self, ride: dict) -> None: """Cancel a booked portion of a recurring ride using the API. :param ride: A recurring ride yielded by the API. """ self.change_ride_status(ride, 'Canceled') def cancel_recurring_ride(self, recurring_ride: dict) -> None: """Cancel a recurring ride using the API. Use this method for cancelling an entire subscription rather than a single booked ride. :param recurring_ride: A recurring ride yielded by the API. """ tries: int = 10 url: str = f'{self.URL}/{recurring_ride["ride_subscription_id"]}/cancel' while True: tries -= 1 sleep(random()) response: Response = requests.put( url=url, headers=self.API.build_auth_headers(), ) if response.status_code == 200: break elif tries == 0: raise HTTPError( f'Requests failed with response: {response.json()}') def change_ride_status(self, ride: dict, status: str) -> None: """Manipulate a recurring ride's status using the API. :param ride: A recurring ride yielded by the API. :param status: A specified status. """ groomed_status: str = status.lower().replace(' ', '_') ride_id: int = ride['ride']['ride_id'] status_data: dict = {'status': f'{groomed_status}'} url: str = API.build_api_url( path=f'/ondemand/{AGENCY}/rides/{ride_id}') self.API.change_ride_status_request(data=status_data, expected_response=[200], url=url) def complete_ride(self, ride: dict) -> None: """Complete a ride using the API. :param ride: A recurring ride yielded by the API. """ self.change_ride_status(ride=ride, status='In Progress') self.change_ride_status(ride=ride, status='Complete') def create_recurring_ride(self, ride_data: dict) -> dict: """Add a recurring ride using the API, then surface the ride ID for teardown. :param ride_data: A dictionary of recurring ride data for API consumption. """ return self.API.create_request(data=ride_data, expected_response=[200], url=self.URL) def set_no_show(self, ride: dict) -> None: """Set a ride to 'No Show' status. :param ride: A recurring ride yielded by the API. """ self.change_ride_status(ride=ride, status='No Show') def start_ride(self, ride: dict) -> None: """Start a ride using the API. :param ride: A recurring ride yielded by the API. """ self.change_ride_status(ride=ride, status='In Progress')
def set_api(self) -> None: """Instantiate all APIs for API helper testing.""" self.api: API = API()