From e7570c52e68cef8f8bcce05bfcf77b74b2aa5042 Mon Sep 17 00:00:00 2001
From: stephensanchez <steve@edx.org>
Date: Wed, 22 Oct 2014 18:08:37 +0000
Subject: [PATCH] adding all the tests for data and api.

Updating tests for views

Last chunk of tests and pep8 cleanup

Code Review cleanup.

Additional CR comments

Changing serialization of suggested prices.
---
 common/djangoapps/enrollment/api.py           | 317 +++++++++++++-----
 common/djangoapps/enrollment/data.py          |  67 +++-
 common/djangoapps/enrollment/serializers.py   |  42 ++-
 .../enrollment/tests/fake_data_api.py         |  96 ++++++
 .../djangoapps/enrollment/tests/test_api.py   | 151 +++++++++
 .../djangoapps/enrollment/tests/test_data.py  | 172 ++++++++++
 .../djangoapps/enrollment/tests/test_views.py | 152 +++++++++
 common/djangoapps/enrollment/views.py         |  54 ++-
 8 files changed, 950 insertions(+), 101 deletions(-)
 create mode 100644 common/djangoapps/enrollment/tests/fake_data_api.py
 create mode 100644 common/djangoapps/enrollment/tests/test_api.py
 create mode 100644 common/djangoapps/enrollment/tests/test_data.py
 create mode 100644 common/djangoapps/enrollment/tests/test_views.py

diff --git a/common/djangoapps/enrollment/api.py b/common/djangoapps/enrollment/api.py
index 0c65f4de66e..ea76e032a88 100644
--- a/common/djangoapps/enrollment/api.py
+++ b/common/djangoapps/enrollment/api.py
@@ -3,7 +3,11 @@ Enrollment API for creating, updating, and deleting enrollments. Also provides a
 course level, such as available course modes.
 
 """
-from enrollment import data
+from django.utils import importlib
+import logging
+from django.conf import settings
+
+log = logging.getLogger(__name__)
 
 
 class CourseEnrollmentError(Exception):
@@ -12,8 +16,25 @@ class CourseEnrollmentError(Exception):
     Describes any error that may occur when reading or updating enrollment information for a student or a course.
 
     """
+    def __init__(self, msg, data=None):
+        super(Exception, self).__init__(msg)
+        # Corresponding information to help resolve the error.
+        self.data = data
+
+
+class CourseModeNotFoundError(CourseEnrollmentError):
+    pass
+
+
+class EnrollmentNotFoundError(CourseEnrollmentError):
+    pass
+
+
+class EnrollmentApiLoadError(CourseEnrollmentError):
     pass
 
+DEFAULT_DATA_API = 'enrollment.data'
+
 
 def get_enrollments(student_id):
     """ Retrieves all the courses a student is enrolled in.
@@ -31,36 +52,58 @@ def get_enrollments(student_id):
         >>> get_enrollments("Bob")
         [
             {
-                course_id: "edX/DemoX/2014T2",
-                is_active: True,
-                mode: "honor",
-                student: "Bob",
-                course_modes: [
-                    "audit",
-                    "honor"
-                ],
-                enrollment_start: 2014-04-07,
-                enrollment_end: 2014-06-07,
-                invite_only: False
+                "created": "2014-10-20T20:18:00Z",
+                "mode": "honor",
+                "is_active": True,
+                "student": "Bob",
+                "course": {
+                    "course_id": "edX/DemoX/2014T2",
+                    "enrollment_end": 2014-12-20T20:18:00Z,
+                    "course_modes": [
+                        {
+                            "slug": "honor",
+                            "name": "Honor Code Certificate",
+                            "min_price": 0,
+                            "suggested_prices": "",
+                            "currency": "usd",
+                            "expiration_datetime": null,
+                            "description": null
+                        }
+                    ],
+                    "enrollment_start": 2014-10-15T20:18:00Z,
+                    "invite_only": False
+                }
             },
             {
-                course_id: "edX/edX-Insider/2014T2",
-                is_active: True,
-                mode: "honor",
-                student: "Bob",
-                course_modes: [
-                    "audit",
-                    "honor",
-                    "verified"
-                ],
-                enrollment_start: 2014-05-01,
-                enrollment_end: 2014-06-01,
-                invite_only: True
-            },
+                "created": "2014-10-25T20:18:00Z",
+                "mode": "verified",
+                "is_active": True,
+                "student": "Bob",
+                "course": {
+                    "course_id": "edX/edX-Insider/2014T2",
+                    "enrollment_end": 2014-12-20T20:18:00Z,
+                    "course_modes": [
+                        {
+                            "slug": "honor",
+                            "name": "Honor Code Certificate",
+                            "min_price": 0,
+                            "suggested_prices": "",
+                            "currency": "usd",
+                            "expiration_datetime": null,
+                            "description": null
+                        }
+                    ],
+                    "enrollment_start": 2014-10-15T20:18:00Z,
+                    "invite_only": True
+                }
+            }
         ]
 
     """
-    return data.get_course_enrollments(student_id)
+    enrollments = _data_api().get_course_enrollments(student_id)
+    for enrollment in enrollments:
+        enrollment['student'] = student_id
+    return enrollments
 
 
 def get_enrollment(student_id, course_id):
@@ -76,23 +119,35 @@ def get_enrollment(student_id, course_id):
         A serializable dictionary of the course enrollment.
 
     Example:
