def test_cannot_make_credit_request_after_response(self, status): # Create the first request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Provider updates the status api.update_credit_request_status(request["parameters"]["request_uuid"], self.PROVIDER_ID, status) # Attempting a second request raises an exception with self.assertRaises(RequestAlreadyCompleted): api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username'])
def test_user_is_not_eligible(self, requirement_status): # Simulate a user who is not eligible for credit CreditEligibility.objects.all().delete() status = CreditRequirementStatus.objects.get(username=self.USER_INFO['username']) status.status = requirement_status status.reason = {} status.save() with self.assertRaises(UserIsNotEligible): api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username'])
def test_user_is_not_eligible(self, status): # Simulate a user who is not eligible for credit CreditEligibility.objects.all().delete() status = CreditRequirementStatus.objects.get(username=self.USER_INFO['username']) status.status = status status.reason = {} status.save() with self.assertRaises(UserIsNotEligible): api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username'])
def test_create_credit_request_for_second_course(self): # Create the first request first_request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Create a request for a second course other_course_key = CourseKey.from_string("edX/other/2015") self._configure_credit(course_key=other_course_key) second_request = api.create_credit_request(other_course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Check that the requests have the correct course number self.assertEqual(first_request["parameters"]["course_num"], self.course_key.course) self.assertEqual(second_request["parameters"]["course_num"], other_course_key.course)
def test_user_has_no_final_grade(self): # Simulate an error condition that should never happen: # a user is eligible for credit, but doesn't have a final # grade recorded in the eligibility requirement. grade_status = CreditRequirementStatus.objects.get( username=self.USER_INFO["username"], requirement__namespace="grade", requirement__name="grade" ) grade_status.reason = {} grade_status.save() with self.assertRaises(UserIsNotEligible): api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"])
def test_reuse_credit_request(self): # Create the first request first_request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Update the user's profile information, then attempt a second request self.user.profile.name = "Bobby" self.user.profile.save() second_request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Request UUID should be the same self.assertEqual(first_request['uuid'], second_request['uuid']) # Request should use the updated information self.assertEqual(second_request['user_full_name'], "Bobby")
def test_user_has_no_final_grade(self): # Simulate an error condition that should never happen: # a user is eligible for credit, but doesn't have a final # grade recorded in the eligibility requirement. grade_status = CreditRequirementStatus.objects.get( username=self.USER_INFO['username'], requirement__namespace="grade", requirement__name="grade") grade_status.reason = {} grade_status.save() with self.assertRaises(UserIsNotEligible): api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"])
def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Validate the URL and method self.assertIn('url', request) self.assertEqual(request['url'], self.PROVIDER_URL) self.assertIn('method', request) self.assertEqual(request['method'], "POST") self.assertIn('parameters', request) parameters = request['parameters'] # Validate the UUID self.assertIn('request_uuid', parameters) self.assertEqual(len(parameters['request_uuid']), 32) # Validate the timestamp self.assertIn('timestamp', parameters) parsed_date = from_timestamp(parameters['timestamp']) self.assertLess(parsed_date, datetime.datetime.now(pytz.UTC)) # Validate course information self.assertEqual(parameters['course_org'], self.course_key.org) self.assertEqual(parameters['course_num'], self.course_key.course) self.assertEqual(parameters['course_run'], self.course_key.run) self.assertEqual(parameters['final_grade'], unicode(self.FINAL_GRADE)) # Validate user information for key in self.USER_INFO.keys(): param_key = 'user_{key}'.format(key=key) self.assertIn(param_key, parameters) expected = '' if key == 'mailing_address' else self.USER_INFO[key] self.assertEqual(parameters[param_key], expected)
def post(self, request, provider_id): """ POST handler. """ # Get the provider, or return HTTP 404 if it doesn't exist provider = generics.get_object_or_404(CreditProvider, provider_id=provider_id) # Validate the course key course_key = request.data.get('course_key') try: course_key = CourseKey.from_string(course_key) except InvalidKeyError: raise InvalidCourseKey(course_key) # Validate the username username = request.data.get('username') if not username: raise ValidationError({'detail': 'A username must be specified.'}) # Ensure the user is actually eligible to receive credit if not CreditEligibility.is_user_eligible_for_credit( course_key, username): raise UserNotEligibleException(course_key, username) try: credit_request = create_credit_request(course_key, provider.provider_id, username) return Response(credit_request) except CreditApiBadRequest as ex: raise InvalidCreditRequest(text_type(ex))
def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Validate the UUID self.assertIn('uuid', request) self.assertEqual(len(request['uuid']), 32) # Validate the timestamp self.assertIn('timestamp', request) parsed_date = date_parser.parse(request['timestamp']) self.assertTrue(parsed_date < datetime.datetime.now(pytz.UTC)) # Validate course information self.assertIn('course_org', request) self.assertEqual(request['course_org'], self.course_key.org) self.assertIn('course_num', request) self.assertEqual(request['course_num'], self.course_key.course) self.assertIn('course_run', request) self.assertEqual(request['course_run'], self.course_key.run) self.assertIn('final_grade', request) self.assertEqual(request['final_grade'], self.FINAL_GRADE) # Validate user information for key in self.USER_INFO.keys(): request_key = 'user_{key}'.format(key=key) self.assertIn(request_key, request) self.assertEqual(request[request_key], self.USER_INFO[key])
def test_query_counts(self): # Yes, this is a lot of queries, but this API call is also doing a lot of work :) # - 1 query: Check the user's eligibility and retrieve the credit course # - 1 Get the provider of the credit course. # - 2 queries: Get-or-create the credit request. # - 1 query: Retrieve user account and profile information from the user API. # - 1 query: Look up the user's final grade from the credit requirements table. # - 1 query: Look up the user's enrollment date in the course. # - 2 query: Look up the user's completion date in the course. # - 1 query: Update the request. # - 2 queries: Update the history table for the request. # - 4 Django savepoints with self.assertNumQueries(16): request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # - 2 queries: Retrieve and update the request # - 1 query: Update the history table for the request. uuid = request["parameters"]["request_uuid"] with self.assertNumQueries(3): api.update_credit_request_status(uuid, self.PROVIDER_ID, "approved") with self.assertNumQueries(1): api.get_credit_requests_for_user(self.USER_INFO["username"])
def post(self, request, provider_id): """ POST handler. """ # Get the provider, or return HTTP 404 if it doesn't exist provider = generics.get_object_or_404(CreditProvider, provider_id=provider_id) # Validate the course key course_key = request.data.get('course_key') try: course_key = CourseKey.from_string(course_key) except InvalidKeyError: raise InvalidCourseKey(course_key) # Validate the username username = request.data.get('username') if not username: raise ValidationError({'detail': 'A username must be specified.'}) # Ensure the user is actually eligible to receive credit if not CreditEligibility.is_user_eligible_for_credit(course_key, username): raise UserNotEligibleException(course_key, username) try: credit_request = create_credit_request(course_key, provider.provider_id, username) return Response(credit_request) except CreditApiBadRequest as ex: raise InvalidCreditRequest(text_type(ex))
def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Validate the URL and method self.assertIn('url', request) self.assertEqual(request['url'], self.PROVIDER_URL) self.assertIn('method', request) self.assertEqual(request['method'], "POST") self.assertIn('parameters', request) parameters = request['parameters'] # Validate the UUID self.assertIn('request_uuid', parameters) self.assertEqual(len(parameters['request_uuid']), 32) # Validate the timestamp self.assertIn('timestamp', parameters) parsed_date = from_timestamp(parameters['timestamp']) self.assertTrue(parsed_date < datetime.datetime.now(pytz.UTC)) # Validate course information self.assertEqual(parameters['course_org'], self.course_key.org) self.assertEqual(parameters['course_num'], self.course_key.course) self.assertEqual(parameters['course_run'], self.course_key.run) self.assertEqual(parameters['final_grade'], unicode(self.FINAL_GRADE)) # Validate user information for key in self.USER_INFO.keys(): param_key = 'user_{key}'.format(key=key) self.assertIn(param_key, parameters) expected = '' if key == 'mailing_address' else self.USER_INFO[key] self.assertEqual(parameters[param_key], expected)
def _initiate_request(self): """Initiate a request for credit from a provider. """ request = credit_api.create_credit_request( self.course.id, # pylint: disable=no-member self.PROVIDER_ID, self.USERNAME) return request["parameters"]["request_uuid"]
def test_credit_request(self): # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Validate the URL and method self.assertIn("url", request) self.assertEqual(request["url"], self.PROVIDER_URL) self.assertIn("method", request) self.assertEqual(request["method"], "POST") self.assertIn("parameters", request) parameters = request["parameters"] # Validate the UUID self.assertIn("request_uuid", parameters) self.assertEqual(len(parameters["request_uuid"]), 32) # Validate the timestamp self.assertIn("timestamp", parameters) parsed_date = from_timestamp(parameters["timestamp"]) self.assertLess(parsed_date, datetime.datetime.now(pytz.UTC)) # Validate course information self.assertEqual(parameters["course_org"], self.course_key.org) self.assertEqual(parameters["course_num"], self.course_key.course) self.assertEqual(parameters["course_run"], self.course_key.run) self.assertEqual(parameters["final_grade"], unicode(self.FINAL_GRADE)) # Validate user information for key in self.USER_INFO.keys(): param_key = "user_{key}".format(key=key) self.assertIn(param_key, parameters) expected = "" if key == "mailing_address" else self.USER_INFO[key] self.assertEqual(parameters[param_key], expected)
def _initiate_request(self): """Initiate a request for credit from a provider. """ request = credit_api.create_credit_request( self.course.id, # pylint: disable=no-member self.PROVIDER_ID, self.USERNAME ) return request["parameters"]["request_uuid"]
def test_create_request_null_mailing_address(self): # User did not specify a mailing address self.user.profile.mailing_address = None self.user.profile.save() # Request should include an empty mailing address field request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) self.assertEqual(request["parameters"]["user_mailing_address"], "")
def test_reuse_credit_request(self): # Create the first request first_request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Update the user's profile information, then attempt a second request self.user.profile.name = "Bobby" self.user.profile.save() second_request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Request UUID should be the same self.assertEqual( first_request["parameters"]["request_uuid"], second_request["parameters"]["request_uuid"] ) # Request should use the updated information self.assertEqual(second_request["parameters"]["user_full_name"], "Bobby")
def test_update_invalid_credit_status(self): # The request status must be either "approved" or "rejected" request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) with self.assertRaises(InvalidCreditStatus): api.update_credit_request_status( request["parameters"]["request_uuid"], self.PROVIDER_ID, "invalid")
def test_credit_request_status(self, status): request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # Initial status should be "pending" self._assert_credit_status("pending") # Update the status api.update_credit_request_status(request['uuid'], status) self._assert_credit_status(status)
def test_create_request_null_country(self): # Simulate users who registered accounts before the country field was introduced. # We need to manipulate the database directly because the country Django field # coerces None values to empty strings. query = "UPDATE auth_userprofile SET country = NULL WHERE id = %s" connection.cursor().execute(query, [str(self.user.profile.id)]) # Request should include an empty country field request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) self.assertEqual(request["parameters"]["user_country"], "")
def test_credit_request_disable_integration(self): CreditProvider.objects.all().update(enable_integration=False) # Initiate a request with automatic integration disabled request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # We get a URL and a GET method, so we can provide students # with a link to the credit provider, where they can request # credit directly. self.assertIn("url", request) self.assertEqual(request["url"], self.PROVIDER_URL) self.assertIn("method", request) self.assertEqual(request["method"], "GET")
def test_credit_messages(self): self._set_creditcourse() requirement = CreditRequirement.objects.create( course=self.credit_course, namespace="grade", name="grade", active=True ) status = CreditRequirementStatus.objects.create( username=self.student.username, requirement=requirement, ) status.status = "satisfied" status.reason = {"final_grade": self.FINAL_GRADE} status.save() self._set_user_eligible(self.credit_course, self.student.username) response = self.client.get(reverse("dashboard")) self.assertContains( response, "<b>Congratulations</b> {}, You have meet requirements for credit.".format( self.student.get_full_name() # pylint: disable=no-member ) ) api.create_credit_request(self.course.id, self.first_provider.provider_id, self.student.username) response = self.client.get(reverse("dashboard")) self.assertContains( response, 'Thank you, your payment is complete, your credit is processing. ' 'Please see {provider_link} for more information.'.format( provider_link='<a href="#" target="_blank">{provider_name}</a>'.format( provider_name=self.first_provider.display_name ) ) )
def test_create_credit_request_grade_length(self): """ Verify the length of the final grade is limited to seven (7) characters total. This is a hack for ASU. """ # Update the user's grade status = CreditRequirementStatus.objects.get(username=self.USER_INFO["username"]) status.status = "satisfied" status.reason = {"final_grade": 1.0 / 3.0} status.save() # Initiate a credit request request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) self.assertEqual(request['parameters']['final_grade'], u'0.33333')
def test_credit_request_status(self, status): request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # Initial status should be "pending" self._assert_credit_status("pending") credit_request_status = api.get_credit_request_status(self.USER_INFO['username'], self.course_key) self.assertEqual(credit_request_status["status"], "pending") # Update the status api.update_credit_request_status(request["parameters"]["request_uuid"], self.PROVIDER_ID, status) self._assert_credit_status(status) credit_request_status = api.get_credit_request_status(self.USER_INFO['username'], self.course_key) self.assertEqual(credit_request_status["status"], status)
def test_query_counts(self): # Yes, this is a lot of queries, but this API call is also doing a lot of work :) # - 1 query: Check the user's eligibility and retrieve the credit course and provider. # - 2 queries: Get-or-create the credit request. # - 1 query: Retrieve user account and profile information from the user API. # - 1 query: Look up the user's final grade from the credit requirements table. # - 2 queries: Update the request. # - 2 queries: Update the history table for the request. with self.assertNumQueries(9): request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO['username']) # - 3 queries: Retrieve and update the request # - 1 query: Update the history table for the request. with self.assertNumQueries(4): api.update_credit_request_status(request['uuid'], "approved") with self.assertNumQueries(1): api.get_credit_requests_for_user(self.USER_INFO['username'])
def test_query_counts(self): # Yes, this is a lot of queries, but this API call is also doing a lot of work :) # - 1 query: Check the user's eligibility and retrieve the credit course # - 1 Get the provider of the credit course. # - 2 queries: Get-or-create the credit request. # - 1 query: Retrieve user account and profile information from the user API. # - 1 query: Look up the user's final grade from the credit requirements table. # - 1 query: Update the request. # - 2 queries: Update the history table for the request. # - 4 Django savepoints with self.assertNumQueries(13): request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) # - 2 queries: Retrieve and update the request # - 1 query: Update the history table for the request. uuid = request["parameters"]["request_uuid"] with self.assertNumQueries(3): api.update_credit_request_status(uuid, self.PROVIDER_ID, "approved") with self.assertNumQueries(1): api.get_credit_requests_for_user(self.USER_INFO["username"])
def create_credit_request(request, provider_id): """ Initiate a request for credit in a course. This end-point will get-or-create a record in the database to track the request. It will then calculate the parameters to send to the credit provider and digitally sign the parameters, using a secret key shared with the credit provider. The user's browser is responsible for POSTing these parameters directly to the credit provider. **Example Usage:** POST /api/credit/v1/providers/hogwarts/request/ { "username": "******", "course_key": "edX/DemoX/Demo_Course" } Response: 200 OK Content-Type: application/json { "url": "http://example.com/request-credit", "method": "POST", "parameters": { request_uuid: "557168d0f7664fe59097106c67c3f847" timestamp: 1434631630, course_org: "ASUx" course_num: "DemoX" course_run: "1T2015" final_grade: 0.95, user_username: "******", user_email: "*****@*****.**" user_full_name: "John Smith" user_mailing_address: "", user_country: "US", signature: "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI=" } } **Parameters:** * username (unicode): The username of the user requesting credit. * course_key (unicode): The identifier for the course for which the user is requesting credit. **Responses:** * 200 OK: The request was created successfully. Returned content is a JSON-encoded dictionary describing what the client should send to the credit provider. * 400 Bad Request: - The provided course key did not correspond to a valid credit course. - The user already has a completed credit request for this course and provider. * 403 Not Authorized: - The username does not match the name of the logged in user. - The user is not eligible for credit in the course. * 404 Not Found: - The provider does not exist. """ response, parameters = _validate_json_parameters(request.body, ["username", "course_key"]) if response is not None: return response try: course_key = CourseKey.from_string(parameters["course_key"]) except InvalidKeyError: return HttpResponseBadRequest( u'Could not parse "{course_key}" as a course key'.format( course_key=parameters["course_key"] ) ) # Check user authorization if not (request.user and request.user.username == parameters["username"]): log.warning( u'User with ID %s attempted to initiate a credit request for user with username "%s"', request.user.id if request.user else "[Anonymous]", parameters["username"] ) return HttpResponseForbidden("Users are not allowed to initiate credit requests for other users.") # Initiate the request try: credit_request = api.create_credit_request(course_key, provider_id, parameters["username"]) except CreditApiBadRequest as ex: return HttpResponseBadRequest(ex) else: return JsonResponse(credit_request)
def test_create_credit_request_address_empty(self): """ Verify the mailing address is always empty. """ request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.user.username) self.assertEqual(request['parameters']['user_mailing_address'], '')
def test_update_invalid_credit_status(self): # The request status must be either "approved" or "rejected" request = api.create_credit_request(self.course_key, self.PROVIDER_ID, self.USER_INFO["username"]) with self.assertRaises(InvalidCreditStatus): api.update_credit_request_status(request["parameters"]["request_uuid"], self.PROVIDER_ID, "invalid")
def create_credit_request(request, provider_id): """ Initiate a request for credit in a course. This end-point will get-or-create a record in the database to track the request. It will then calculate the parameters to send to the credit provider and digitally sign the parameters, using a secret key shared with the credit provider. The user's browser is responsible for POSTing these parameters directly to the credit provider. **Example Usage:** POST /api/credit/v1/providers/hogwarts/request/ { "username": "******", "course_key": "edX/DemoX/Demo_Course" } Response: 200 OK Content-Type: application/json { "url": "http://example.com/request-credit", "method": "POST", "parameters": { request_uuid: "557168d0f7664fe59097106c67c3f847" timestamp: 1434631630, course_org: "ASUx" course_num: "DemoX" course_run: "1T2015" final_grade: "0.95", user_username: "******", user_email: "*****@*****.**" user_full_name: "John Smith" user_mailing_address: "", user_country: "US", signature: "cRCNjkE4IzY+erIjRwOQCpRILgOvXx4q2qvx141BCqI=" } } **Parameters:** * username (unicode): The username of the user requesting credit. * course_key (unicode): The identifier for the course for which the user is requesting credit. **Responses:** * 200 OK: The request was created successfully. Returned content is a JSON-encoded dictionary describing what the client should send to the credit provider. * 400 Bad Request: - The provided course key did not correspond to a valid credit course. - The user already has a completed credit request for this course and provider. * 403 Not Authorized: - The username does not match the name of the logged in user. - The user is not eligible for credit in the course. * 404 Not Found: - The provider does not exist. """ response, parameters = _validate_json_parameters( request.body, ["username", "course_key"]) if response is not None: return response try: course_key = CourseKey.from_string(parameters["course_key"]) except InvalidKeyError: return HttpResponseBadRequest( u'Could not parse "{course_key}" as a course key'.format( course_key=parameters["course_key"])) # Check user authorization if not (request.user and request.user.username == parameters["username"]): log.warning( u'User with ID %s attempted to initiate a credit request for user with username "%s"', request.user.id if request.user else "[Anonymous]", parameters["username"]) return HttpResponseForbidden( "Users are not allowed to initiate credit requests for other users." ) # Initiate the request try: credit_request = api.create_credit_request(course_key, provider_id, parameters["username"]) except CreditApiBadRequest as ex: return HttpResponseBadRequest(ex) else: return JsonResponse(credit_request)