Ejemplo n.º 1
0
 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()
Ejemplo n.º 2
0
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
Ejemplo n.º 3
0
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])
Ejemplo n.º 4
0
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
Ejemplo n.º 5
0
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()
Ejemplo n.º 6
0
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,
        )
Ejemplo n.º 7
0
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