-        >>> add_enrollment("Bob", "edX/DemoX/2014T2")
+        >>> get_enrollment("Bob", "edX/DemoX/2014T2")
         {
-            course_id: "edX/DemoX/2014T2",
-            is_active: True,
-            mode: "honor",
-            student: "Bob",
-            course_modes: [
-                "audit",
-                "honor"
-            ],
-            enrollment_start: 2014-04-07,
-            enrollment_end: 2014-06-07,
-            invite_only: False
+            "created": "2014-10-20T20:18:00Z",
+            "mode": "honor",
+            "is_active": True,
+            "student": "Bob",
+            "course": {
+                "course_id": "edX/DemoX/2014T2",
+                "enrollment_end": 2014-12-20T20:18:00Z,
+                "course_modes": [
+                    {
+                        "slug": "honor",
+                        "name": "Honor Code Certificate",
+                        "min_price": 0,
+                        "suggested_prices": "",
+                        "currency": "usd",
+                        "expiration_datetime": null,
+                        "description": null
+                    }
+                ],
+                "enrollment_start": 2014-10-15T20:18:00Z,
+                "invite_only": False
+            }
         }
 
     """
-    return data.get_course_enrollment(student_id, course_id)
+    enrollment = _data_api().get_course_enrollment(student_id, course_id)
+    enrollment['student'] = student_id
+    return enrollment
 
 
 def add_enrollment(student_id, course_id, mode='honor', is_active=True):
@@ -114,20 +169,33 @@ def add_enrollment(student_id, course_id, mode='honor', is_active=True):
     Example:
         >>> add_enrollment("Bob", "edX/DemoX/2014T2", mode="audit")
         {
-            course_id: "edX/DemoX/2014T2",
-            is_active: True,
-            mode: "audit",
-            student: "Bob",
-            course_modes: [
-                "audit",
-                "honor"
-            ],
-            enrollment_start: 2014-04-07,
-            enrollment_end: 2014-06-07,
-            invite_only: False
+            "created": "2014-10-20T20:18:00Z",
+            "mode": "honor",
+            "is_active": True,
+            "student": "Bob",
+            "course": {
+                "course_id": "edX/DemoX/2014T2",
+                "enrollment_end": 2014-12-20T20:18:00Z,
+                "course_modes": [
+                    {
+                        "slug": "honor",
+                        "name": "Honor Code Certificate",
+                        "min_price": 0,
+                        "suggested_prices": "",
+                        "currency": "usd",
+                        "expiration_datetime": null,
+                        "description": null
+                    }
+                ],
+                "enrollment_start": 2014-10-15T20:18:00Z,
+                "invite_only": False
+            }
         }
     """
-    return data.update_course_enrollment(student_id, course_id, mode=mode, is_active=is_active)
+    _validate_course_mode(course_id, mode)
+    enrollment = _data_api().update_course_enrollment(student_id, course_id, mode=mode, is_active=is_active)
+    enrollment['student'] = student_id
+    return enrollment
 
 
 def deactivate_enrollment(student_id, course_id):
@@ -146,20 +214,38 @@ def deactivate_enrollment(student_id, course_id):
     Example:
         >>> deactivate_enrollment("Bob", "edX/DemoX/2014T2")
         {
-            course_id: "edX/DemoX/2014T2",
-            mode: "honor",
-            is_active: False,
-            student: "Bob",
-            course_modes: [
-                "audit",
-                "honor"
-            ],
-            enrollment_start: 2014-04-07,
-            enrollment_end: 2014-06-07,
-            invite_only: False
+            "created": "2014-10-20T20:18:00Z",
+            "mode": "honor",
+            "is_active": False,
+            "student": "Bob",
+            "course": {
+                "course_id": "edX/DemoX/2014T2",
+                "enrollment_end": 2014-12-20T20:18:00Z,
+                "course_modes": [
+                    {
+                        "slug": "honor",
+                        "name": "Honor Code Certificate",
+                        "min_price": 0,
+                        "suggested_prices": "",
+                        "currency": "usd",
+                        "expiration_datetime": null,
+                        "description": null
+                    }
+                ],
+                "enrollment_start": 2014-10-15T20:18:00Z,
+                "invite_only": False
+            }
         }
     """
-    return data.update_course_enrollment(student_id, course_id, is_active=False)
+    # Check to see if there is an enrollment. We do not want to create a deactivated enrollment.
+    if not _data_api().get_course_enrollment(student_id, course_id):
+        raise EnrollmentNotFoundError(
+            u"No enrollment was found for student {student} in course {course}"
+            .format(student=student_id, course=course_id)
+        )
+    enrollment = _data_api().update_course_enrollment(student_id, course_id, is_active=False)
+    enrollment['student'] = student_id
+    return enrollment
 
 
 def update_enrollment(student_id, course_id, mode):
@@ -178,21 +264,34 @@ def update_enrollment(student_id, course_id, mode):
     Example:
         >>> update_enrollment("Bob", "edX/DemoX/2014T2", "honor")
         {
-            course_id: "edX/DemoX/2014T2",
-            mode: "honor",
-            is_active: True,
-            student: "Bob",
-            course_modes: [
-                "audit",
-                "honor"
-            ],
-            enrollment_start: 2014-04-07,
-            enrollment_end: 2014-06-07,
-            invite_only: False
+            "created": "2014-10-20T20:18:00Z",
+            "mode": "honor",
+            "is_active": True,
+            "student": "Bob",
+            "course": {
+                "course_id": "edX/DemoX/2014T2",
+                "enrollment_end": 2014-12-20T20:18:00Z,
+                "course_modes": [
+                    {
+                        "slug": "honor",
+                        "name": "Honor Code Certificate",
+                        "min_price": 0,
+                        "suggested_prices": "",
+                        "currency": "usd",
+                        "expiration_datetime": null,
+                        "description": null
+                    }
+                ],
+                "enrollment_start": 2014-10-15T20:18:00Z,
+                "invite_only": False
+            }
         }
 
     """
