def test_check_service_message_limit_in_cache_over_message_limit_fails( notify_db_session, key_type, mocker): with freeze_time("2016-01-01 12:00:00.000000"): mocker.patch('app.redis_store.get', side_effect=[ None, # The serialised service 5, # The rolling count ]) mocker.patch('app.notifications.validators.redis_store.set') mocker.patch('app.notifications.validators.services_dao') service = create_service(restricted=True, message_limit=4) serialised_service = SerialisedService.from_id(service.id) with pytest.raises(TooManyRequestsError) as e: check_service_over_daily_message_limit(key_type, serialised_service) assert e.value.status_code == 429 assert e.value.message == 'Exceeded send limits (4) for today' assert e.value.fields == [] app.notifications.validators.redis_store.set.assert_called_once_with( f'service-{serialised_service.id}', ANY, ex=ANY, ) assert not app.notifications.validators.services_dao.mock_calls
def test_that_when_exceed_rate_limit_request_fails( key_type, sample_service, mocker): with freeze_time("2016-01-01 12:00:00.000000"): if key_type == 'live': api_key_type = 'normal' else: api_key_type = key_type mocker.patch('app.redis_store.exceeded_rate_limit', return_value=True) mocker.patch('app.notifications.validators.services_dao') sample_service.restricted = True api_key = create_api_key(sample_service, key_type=api_key_type) serialised_service = SerialisedService.from_id(sample_service.id) serialised_api_key = SerialisedAPIKeyCollection.from_service_id(serialised_service.id)[0] with pytest.raises(RateLimitError) as e: check_service_over_api_rate_limit(serialised_service, serialised_api_key) assert app.redis_store.exceeded_rate_limit.called_with( "{}-{}".format(str(sample_service.id), api_key.key_type), sample_service.rate_limit, 60 ) assert e.value.status_code == 429 assert e.value.message == 'Exceeded rate limit for key type {} of {} requests per {} seconds'.format( key_type.upper(), sample_service.rate_limit, 60 ) assert e.value.fields == []
def save_letter( self, service_id, notification_id, encrypted_notification, ): notification = encryption.decrypt(encrypted_notification) postal_address = PostalAddress.from_personalisation( Columns(notification['personalisation'])) service = SerialisedService.from_id(service_id) template = SerialisedTemplate.from_id_and_service_id( notification['template'], service_id=service.id, version=notification['template_version'], ) try: # if we don't want to actually send the letter, then start it off in SENDING so we don't pick it up status = NOTIFICATION_CREATED if not service.research_mode else NOTIFICATION_SENDING saved_notification = persist_notification( template_id=notification['template'], template_version=notification['template_version'], postage=postal_address.postage if postal_address.international else template.postage, recipient=postal_address.normalised, service=service, personalisation=notification['personalisation'], notification_type=LETTER_TYPE, api_key_id=None, key_type=KEY_TYPE_NORMAL, created_at=datetime.utcnow(), job_id=notification['job'], job_row_number=notification['row_number'], notification_id=notification_id, reference=create_random_identifier(), reply_to_text=template.reply_to_text, status=status) if not service.research_mode: letters_pdf_tasks.get_pdf_for_templated_letter.apply_async( [str(saved_notification.id)], queue=QueueNames.CREATE_LETTERS_PDF) elif current_app.config['NOTIFY_ENVIRONMENT'] in [ 'preview', 'development' ]: research_mode_tasks.create_fake_letter_response_file.apply_async( (saved_notification.reference, ), queue=QueueNames.RESEARCH_MODE) else: update_notification_status_by_reference( saved_notification.reference, 'delivered') current_app.logger.debug("Letter {} created at {}".format( saved_notification.id, saved_notification.created_at)) except SQLAlchemyError as e: handle_exception(self, notification, notification_id, e)
def test_service_can_send_to_recipient_passes_for_live_service_non_team_member(key_type, sample_service): serialised_service = SerialisedService.from_id(sample_service.id) assert service_can_send_to_recipient("*****@*****.**", key_type, serialised_service) is None assert service_can_send_to_recipient('07513332413', key_type, serialised_service) is None
def test_should_not_access_database_if_redis_disabled(notify_api, sample_service, mocker): serialised_service = SerialisedService.from_id(sample_service.id) with set_config(notify_api, 'REDIS_ENABLED', False): db_mock = mocker.patch('app.notifications.validators.services_dao') check_service_over_daily_message_limit('normal', serialised_service) assert db_mock.method_calls == []
def test_service_can_send_to_recipient_passes(key_type, notify_db_session): trial_mode_service = create_service(service_name='trial mode', restricted=True) serialised_service = SerialisedService.from_id(trial_mode_service.id) assert service_can_send_to_recipient(trial_mode_service.users[0].email_address, key_type, serialised_service) is None assert service_can_send_to_recipient(trial_mode_service.users[0].mobile_number, key_type, serialised_service) is None
def requires_auth(): request_helper.check_proxy_header_before_request() auth_token = get_auth_token(request) issuer = __get_token_issuer(auth_token) # ie the `iss` claim which should be a service ID try: with AUTH_DB_CONNECTION_DURATION_SECONDS.time(): service = SerialisedService.from_id(issuer) except DataError: raise AuthError("Invalid token: service id is not the right data type", 403) except NoResultFound: raise AuthError("Invalid token: service not found", 403) if not service.api_keys: raise AuthError("Invalid token: service has no API keys", 403, service_id=service.id) if not service.active: raise AuthError("Invalid token: service is archived", 403, service_id=service.id) for api_key in service.api_keys: try: decode_jwt_token(auth_token, api_key.secret) except TokenExpiredError: err_msg = "Error: Your system clock must be accurate to within 30 seconds" raise AuthError(err_msg, 403, service_id=service.id, api_key_id=api_key.id) except TokenAlgorithmError: err_msg = "Invalid token: algorithm used is not HS256" raise AuthError(err_msg, 403, service_id=service.id, api_key_id=api_key.id) except TokenDecodeError: # we attempted to validate the token but it failed meaning it was not signed using this api key. # Let's try the next one # TODO: Change this so it doesn't also catch `TokenIssuerError` or `TokenIssuedAtError` exceptions (which # are children of `TokenDecodeError`) as these should cause an auth error immediately rather than # continue on to check the next API key continue except TokenError: # General error when trying to decode and validate the token raise AuthError(GENERAL_TOKEN_ERROR_MESSAGE, 403, service_id=service.id, api_key_id=api_key.id) if api_key.expiry_date: raise AuthError("Invalid token: API key revoked", 403, service_id=service.id, api_key_id=api_key.id) g.service_id = service.id _request_ctx_stack.top.authenticated_service = service _request_ctx_stack.top.api_user = api_key current_app.logger.info('API authorised for service {} with api key {}, using issuer {} for URL: {}'.format( service.id, api_key.id, request.headers.get('User-Agent'), request.base_url )) return else: # service has API keys, but none matching the one the user provided raise AuthError("Invalid token: API key not found", 403, service_id=service.id)
def save_sms(self, service_id, notification_id, encrypted_notification, sender_id=None): notification = encryption.decrypt(encrypted_notification) service = SerialisedService.from_id(service_id) template = SerialisedTemplate.from_id_and_service_id( notification['template'], service_id=service.id, version=notification['template_version'], ) if sender_id: reply_to_text = dao_get_service_sms_senders_by_id(service_id, sender_id).sms_sender else: reply_to_text = template.reply_to_text if not service_allowed_to_send_to(notification['to'], service, KEY_TYPE_NORMAL): current_app.logger.debug( "SMS {} failed as restricted service".format(notification_id) ) return try: saved_notification = persist_notification( template_id=notification['template'], template_version=notification['template_version'], recipient=notification['to'], service=service, personalisation=notification.get('personalisation'), notification_type=SMS_TYPE, api_key_id=None, key_type=KEY_TYPE_NORMAL, created_at=datetime.utcnow(), job_id=notification.get('job', None), job_row_number=notification.get('row_number', None), notification_id=notification_id, reply_to_text=reply_to_text ) provider_tasks.deliver_sms.apply_async( [str(saved_notification.id)], queue=QueueNames.SEND_SMS if not service.research_mode else QueueNames.RESEARCH_MODE ) current_app.logger.debug( "SMS {} created at {} for job {}".format( saved_notification.id, saved_notification.created_at, notification.get('job', None)) ) except SQLAlchemyError as e: handle_exception(self, notification, notification_id, e)
def test_rejects_api_calls_with_international_numbers_if_service_does_not_allow_int_sms( key_type, notify_db_session, ): service = create_service(service_permissions=[SMS_TYPE]) service_model = SerialisedService.from_id(service.id) with pytest.raises(BadRequestError) as e: validate_and_format_recipient('20-12-1234-1234', key_type, service_model, SMS_TYPE) assert e.value.status_code == 400 assert e.value.message == 'Cannot send to international mobile numbers' assert e.value.fields == []
def test_should_not_rate_limit_if_limiting_is_disabled( sample_service, mocker): with freeze_time("2016-01-01 12:00:00.000000"): current_app.config['API_RATE_LIMIT_ENABLED'] = False mocker.patch('app.redis_store.exceeded_rate_limit', return_value=False) mocker.patch('app.notifications.validators.services_dao') sample_service.restricted = True create_api_key(sample_service) serialised_service = SerialisedService.from_id(sample_service.id) serialised_api_key = SerialisedAPIKeyCollection.from_service_id(serialised_service.id)[0] check_service_over_api_rate_limit(serialised_service, serialised_api_key) assert not app.redis_store.exceeded_rate_limit.called
def test_should_set_cache_value_as_value_from_database_if_cache_not_set( key_type, sample_template, sample_service, mocker ): serialised_service = SerialisedService.from_id(sample_service.id) with freeze_time("2016-01-01 12:00:00.000000"): for _ in range(5): create_notification(sample_template) mocker.patch('app.notifications.validators.redis_store.get', return_value=None) mocker.patch('app.notifications.validators.redis_store.set') check_service_over_daily_message_limit(key_type, serialised_service) app.notifications.validators.redis_store.set.assert_called_with( str(sample_service.id) + "-2016-01-01-count", 5, ex=3600 )
def test_that_when_not_exceeded_rate_limit_request_succeeds( sample_service, mocker): with freeze_time("2016-01-01 12:00:00.000000"): mocker.patch('app.redis_store.exceeded_rate_limit', return_value=False) mocker.patch('app.notifications.validators.services_dao') sample_service.restricted = True api_key = create_api_key(sample_service) serialised_service = SerialisedService.from_id(sample_service.id) serialised_api_key = SerialisedAPIKeyCollection.from_service_id(serialised_service.id)[0] check_service_over_api_rate_limit(serialised_service, serialised_api_key) assert app.redis_store.exceeded_rate_limit.called_with( "{}-{}".format(str(sample_service.id), api_key.key_type), 3000, 60 )
def test_check_service_message_limit_in_cache_under_message_limit_passes( key_type, sample_service, mocker): mocker.patch('app.notifications.validators.redis_store.get', side_effect=[ None, # The serialised service 1, # The rolling count ]) mocker.patch('app.notifications.validators.redis_store.set') mocker.patch('app.notifications.validators.services_dao') serialised_service = SerialisedService.from_id(sample_service.id) check_service_over_daily_message_limit(key_type, serialised_service) app.notifications.validators.redis_store.set.assert_called_once_with( f'service-{serialised_service.id}', ANY, ex=ANY, ) assert not app.notifications.validators.services_dao.mock_calls
def save_api_email_or_sms(self, encrypted_notification): notification = encryption.decrypt(encrypted_notification) service = SerialisedService.from_id(notification['service_id']) q = QueueNames.SEND_EMAIL if notification[ 'notification_type'] == EMAIL_TYPE else QueueNames.SEND_SMS provider_task = provider_tasks.deliver_email if notification['notification_type'] == EMAIL_TYPE \ else provider_tasks.deliver_sms try: persist_notification( notification_id=notification["id"], template_id=notification['template_id'], template_version=notification['template_version'], recipient=notification['to'], service=service, personalisation=notification.get('personalisation'), notification_type=notification['notification_type'], client_reference=notification['client_reference'], api_key_id=notification.get('api_key_id'), key_type=KEY_TYPE_NORMAL, created_at=notification['created_at'], reply_to_text=notification['reply_to_text'], status=notification['status'], document_download_count=notification['document_download_count']) q = q if not service.research_mode else QueueNames.RESEARCH_MODE provider_task.apply_async([notification['id']], queue=q) current_app.logger.debug( f"{notification['notification_type']} {notification['id']} has been persisted and sent to delivery queue." ) except IntegrityError: current_app.logger.info( f"{notification['notification_type']} {notification['id']} already exists." ) except SQLAlchemyError: try: self.retry(queue=QueueNames.RETRY) except self.MaxRetriesExceededError: current_app.logger.error( f"Max retry failed Failed to persist notification {notification['id']}" )
def test_check_service_message_limit_over_message_limit_fails(key_type, sample_service, mocker): with freeze_time("2016-01-01 12:00:00.000000"): mocker.patch('app.redis_store.get', return_value=None) mocker.patch('app.notifications.validators.redis_store.set') sample_service.restricted = True sample_service.message_limit = 4 template = create_template(sample_service) serialised_service = SerialisedService.from_id(sample_service.id) for _ in range(5): create_notification(template) with pytest.raises(TooManyRequestsError) as e: check_service_over_daily_message_limit(key_type, serialised_service) assert e.value.status_code == 429 assert e.value.message == 'Exceeded send limits (4) for today' assert e.value.fields == [] app.notifications.validators.redis_store.set.assert_called_with( str(sample_service.id) + "-2016-01-01-count", 5, ex=3600 )
def test_get_html_email_options_return_email_branding_from_serialised_service( sample_service): branding = create_email_branding() sample_service.email_branding = branding service = SerialisedService.from_id(sample_service.id) email_options = get_html_email_options(service) assert email_options is not None assert email_options == { 'govuk_banner': branding.brand_type == BRANDING_BOTH, 'brand_banner': branding.brand_type == BRANDING_ORG_BANNER, 'brand_colour': branding.colour, 'brand_logo': get_logo_url(current_app.config['ADMIN_BASE_URL'], branding.logo), 'brand_text': branding.text, 'brand_name': branding.name, }
def test_should_not_interact_with_cache_for_test_key(sample_service, mocker): mocker.patch('app.notifications.validators.redis_store') mocker.patch('app.notifications.validators.redis_store.get', side_effect=[None]) serialised_service = SerialisedService.from_id(sample_service.id) check_service_over_daily_message_limit('test', serialised_service) assert not app.notifications.validators.redis_store.mock_calls
def test_allows_api_calls_with_international_numbers_if_service_does_allow_int_sms( key_type, sample_service_full_permissions): service_model = SerialisedService.from_id(sample_service_full_permissions.id) result = validate_and_format_recipient('20-12-1234-1234', key_type, service_model, SMS_TYPE) assert result == '201212341234'