def register_email_not_exists_with_recaptcha_invalid(self): """Yield a function for this step""" self.flow_started = True with patch( "authentication.views.requests.post", return_value=MockResponse( content= '{"success": false, "error-codes": ["bad-request"]}', status_code=status.HTTP_200_OK, ), ) as mock_recaptcha_failure, override_settings( **{"RECAPTCHA_SITE_KEY": "fakse"}): assert_api_call( self.client, "psa-register-email", { "flow": SocialAuthState.FLOW_REGISTER, "email": NEW_EMAIL, "recaptcha": "fake", }, { "error-codes": ["bad-request"], "success": False }, expect_status=status.HTTP_400_BAD_REQUEST, use_defaults=False, ) mock_recaptcha_failure.assert_called_once() self.mock_email_send.assert_not_called()
def test_is_json_response(content, content_type, expected): """ is_json_response should return True if the given response's content type indicates JSON content """ mock_response = MockResponse(status_code=400, content=content, content_type=content_type) assert is_json_response(mock_response) is expected
def test_enroll_pro_api_fail(mocker, user): """ Tests that enroll_in_edx_course_runs raises an EdxApiEnrollErrorException if the request fails for some reason besides an enrollment mode error """ mock_client = mocker.MagicMock() pro_enrollment_response = MockResponse({"message": "no dice"}, status_code=401) mock_client.enrollments.create_student_enrollment = mocker.Mock( side_effect=HTTPError(response=pro_enrollment_response)) mocker.patch("courseware.api.get_edx_api_client", return_value=mock_client) course_run = CourseRunFactory.build() with pytest.raises(EdxApiEnrollErrorException): enroll_in_edx_course_runs(user, [course_run])
def test_get_error_response_summary(content, content_type, exp_summary_content, exp_url_in_summary): """ get_error_response_summary should provide a summary of an error HTTP response object with the correct bits of information depending on the type of content. """ status_code = 400 url = "http://example.com" mock_response = MockResponse(status_code=status_code, content=content, content_type=content_type, url=url) summary = get_error_response_summary(mock_response) assert f"Response - code: {status_code}" in summary assert f"content: {exp_summary_content}" in summary assert (f"url: {url}" in summary) is exp_url_in_summary
def test_enroll_in_edx_course_runs_audit(mocker, user, error_text): """Tests that enroll_in_edx_course_runs fails over to attempting enrollment with 'audit' mode""" mock_client = mocker.MagicMock() pro_enrollment_response = MockResponse({"message": error_text}) audit_result = {"good": "result"} mock_client.enrollments.create_student_enrollment = mocker.Mock( side_effect=[ HTTPError(response=pro_enrollment_response), audit_result ]) patched_log_error = mocker.patch("courseware.api.log.error") mocker.patch("courseware.api.get_edx_api_client", return_value=mock_client) course_run = CourseRunFactory.build() results = enroll_in_edx_course_runs(user, [course_run]) assert mock_client.enrollments.create_student_enrollment.call_count == 2 mock_client.enrollments.create_student_enrollment.assert_any_call( course_run.courseware_id, mode=EDX_ENROLLMENT_PRO_MODE) mock_client.enrollments.create_student_enrollment.assert_any_call( course_run.courseware_id, mode=EDX_ENROLLMENT_AUDIT_MODE) assert results == [audit_result] patched_log_error.assert_called_once()
class AuthStateMachine(RuleBasedStateMachine): """ State machine for auth flows How to understand this code: This code exercises our social auth APIs, which is basically a graph of nodes and edges that the user traverses. You can understand the bundles defined below to be the nodes and the methods of this class to be the edges. If you add a new state to the auth flows, create a new bundle to represent that state and define methods to define transitions into and (optionally) out of that state. """ # pylint: disable=too-many-instance-attributes ConfirmationSentAuthStates = Bundle("confirmation-sent") ConfirmationRedeemedAuthStates = Bundle("confirmation-redeemed") RegisterExtraDetailsAuthStates = Bundle("register-details-extra") LoginPasswordAuthStates = Bundle("login-password") LoginPasswordAbandonedAuthStates = Bundle("login-password-abandoned") recaptcha_patcher = patch( "authentication.views.requests.post", return_value=MockResponse(content='{"success": true}', status_code=status.HTTP_200_OK), ) email_send_patcher = patch("mail.verification_api.send_verification_email", autospec=True) courseware_api_patcher = patch( "authentication.pipeline.user.courseware_api") courseware_tasks_patcher = patch( "authentication.pipeline.user.courseware_tasks") def __init__(self): """Setup the machine""" super().__init__() # wrap the execution in a django transaction, similar to django's TestCase self.atomic = transaction.atomic() self.atomic.__enter__() # wrap the execution in a patch() self.mock_email_send = self.email_send_patcher.start() self.mock_courseware_api = self.courseware_api_patcher.start() self.mock_courseware_tasks = self.courseware_tasks_patcher.start() # django test client self.client = Client() # shared data self.email = fake.email() self.user = None self.password = "******" # track whether we've hit an action that starts a flow or not self.flow_started = False def teardown(self): """Cleanup from a run""" # clear the mailbox del mail.outbox[:] # stop the patches self.email_send_patcher.stop() self.courseware_api_patcher.stop() self.courseware_tasks_patcher.stop() # end the transaction with a rollback to cleanup any state transaction.set_rollback(True) self.atomic.__exit__(None, None, None) def create_existing_user(self): """Create an existing user""" self.user = UserFactory.create(email=self.email) self.user.set_password(self.password) self.user.save() UserSocialAuthFactory.create(user=self.user, provider=EmailAuth.name, uid=self.user.email) @rule( target=ConfirmationSentAuthStates, recaptcha_enabled=st.sampled_from([True, False]), ) @precondition(lambda self: not self.flow_started) def register_email_not_exists(self, recaptcha_enabled): """Register email not exists""" self.flow_started = True with ExitStack() as stack: mock_recaptcha_success = None if recaptcha_enabled: mock_recaptcha_success = stack.enter_context( self.recaptcha_patcher) stack.enter_context( override_settings(**{"RECAPTCHA_SITE_KEY": "fake"})) result = assert_api_call( self.client, "psa-register-email", { "flow": SocialAuthState.FLOW_REGISTER, "email": self.email, **({ "recaptcha": "fake" } if recaptcha_enabled else {}), }, { "flow": SocialAuthState.FLOW_REGISTER, "partial_token": None, "state": SocialAuthState.STATE_REGISTER_CONFIRM_SENT, }, ) self.mock_email_send.assert_called_once() if mock_recaptcha_success: mock_recaptcha_success.assert_called_once() return result @rule(target=LoginPasswordAuthStates, recaptcha_enabled=st.sampled_from([True, False])) @precondition(lambda self: not self.flow_started) def register_email_exists(self, recaptcha_enabled): """Register email exists""" self.flow_started = True self.create_existing_user() with ExitStack() as stack: mock_recaptcha_success = None if recaptcha_enabled: mock_recaptcha_success = stack.enter_context( self.recaptcha_patcher) stack.enter_context( override_settings(**{"RECAPTCHA_SITE_KEY": "fake"})) result = assert_api_call( self.client, "psa-register-email", { "flow": SocialAuthState.FLOW_REGISTER, "email": self.email, "next": NEXT_URL, **({ "recaptcha": "fake" } if recaptcha_enabled else {}), }, { "flow": SocialAuthState.FLOW_REGISTER, "state": SocialAuthState.STATE_LOGIN_PASSWORD, "errors": ["Password is required to login"], }, ) self.mock_email_send.assert_not_called() if mock_recaptcha_success: mock_recaptcha_success.assert_called_once() return result @rule() @precondition(lambda self: not self.flow_started) def register_email_not_exists_with_recaptcha_invalid(self): """Yield a function for this step""" self.flow_started = True with patch( "authentication.views.requests.post", return_value=MockResponse( content= '{"success": false, "error-codes": ["bad-request"]}', status_code=status.HTTP_200_OK, ), ) as mock_recaptcha_failure, override_settings( **{"RECAPTCHA_SITE_KEY": "fakse"}): assert_api_call( self.client, "psa-register-email", { "flow": SocialAuthState.FLOW_REGISTER, "email": NEW_EMAIL, "recaptcha": "fake", }, { "error-codes": ["bad-request"], "success": False }, expect_status=status.HTTP_400_BAD_REQUEST, use_defaults=False, ) mock_recaptcha_failure.assert_called_once() self.mock_email_send.assert_not_called() @rule() @precondition(lambda self: not self.flow_started) def login_email_not_exists(self): """Login for an email that doesn't exist""" self.flow_started = True assert_api_call( self.client, "psa-login-email", { "flow": SocialAuthState.FLOW_LOGIN, "email": self.email }, { "field_errors": { "email": "Couldn't find your account" }, "flow": SocialAuthState.FLOW_LOGIN, "partial_token": None, "state": SocialAuthState.STATE_REGISTER_REQUIRED, }, ) assert User.objects.filter(email=self.email).exists() is False @rule(target=LoginPasswordAuthStates) @precondition(lambda self: not self.flow_started) def login_email_exists(self): """Login with a user that exists""" self.flow_started = True self.create_existing_user() return assert_api_call( self.client, "psa-login-email", { "flow": SocialAuthState.FLOW_LOGIN, "email": self.user.email, "next": NEXT_URL, }, { "flow": SocialAuthState.FLOW_LOGIN, "state": SocialAuthState.STATE_LOGIN_PASSWORD, "extra_data": { "name": self.user.name }, }, ) @rule( target=LoginPasswordAbandonedAuthStates, auth_state=consumes(RegisterExtraDetailsAuthStates), ) @precondition(lambda self: self.flow_started) def login_email_abandoned(self, auth_state): # pylint: disable=unused-argument """Login with a user that abandoned the register flow""" # NOTE: This works by "consuming" an extra details auth state, # but discarding the state and starting a new login. # It then re-targets the new state into the extra details again. auth_state = None # assign None to ensure no accidental usage here return assert_api_call( self.client, "psa-login-email", { "flow": SocialAuthState.FLOW_LOGIN, "email": self.user.email, "next": NEXT_URL, }, { "flow": SocialAuthState.FLOW_LOGIN, "state": SocialAuthState.STATE_LOGIN_PASSWORD, "extra_data": { "name": self.user.name }, }, ) @rule( target=RegisterExtraDetailsAuthStates, auth_state=consumes(LoginPasswordAbandonedAuthStates), ) def login_password_abandoned(self, auth_state): """Login with an abandoned registration user""" return assert_api_call( self.client, "psa-login-password", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, }, { "flow": auth_state["flow"], "state": SocialAuthState.STATE_REGISTER_EXTRA_DETAILS, }, ) @rule(auth_state=consumes(LoginPasswordAuthStates)) def login_password_valid(self, auth_state): """Login with a valid password""" assert_api_call( self.client, "psa-login-password", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, }, { "flow": auth_state["flow"], "redirect_url": NEXT_URL, "partial_token": None, "state": SocialAuthState.STATE_SUCCESS, }, expect_authenticated=True, ) @rule(target=LoginPasswordAuthStates, auth_state=consumes(LoginPasswordAuthStates)) def login_password_invalid(self, auth_state): """Login with an invalid password""" return assert_api_call( self.client, "psa-login-password", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": "******", }, { "field_errors": { "password": "******" }, "flow": auth_state["flow"], "state": SocialAuthState.STATE_ERROR, }, ) @rule( auth_state=consumes(LoginPasswordAuthStates), verify_exports=st.sampled_from([True, False]), ) def login_password_user_inactive(self, auth_state, verify_exports): """Login for an inactive user""" self.user.is_active = False self.user.save() cm = export_check_response("100_success") if verify_exports else noop() with cm: assert_api_call( self.client, "psa-login-password", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, }, { "flow": auth_state["flow"], "redirect_url": NEXT_URL, "partial_token": None, "state": SocialAuthState.STATE_SUCCESS, }, expect_authenticated=True, ) @rule(auth_state=consumes(LoginPasswordAuthStates)) def login_password_exports_temporary_error(self, auth_state): """Login for a user who hasn't been OFAC verified yet""" with override_settings(**get_cybersource_test_settings()), patch( "authentication.pipeline.compliance.api.verify_user_with_exports", side_effect=Exception( "register_details_export_temporary_error"), ): assert_api_call( self.client, "psa-login-password", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, }, { "flow": auth_state["flow"], "partial_token": None, "state": SocialAuthState.STATE_ERROR_TEMPORARY, "errors": [ "Unable to register at this time, please try again later" ], }, ) @rule( target=ConfirmationRedeemedAuthStates, auth_state=consumes(ConfirmationSentAuthStates), ) def redeem_confirmation_code(self, auth_state): """Redeem a registration confirmation code""" _, _, code, partial_token = self.mock_email_send.call_args[0] return assert_api_call( self.client, "psa-register-confirm", { "flow": auth_state["flow"], "verification_code": code.code, "partial_token": partial_token, }, { "flow": auth_state["flow"], "state": SocialAuthState.STATE_REGISTER_DETAILS, }, ) @rule(auth_state=consumes(ConfirmationRedeemedAuthStates)) def redeem_confirmation_code_twice(self, auth_state): """Redeeming a code twice should fail""" _, _, code, partial_token = self.mock_email_send.call_args[0] assert_api_call( self.client, "psa-register-confirm", { "flow": auth_state["flow"], "verification_code": code.code, "partial_token": partial_token, }, { "errors": [], "flow": auth_state["flow"], "redirect_url": None, "partial_token": None, "state": SocialAuthState.STATE_INVALID_LINK, }, ) @rule(auth_state=consumes(ConfirmationRedeemedAuthStates)) def redeem_confirmation_code_twice_existing_user(self, auth_state): """Redeeming a code twice with an existing user should fail with existing account state""" _, _, code, partial_token = self.mock_email_send.call_args[0] self.create_existing_user() assert_api_call( self.client, "psa-register-confirm", { "flow": auth_state["flow"], "verification_code": code.code, "partial_token": partial_token, }, { "errors": [], "flow": auth_state["flow"], "redirect_url": None, "partial_token": None, "state": SocialAuthState.STATE_EXISTING_ACCOUNT, }, ) @rule( target=RegisterExtraDetailsAuthStates, auth_state=consumes(ConfirmationRedeemedAuthStates), ) def register_details(self, auth_state): """Complete the register confirmation details page""" result = assert_api_call( self.client, "psa-register-details", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, "name": "Sally Smith", "legal_address": { "first_name": "Sally", "last_name": "Smith", "street_address": ["Main Street"], "country": "US", "state_or_territory": "US-CO", "city": "Boulder", "postal_code": "02183", }, }, { "flow": auth_state["flow"], "state": SocialAuthState.STATE_REGISTER_EXTRA_DETAILS, }, ) self.user = User.objects.get(email=self.email) return result @rule( target=RegisterExtraDetailsAuthStates, auth_state=consumes(ConfirmationRedeemedAuthStates), ) def register_details_export_success(self, auth_state): """Complete the register confirmation details page with exports enabled""" with export_check_response("100_success"): result = assert_api_call( self.client, "psa-register-details", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, "name": "Sally Smith", "legal_address": { "first_name": "Sally", "last_name": "Smith", "street_address": ["Main Street"], "country": "US", "state_or_territory": "US-CO", "city": "Boulder", "postal_code": "02183", }, }, { "flow": auth_state["flow"], "state": SocialAuthState.STATE_REGISTER_EXTRA_DETAILS, }, ) assert ExportsInquiryLog.objects.filter( user__email=self.email).exists() assert (ExportsInquiryLog.objects.get( user__email=self.email).computed_result == RESULT_SUCCESS) assert len(mail.outbox) == 0 self.user = User.objects.get(email=self.email) return result @rule(auth_state=consumes(ConfirmationRedeemedAuthStates)) def register_details_export_reject(self, auth_state): """Complete the register confirmation details page with exports enabled""" with export_check_response("700_reject"): assert_api_call( self.client, "psa-register-details", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, "name": "Sally Smith", "legal_address": { "first_name": "Sally", "last_name": "Smith", "street_address": ["Main Street"], "country": "US", "state_or_territory": "US-CO", "city": "Boulder", "postal_code": "02183", }, }, { "flow": auth_state["flow"], "partial_token": None, "errors": ["Error code: CS_700"], "state": SocialAuthState.STATE_USER_BLOCKED, }, ) assert ExportsInquiryLog.objects.filter( user__email=self.email).exists() assert (ExportsInquiryLog.objects.get( user__email=self.email).computed_result == RESULT_DENIED) assert len(mail.outbox) == 1 @rule(auth_state=consumes(ConfirmationRedeemedAuthStates)) def register_details_export_temporary_error(self, auth_state): """Complete the register confirmation details page with exports raising a temporary error""" with override_settings(**get_cybersource_test_settings()), patch( "authentication.pipeline.compliance.api.verify_user_with_exports", side_effect=Exception( "register_details_export_temporary_error"), ): assert_api_call( self.client, "psa-register-details", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "password": self.password, "name": "Sally Smith", "legal_address": { "first_name": "Sally", "last_name": "Smith", "street_address": ["Main Street"], "country": "US", "state_or_territory": "US-CO", "city": "Boulder", "postal_code": "02183", }, }, { "flow": auth_state["flow"], "partial_token": None, "errors": [ "Unable to register at this time, please try again later" ], "state": SocialAuthState.STATE_ERROR_TEMPORARY, }, ) assert not ExportsInquiryLog.objects.filter( user__email=self.email).exists() assert len(mail.outbox) == 0 @rule(auth_state=consumes(RegisterExtraDetailsAuthStates)) def register_user_extra_details(self, auth_state): """Complete the user's extra details""" assert_api_call( Client(), "psa-register-extra", { "flow": auth_state["flow"], "partial_token": auth_state["partial_token"], "gender": "f", "birth_year": "2000", "company": "MIT", "job_title": "QA Manager", }, { "flow": auth_state["flow"], "state": SocialAuthState.STATE_SUCCESS, "partial_token": None, }, expect_authenticated=True, )
def test_mock_response(content, expected_content, expected_json): """ assert MockResponse returns correct values """ response = MockResponse(content, 404) assert response.status_code == 404 assert response.content == expected_content assert response.json() == expected_json