-    return data.update_course_enrollment(student_id, course_id, mode)
+    _validate_course_mode(course_id, mode)
+    enrollment = _data_api().update_course_enrollment(student_id, course_id, mode)
+    enrollment['student'] = student_id
+    return enrollment
 
 
 def get_course_enrollment_details(course_id):
@@ -209,15 +308,67 @@ def get_course_enrollment_details(course_id):
     Example:
         >>> get_course_enrollment_details("edX/DemoX/2014T2")
         {
-            course_id: "edX/DemoX/2014T2",
-            course_modes: [
-                "audit",
-                "honor"
+            "course_id": "edX/DemoX/2014T2",
+            "enrollment_end": 2014-12-20T20:18:00Z,
+            "course_modes": [
+                {
+                    "slug": "honor",
+                    "name": "Honor Code Certificate",
+                    "min_price": 0,
+                    "suggested_prices": "",
+                    "currency": "usd",
+                    "expiration_datetime": null,
+                    "description": null
+                }
             ],
-            enrollment_start: 2014-04-01,
-            enrollment_end: 2014-06-01,
-            invite_only: False
+            "enrollment_start": 2014-10-15T20:18:00Z,
+            "invite_only": False
         }
 
     """
-    pass
+    return _data_api().get_course_enrollment_info(course_id)
+
+
+def _validate_course_mode(course_id, mode):
+    """Checks to see if the specified course mode is valid for the course.
+
+    If the requested course mode is not available for the course, raise an error with corresponding
+    course enrollment information.
+
+    'honor' is special cased. If there are no course modes configured, and the specified mode is
+    'honor', return true, allowing the enrollment to be 'honor' even if the mode is not explicitly
+    set for the course.
+
+    Args:
+        course_id (str): The course to check against for available course modes.
+        mode (str): The slug for the course mode specified in the enrollment.
+
+    Returns:
+        None
+
+    Raises:
+        CourseModeNotFound: raised if the course mode is not found.
+    """
+    course_enrollment_info = _data_api().get_course_enrollment_info(course_id)
+    course_modes = course_enrollment_info["course_modes"]
+    if mode not in (mode['slug'] for mode in course_modes):
+        msg = u"Specified course mode unavailable for course {course_id}".format(course_id=course_id)
+        log.warn(msg)
+        error = CourseModeNotFoundError(msg, course_enrollment_info)
+        raise error
+
+
+def _data_api():
+    """Returns a Data API.
+    This relies on Django settings to find the appropriate data API.
+
+    """
+    # We retrieve the settings in-line here (rather than using the
+    # top-level constant), so that @override_settings will work
+    # in the test suite.
+    api_path = getattr(settings, "ENROLLMENT_DATA_API", DEFAULT_DATA_API)
+
+    try:
+        return importlib.import_module(api_path)
+    except (ImportError, ValueError):
+        raise EnrollmentApiLoadError(api_path)
diff --git a/common/djangoapps/enrollment/data.py b/common/djangoapps/enrollment/data.py
index b6686f6731a..f72c5fcb016 100644
--- a/common/djangoapps/enrollment/data.py
+++ b/common/djangoapps/enrollment/data.py
@@ -3,13 +3,29 @@ Data Aggregation Layer of the Enrollment API. Collects all enrollment specific d
 source to be used throughout the API.
 
 """
+import logging
 from django.contrib.auth.models import User
 from opaque_keys.edx.keys import CourseKey
-from enrollment.serializers import CourseEnrollmentSerializer
-from student.models import CourseEnrollment
+from xmodule.modulestore.django import modulestore
+from xmodule.modulestore.exceptions import ItemNotFoundError
+from enrollment.serializers import CourseEnrollmentSerializer, CourseField
+from student.models import CourseEnrollment, NonExistentCourseError
+
+log = logging.getLogger(__name__)
 
 
 def get_course_enrollments(student_id):
+    """Retrieve a list representing all aggregated data for a student's course enrollments.
+
+    Construct a representation of all course enrollment data for a specific student..
+
+    Args:
+        student_id (str): The name of the student to retrieve course enrollment information for.
+
+    Returns:
+        A serializable list of dictionaries of all aggregated enrollment data for a student.
+
+    """
     qset = CourseEnrollment.objects.filter(
         user__username=student_id, is_active=True
     ).order_by('created')
@@ -17,6 +33,18 @@ def get_course_enrollments(student_id):
 
 
 def get_course_enrollment(student_id, course_id):
+    """Retrieve an object representing all aggregated data for a student's course enrollment.
+
+    Get the course enrollment information for a specific student and course.
+
+    Args:
+        student_id (str): The name of the student to retrieve course enrollment information for.
+        course_id (str): The course to retrieve course enrollment information for.
+
+    Returns:
+        A serializable dictionary representing the course enrollment.
+
+    """
     course_key = CourseKey.from_string(course_id)
     try:
         enrollment = CourseEnrollment.objects.get(
@@ -28,6 +56,20 @@ def get_course_enrollment(student_id, course_id):
 
 
 def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
+    """Modify a course enrollment for a student.
+
+    Allows updates to a specific course enrollment.
+
+    Args:
+        student_id (str): The name of the student to retrieve course enrollment information for.
+        course_id (str): The course to retrieve course enrollment information for.
+        mode (str): (Optional) The mode for the new enrollment.
+        is_active (boolean): (Optional) Determines if the enrollment is active.
+
+    Returns:
+        A serializable dictionary representing the modified course enrollment.
+
+    """
     course_key = CourseKey.from_string(course_id)
     student = User.objects.get(username=student_id)
     if not CourseEnrollment.is_enrolled(student, course_key):
@@ -41,8 +83,23 @@ def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
 
 
 def get_course_enrollment_info(course_id):
-    pass
+    """Returns all course enrollment information for the given course.
 
+    Based on the course id, return all related course information..
 
-def get_course_enrollments_info(student_id):
-    pass
+    Args:
+        course_id (str): The course to retrieve enrollment information for.
+
+    Returns:
+        A serializable dictionary representing the course's enrollment information.
+
+    """
+    course_key = CourseKey.from_string(course_id)
+    course = modulestore().get_course(course_key)
+    if course is None:
+        log.warning(
+            u"Requested enrollment information for unknown course {course}"
+            .format(course=course_id)
+        )
+        raise NonExistentCourseError
+    return CourseField().to_native(course)
diff --git a/common/djangoapps/enrollment/serializers.py b/common/djangoapps/enrollment/serializers.py
index 324c03b0582..d01deb0023d 100644
--- a/common/djangoapps/enrollment/serializers.py
+++ b/common/djangoapps/enrollment/serializers.py
@@ -3,12 +3,36 @@ Serializers for all Course Enrollment related return objects.
 
 """
 from rest_framework import serializers
+from rest_framework.fields import Field
 from student.models import CourseEnrollment
 from course_modes.models import CourseMode
 
 
+class StringListField(serializers.CharField):
+    """Custom Serializer for turning a comma delimited string into a list.
+
+    This field is designed to take a string such as "1,2,3" and turn it into an actual list
+    [1,2,3]
+
+    """
+    def field_to_native(self, obj, field_name):
+        """
+        Serialize the object's class name.
+        """
+        if not obj.suggested_prices:
+            return []
+
+        items = obj.suggested_prices.split(',')
+        return [int(item) for item in items]
+
+
 class CourseField(serializers.RelatedField):
-    """Custom field to wrap a CourseDescriptor object. Read-only."""
+    """Read-Only representation of course enrollment information.
+
+    Aggregates course information from the CourseDescriptor as well as the Course Modes configured
+    for enrolling in the course.
+
+    """
 
     def to_native(self, course):
         course_id = unicode(course.id)
@@ -24,8 +48,10 @@ class CourseField(serializers.RelatedField):
 
 
 class CourseEnrollmentSerializer(serializers.ModelSerializer):
-    """
-    Serializes CourseEnrollment models
+    """Serializes CourseEnrollment models
+
+    Aggregates all data from the Course Enrollment table, and pulls in the serialization for
+    the Course Descriptor and course modes, to give a complete representation of course enrollment.
 
     """
     course = CourseField()
@@ -37,11 +63,17 @@ class CourseEnrollmentSerializer(serializers.ModelSerializer):
 
 
 class ModeSerializer(serializers.Serializer):
-    """Serializes a course's 'Mode' tuples"""
+    """Serializes a course's 'Mode' tuples
+
+    Returns a serialized representation of the modes available for course enrollment. The course
+    modes models are designed to return a tuple instead of the model object itself. This serializer
+    does not handle the model object itself, but the tuple.
+
+    """
     slug = serializers.CharField(max_length=100)
     name = serializers.CharField(max_length=255)
     min_price = serializers.IntegerField()
-    suggested_prices = serializers.CharField(max_length=255)
+    suggested_prices = StringListField(max_length=255)
     currency = serializers.CharField(max_length=8)
     expiration_datetime = serializers.DateTimeField()
     description = serializers.CharField()
diff --git a/common/djangoapps/enrollment/tests/fake_data_api.py b/common/djangoapps/enrollment/tests/fake_data_api.py
new file mode 100644
index 00000000000..d1374c3fefb
--- /dev/null
+++ b/common/djangoapps/enrollment/tests/fake_data_api.py
@@ -0,0 +1,96 @@
+"""
+A Fake Data API for testing purposes.
+"""
+import copy
+import datetime
+
+
+_DEFAULT_FAKE_MODE = {
+    "slug": "honor",
+    "name": "Honor Code Certificate",
+    "min_price": 0,
+    "suggested_prices": "",
+    "currency": "usd",
+    "expiration_datetime": None,
+    "description": None
+}
+
+_ENROLLMENTS = []
+
+_COURSES = []
+
+
+def get_course_enrollments(student_id):
+    """Stubbed out Enrollment data request."""
+    return _ENROLLMENTS
+
+
+def get_course_enrollment(student_id, course_id):
+    """Stubbed out Enrollment data request."""
+    return _get_fake_enrollment(student_id, course_id)
+
+
+def update_course_enrollment(student_id, course_id, mode=None, is_active=None):
+    """Stubbed out Enrollment data request."""
+    enrollment = _get_fake_enrollment(student_id, course_id)
+    if not enrollment:
+        enrollment = add_enrollment(student_id, course_id)
+    if mode is not None:
+        enrollment['mode'] = mode
+    if is_active is not None:
+        enrollment['is_active'] = is_active
+    return enrollment
+
+
+def get_course_enrollment_info(course_id):
+    """Stubbed out Enrollment data request."""
+    return _get_fake_course_info(course_id)
+
+
+def _get_fake_enrollment(student_id, course_id):
+    for enrollment in _ENROLLMENTS:
+        if student_id == enrollment['student'] and course_id == enrollment['course']['course_id']:
+            return enrollment
+
+
+def _get_fake_course_info(course_id):
+    for course in _COURSES:
+        if course_id == course['course_id']:
+            return course
+
+
+def add_enrollment(student_id, course_id, is_active=True, mode='honor'):
+    enrollment = {
+        "created": datetime.datetime.now(),
+        "mode": mode,
+        "is_active": is_active,
+        "course": _get_fake_course_info(course_id),
+        "student": student_id
+    }
+    _ENROLLMENTS.append(enrollment)
+    return enrollment
+
+
+def add_course(course_id, enrollment_start=None, enrollment_end=None, invite_only=False, course_modes=None):
+    course_info = {
+        "course_id": course_id,
+        "enrollment_end": enrollment_end,
+        "course_modes": [],
+        "enrollment_start": enrollment_start,
+        "invite_only": invite_only,
+    }
+    if not course_modes:
+        course_info['course_modes'].append(_DEFAULT_FAKE_MODE)
+    else:
+        for mode in course_modes:
+            new_mode = copy.deepcopy(_DEFAULT_FAKE_MODE)
+            new_mode['slug'] = mode
+            course_info['course_modes'].append(new_mode)
+    _COURSES.append(course_info)
+
+
+def reset():
+    global _COURSES
+    _COURSES = []
+    global _ENROLLMENTS
+    _ENROLLMENTS = []
diff --git a/common/djangoapps/enrollment/tests/test_api.py b/common/djangoapps/enrollment/tests/test_api.py
new file mode 100644
index 00000000000..fe996bd6dc2
--- /dev/null
+++ b/common/djangoapps/enrollment/tests/test_api.py
@@ -0,0 +1,151 @@
+"""
+Tests for student enrollment.
+"""
+import ddt
+from nose.tools import raises
+import unittest
+from django.test import TestCase
+from django.test.utils import override_settings
+from django.conf import settings
+from enrollment import api
+from enrollment.tests import fake_data_api
+
+
+@ddt.ddt
+@override_settings(ENROLLMENT_DATA_API="enrollment.tests.fake_data_api")
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class EnrollmentTest(TestCase):
+    """
+    Test student enrollment, especially with different course modes.
+    """
+    USERNAME = "Bob"
+    COURSE_ID = "some/great/course"
+
+    def setUp(self):
+        fake_data_api.reset()
+
+    @ddt.data(
+        # Default (no course modes in the database)
+        # Expect automatically being enrolled as "honor".
+        ([], 'honor'),
+
+        # Audit / Verified / Honor
+        # We should always go to the "choose your course" page.
+        # We should also be enrolled as "honor" by default.
+        (['honor', 'verified', 'audit'], 'honor'),
+
+        # Check for professional ed happy path.
+        (['professional'], 'professional')
+    )
+    @ddt.unpack
+    def test_enroll(self, course_modes, mode):
+        # Add a fake course enrollment information to the fake data API
+        fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
+        # Enroll in the course and verify the URL we get sent to
+        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode=mode)
+        self.assertIsNotNone(result)
+        self.assertEquals(result['student'], self.USERNAME)
+        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
+        self.assertEquals(result['mode'], mode)
+
+        get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
+        self.assertEquals(result, get_result)
+
+    @raises(api.CourseModeNotFoundError)
+    def test_prof_ed_enroll(self):
+        # Add a fake course enrollment information to the fake data API
+        fake_data_api.add_course(self.COURSE_ID, course_modes=['professional'])
+        # Enroll in the course and verify the URL we get sent to
+        api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='verified')
+
+    @ddt.data(
+        # Default (no course modes in the database)
+        # Expect that users are automatically enrolled as "honor".
+        ([], 'honor'),
+
+        # Audit / Verified / Honor
+        # We should always go to the "choose your course" page.
+        # We should also be enrolled as "honor" by default.
+        (['honor', 'verified', 'audit'], 'honor'),
+
+        # Check for professional ed happy path.
+        (['professional'], 'professional')
+    )
+    @ddt.unpack
+    def test_unenroll(self, course_modes, mode):
+        # Add a fake course enrollment information to the fake data API
+        fake_data_api.add_course(self.COURSE_ID, course_modes=course_modes)
+        # Enroll in the course and verify the URL we get sent to
+        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode=mode)
+        self.assertIsNotNone(result)
+        self.assertEquals(result['student'], self.USERNAME)
+        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
+        self.assertEquals(result['mode'], mode)
+        self.assertTrue(result['is_active'])
+
+        result = api.deactivate_enrollment(self.USERNAME, self.COURSE_ID)
+        self.assertIsNotNone(result)
+        self.assertEquals(result['student'], self.USERNAME)
+        self.assertEquals(result['course']['course_id'], self.COURSE_ID)
+        self.assertEquals(result['mode'], mode)
+        self.assertFalse(result['is_active'])
+
+    @raises(api.EnrollmentNotFoundError)
+    def test_unenroll_not_enrolled_in_course(self):
+        # Add a fake course enrollment information to the fake data API
+        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor'])
+        api.deactivate_enrollment(self.USERNAME, self.COURSE_ID)
+
+    @ddt.data(
+        # Simple test of honor and verified.
+        ([
+            {'course_id': 'the/first/course', 'course_modes': [], 'mode': 'honor'},
+            {'course_id': 'the/second/course', 'course_modes': ['honor', 'verified'], 'mode': 'verified'}
+        ]),
+
+        # No enrollments
+        ([]),
+
+        # One Enrollment
+        ([
+            {'course_id': 'the/third/course', 'course_modes': ['honor', 'verified', 'audit'], 'mode': 'audit'}
+        ]),
+    )
+    def test_get_all_enrollments(self, enrollments):
+        for enrollment in enrollments:
+            fake_data_api.add_course(enrollment['course_id'], course_modes=enrollment['course_modes'])
+            api.add_enrollment(self.USERNAME, enrollment['course_id'], enrollment['mode'])
+        result = api.get_enrollments(self.USERNAME)
+        self.assertEqual(len(enrollments), len(result))
+        for result_enrollment in result:
+            self.assertIn(
+                result_enrollment['course']['course_id'],
+                [enrollment['course_id'] for enrollment in enrollments]
+            )
+
+    def test_update_enrollment(self):
+        # Add a fake course enrollment information to the fake data API
+        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
+        # Enroll in the course and verify the URL we get sent to
+        result = api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
+        get_result = api.get_enrollment(self.USERNAME, self.COURSE_ID)
+        self.assertEquals(result, get_result)
+
+        result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='honor')
+        self.assertEquals('honor', result['mode'])
+
+        result = api.update_enrollment(self.USERNAME, self.COURSE_ID, mode='verified')
+        self.assertEquals('verified', result['mode'])
+
+    def test_get_course_details(self):
+        # Add a fake course enrollment information to the fake data API
+        fake_data_api.add_course(self.COURSE_ID, course_modes=['honor', 'verified', 'audit'])
+        result = api.get_course_enrollment_details(self.COURSE_ID)
+        self.assertEquals(result['course_id'], self.COURSE_ID)
+        self.assertEquals(3, len(result['course_modes']))
+
+    @override_settings(ENROLLMENT_DATA_API='foo.bar.biz.baz')
+    @raises(api.EnrollmentApiLoadError)
+    def test_data_api_config_error(self):
+        # Enroll in the course and verify the URL we get sent to
+        api.add_enrollment(self.USERNAME, self.COURSE_ID, mode='audit')
diff --git a/common/djangoapps/enrollment/tests/test_data.py b/common/djangoapps/enrollment/tests/test_data.py
new file mode 100644
index 00000000000..9655ecb00da
--- /dev/null
+++ b/common/djangoapps/enrollment/tests/test_data.py
@@ -0,0 +1,172 @@
+"""
+Test the Data Aggregation Layer for Course Enrollments.
+
+"""
+import ddt
+from nose.tools import raises
+import unittest
+
+from django.test.utils import override_settings
+from django.conf import settings
+from xmodule.modulestore.tests.django_utils import (
+    ModuleStoreTestCase, mixed_store_config
+)
+from xmodule.modulestore.tests.factories import CourseFactory
+from student.tests.factories import UserFactory, CourseModeFactory
+from student.models import CourseEnrollment, NonExistentCourseError
+from enrollment import data
+
+# Since we don't need any XML course fixtures, use a modulestore configuration
+# that disables the XML modulestore.
+MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
+
+
+@ddt.ddt
+@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class EnrollmentDataTest(ModuleStoreTestCase):
+    """
+    Test course enrollment data aggregation.
+
+    """
+    USERNAME = "Bob"
+    EMAIL = "bob@example.com"
+    PASSWORD = "edx"
+
+    def setUp(self):
+        """ Create a course and user, then log in. """
+        super(EnrollmentDataTest, self).setUp()
+        self.course = CourseFactory.create()
+        self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
+        self.client.login(username=self.USERNAME, password=self.PASSWORD)
+
+    @ddt.data(
+        # Default (no course modes in the database)
+        # Expect that users are automatically enrolled as "honor".
+        ([], 'honor'),
+
+        # Audit / Verified / Honor
+        # We should always go to the "choose your course" page.
+        # We should also be enrolled as "honor" by default.
+        (['honor', 'verified', 'audit'], 'honor'),
+    )
+    @ddt.unpack
+    def test_enroll(self, course_modes, enrollment_mode):
+        # Create the course modes (if any) required for this test case
+        self._create_course_modes(course_modes)
+
+        enrollment = data.update_course_enrollment(
+            self.user.username,
+            unicode(self.course.id),
+            mode=enrollment_mode,
+            is_active=True
+        )
+
+        self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
+        course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
+        self.assertTrue(is_active)
+        self.assertEqual(course_mode, enrollment_mode)
+
+        # Confirm the returned enrollment and the data match up.
+        self.assertEqual(course_mode, enrollment['mode'])
+        self.assertEqual(is_active, enrollment['is_active'])
+
+    def test_unenroll(self):
+        # Enroll the student in the course
+        CourseEnrollment.enroll(self.user, self.course.id, mode="honor")
+
+        enrollment = data.update_course_enrollment(
+            self.user.username,
+            unicode(self.course.id),
+            is_active=False
+        )
+
+        # Determine that the returned enrollment is inactive.
+        self.assertFalse(enrollment['is_active'])
+
+        # Expect that we're no longer enrolled
+        self.assertFalse(CourseEnrollment.is_enrolled(self.user, self.course.id))
+
+    @ddt.data(
+        # No course modes, no course enrollments.
+        ([]),
+
+        # Audit / Verified / Honor course modes, with three course enrollments.
+        (['honor', 'verified', 'audit']),
+    )
+    def test_get_course_info(self, course_modes):
+        self._create_course_modes(course_modes, course=self.course)
+        result_course = data.get_course_enrollment_info(unicode(self.course.id))
+        result_slugs = [mode['slug'] for mode in result_course['course_modes']]
+        for course_mode in course_modes:
+            self.assertIn(course_mode, result_slugs)
+
+    @ddt.data(
+        # No course modes, no course enrollments.
+        ([], []),
+
+        # Audit / Verified / Honor course modes, with three course enrollments.
+        (['honor', 'verified', 'audit'], ['1', '2', '3']),
+    )
+    @ddt.unpack
+    def test_get_course_enrollments(self, course_modes, course_numbers):
+        # Create all the courses
+        created_courses = []
+        for course_number in course_numbers:
+            created_courses.append(CourseFactory.create(number=course_number))
+
+        created_enrollments = []
+        for course in created_courses:
+            self._create_course_modes(course_modes, course=course)
+            # Create the original enrollment.
+            created_enrollments.append(data.update_course_enrollment(
+                self.user.username,
+                unicode(course.id),
+            ))
+
+        # Compare the created enrollments with the results
+        # from the get enrollments request.
+        results = data.get_course_enrollments(self.user.username)
+        self.assertEqual(results, created_enrollments)
+
+    @ddt.data(
+        # Default (no course modes in the database)
+        # Expect that users are automatically enrolled as "honor".
+        ([], 'honor'),
+
+        # Audit / Verified / Honor
+        # We should always go to the "choose your course" page.
+        # We should also be enrolled as "honor" by default.
+        (['honor', 'verified', 'audit'], 'verified'),
+    )
+    @ddt.unpack
+    def test_get_course_enrollment(self, course_modes, enrollment_mode):
+        self._create_course_modes(course_modes)
+
+        # Try to get an enrollment before it exists.
+        result = data.get_course_enrollment(self.user.username, unicode(self.course.id))
+        self.assertIsNone(result)
+
+        # Create the original enrollment.
+        enrollment = data.update_course_enrollment(
+            self.user.username,
+            unicode(self.course.id),
+            mode=enrollment_mode,
+            is_active=True
+        )
+        # Get the enrollment and compare it to the original.
+        result = data.get_course_enrollment(self.user.username, unicode(self.course.id))
+        self.assertEqual(enrollment, result)
+
+    @raises(NonExistentCourseError)
+    def test_non_existent_course(self):
+        data.get_course_enrollment_info("this/is/bananas")
+
+    def _create_course_modes(self, course_modes, course=None):
+        course_id = course.id if course else self.course.id
+        for mode_slug in course_modes:
+            CourseModeFactory.create(
+                course_id=course_id,
+                mode_slug=mode_slug,
+                mode_display_name=mode_slug,
+            )
diff --git a/common/djangoapps/enrollment/tests/test_views.py b/common/djangoapps/enrollment/tests/test_views.py
new file mode 100644
index 00000000000..5ed71806376
--- /dev/null
+++ b/common/djangoapps/enrollment/tests/test_views.py
@@ -0,0 +1,152 @@
+"""
+Tests for student enrollment.
+"""
+import ddt
+import json
+import unittest
+
+from django.test.utils import override_settings
+from django.core.urlresolvers import reverse
+from rest_framework.test import APITestCase
+from rest_framework import status
+from django.conf import settings
+from xmodule.modulestore.tests.django_utils import (
+    ModuleStoreTestCase, mixed_store_config
+)
+from xmodule.modulestore.tests.factories import CourseFactory
+from student.tests.factories import UserFactory, CourseModeFactory
+from student.models import CourseEnrollment
+
+# Since we don't need any XML course fixtures, use a modulestore configuration
+# that disables the XML modulestore.
+MODULESTORE_CONFIG = mixed_store_config(settings.COMMON_TEST_DATA_ROOT, {}, include_xml=False)
+
+
+@ddt.ddt
+@override_settings(MODULESTORE=MODULESTORE_CONFIG)
+@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
+class EnrollmentTest(ModuleStoreTestCase, APITestCase):
+    """
+    Test student enrollment, especially with different course modes.
+    """
+    USERNAME = "Bob"
+    EMAIL = "bob@example.com"
+    PASSWORD = "edx"
+
+    def setUp(self):
+        """ Create a course and user, then log in. """
+        super(EnrollmentTest, self).setUp()
+        self.course = CourseFactory.create()
+        self.user = UserFactory.create(username=self.USERNAME, email=self.EMAIL, password=self.PASSWORD)
+        self.client.login(username=self.USERNAME, password=self.PASSWORD)
+
+    @ddt.data(
+        # Default (no course modes in the database)
+        # Expect that users are automatically enrolled as "honor".
+        ([], 'honor'),
+
+        # Audit / Verified / Honor
+        # We should always go to the "choose your course" page.
+        # We should also be enrolled as "honor" by default.
+        (['honor', 'verified', 'audit'], 'honor'),
+    )
+    @ddt.unpack
+    def test_enroll(self, course_modes, enrollment_mode):
+        # Create the course modes (if any) required for this test case
+        for mode_slug in course_modes:
+            CourseModeFactory.create(
+                course_id=self.course.id,
+                mode_slug=mode_slug,
+                mode_display_name=mode_slug,
+            )
+
+        # Enroll in the course and verify the URL we get sent to
+        self._create_enrollment()
+
+        self.assertTrue(CourseEnrollment.is_enrolled(self.user, self.course.id))
+        course_mode, is_active = CourseEnrollment.enrollment_mode_for_user(self.user, self.course.id)
+        self.assertTrue(is_active)
+        self.assertEqual(course_mode, enrollment_mode)
+
+    def test_enroll_prof_ed(self):
+        # Create the prod ed mode.
+        CourseModeFactory.create(
+            course_id=self.course.id,
+            mode_slug='professional',
+            mode_display_name='Professional Education',
+        )
+
+        # Enroll in the course, this will fail if the mode is not explicitly professional.
+        resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))}))
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+        # While the enrollment wrong is invalid, the response content should have
+        # all the valid enrollment modes.
+        data = json.loads(resp.content)
+        self.assertEqual(unicode(self.course.id), data['course_id'])
+        self.assertEqual(1, len(data['course_modes']))
+        self.assertEqual('professional', data['course_modes'][0]['slug'])
+
+    def test_unenroll(self):
+        # Create a course mode.
+        CourseModeFactory.create(
+            course_id=self.course.id,
+            mode_slug='honor',
+            mode_display_name='Honor',
+        )
+
+        # Create an enrollment
+        resp = self._create_enrollment()
+
+        # Deactivate the enrollment in the course and verify the URL we get sent to
+        resp = self.client.post(reverse(
+            'courseenrollment',
+            kwargs={'course_id': (unicode(self.course.id))}
+        ), {'deactivate': True})
+        self.assertEqual(resp.status_code, status.HTTP_200_OK)
+        data = json.loads(resp.content)
+        self.assertEqual(unicode(self.course.id), data['course']['course_id'])
+        self.assertEqual('honor', data['mode'])
+        self.assertFalse(data['is_active'])
+
+    def test_user_not_authenticated(self):
+        # Log out, so we're no longer authenticated
+        self.client.logout()
+
+        # Try to enroll, this should fail.
+        resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))}))
+        self.assertEqual(resp.status_code, status.HTTP_401_UNAUTHORIZED)
+
+    def test_unenroll_not_enrolled_in_course(self):
+        # Deactivate the enrollment in the course and verify the URL we get sent to
+        resp = self.client.post(reverse(
+            'courseenrollment',
+            kwargs={'course_id': (unicode(self.course.id))}
+        ), {'deactivate': True})
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def test_invalid_enrollment_mode(self):
+        # Request an enrollment with verified mode, which does not exist for this course.
+        resp = self.client.post(reverse(
+            'courseenrollment',
+            kwargs={'course_id': (unicode(self.course.id))}),
+            {'mode': 'verified'}
+        )
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+        data = json.loads(resp.content)
+        self.assertEqual(unicode(self.course.id), data['course_id'])
+        self.assertEqual('honor', data['course_modes'][0]['slug'])
+
+    def test_with_invalid_course_id(self):
+        # Create an enrollment
+        resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': 'entirely/fake/course'}))
+        self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
+
+    def _create_enrollment(self):
+        resp = self.client.post(reverse('courseenrollment', kwargs={'course_id': (unicode(self.course.id))}))
+        self.assertEqual(resp.status_code, status.HTTP_200_OK)
+        data = json.loads(resp.content)
+        self.assertEqual(unicode(self.course.id), data['course']['course_id'])
+        self.assertEqual('honor', data['mode'])
+        self.assertTrue(data['is_active'])
+        return resp
diff --git a/common/djangoapps/enrollment/views.py b/common/djangoapps/enrollment/views.py
index 76a2b34e903..9546403c270 100644
--- a/common/djangoapps/enrollment/views.py
+++ b/common/djangoapps/enrollment/views.py
@@ -3,12 +3,14 @@ The Enrollment API Views should be simple, lean HTTP endpoints for API access. T
 consist primarily of authentication, request validation, and serialization.
 
 """
+from rest_framework import status
 from rest_framework.authentication import OAuth2Authentication, SessionAuthentication
 from rest_framework.decorators import api_view, authentication_classes, permission_classes, throttle_classes
 from rest_framework.permissions import IsAuthenticated
 from rest_framework.response import Response
 from rest_framework.throttling import UserRateThrottle
 from enrollment import api
+from student.models import NonExistentCourseError
 
 
 class EnrollmentUserThrottle(UserRateThrottle):
@@ -20,6 +22,17 @@ class EnrollmentUserThrottle(UserRateThrottle):
 @permission_classes((IsAuthenticated,))
 @throttle_classes([EnrollmentUserThrottle])
 def list_student_enrollments(request):
+    """List out all the enrollments for the current student
+
+    Returns a JSON response with all the course enrollments for the current student.
+
+    Args:
+        request (Request): The GET request for course enrollment listings.
+
+    Returns:
+        A JSON serialized representation of the student's course enrollments.
+
+    """
     return Response(api.get_enrollments(request.user.username))
 
 
@@ -28,11 +41,36 @@ def list_student_enrollments(request):
 @permission_classes((IsAuthenticated,))
 @throttle_classes([EnrollmentUserThrottle])
 def get_course_enrollment(request, course_id=None):
-    if 'mode' in request.DATA:
-        return Response(api.update_enrollment(request.user.username, course_id, request.DATA['mode']))
-    elif 'deactivate' in request.DATA:
-        return Response(api.deactivate_enrollment(request.user.username, course_id))
-    elif course_id and request.method == 'POST':
-        return Response(api.add_enrollment(request.user.username, course_id))
-    else:
-        return Response(api.get_enrollment(request.user.username, course_id))
+    """Create, read, or update enrollment information for a student.
+
+    HTTP Endpoint for all CRUD operations for a student course enrollment. Allows creation, reading, and
+    updates of the current enrollment for a particular course.
+
+    Args:
+        request (Request): To get current course enrollment information, a GET request will return
+            information for the current user and the specified course. A POST request will create a
+            new course enrollment for the current user. If 'mode' or 'deactivate' are found in the
+            POST parameters, the mode can be modified, or the enrollment can be deactivated.
+        course_id (str): URI element specifying the course location. Enrollment information will be
+            returned, created, or updated for this particular course.
+
+    Return:
+        A JSON serialized representation of the course enrollment. If this is a new or modified enrollment,
+        the returned enrollment will reflect all changes.
+
+    """
+    try:
+        if 'mode' in request.DATA:
+            return Response(api.update_enrollment(request.user.username, course_id, request.DATA['mode']))
+        elif 'deactivate' in request.DATA:
+            return Response(api.deactivate_enrollment(request.user.username, course_id))
+        elif course_id and request.method == 'POST':
+            return Response(api.add_enrollment(request.user.username, course_id))
+        else:
+            return Response(api.get_enrollment(request.user.username, course_id))
+    except api.CourseModeNotFoundError as error:
+        return Response(status=status.HTTP_400_BAD_REQUEST, data=error.data)
+    except NonExistentCourseError:
+        return Response(status=status.HTTP_400_BAD_REQUEST)
+    except api.EnrollmentNotFoundError:
+        return Response(status=status.HTTP_400_BAD_REQUEST)
-- 
GitLab