Repository: box/box-python-sdk
Pull request #328 (2018-08-21 17:44:41)

Retention Policies v2

boxcla: Hi @carycheng, thanks for the pull request. Before we can merge it, we need you to sign our Contributor License Agreement. You can do so electronically here: http://opensource.box.com/cla

Once you have signed, just add a comment to this pull request saying, "CLA signed". Thanks!

Patch:
diff --git a/boxsdk/client/client.py b/boxsdk/client/client.py
index e511e50..36f9c46 100644
--- a/boxsdk/client/client.py
+++ b/boxsdk/client/client.py
@@ -1,5 +1,5 @@
 # coding: utf-8
-
+# pylint: disable=too-many-lines
 from __future__ import unicode_literals, absolute_import
 import json
 
@@ -585,6 +585,253 @@ def create_group(
             response_object=response,
         )
 
+    def retention_policy(self, retention_id):
+        """
+        Initialize a :class:`RetentionPolicy` object, whose box id is retention_id.
+
+        :param retention_id:
+            The box ID of the :class:`RetentionPolicy` object.
+        :type retention_id:
+            `unicode`
+        :return:
+            A :class:`RetentionPolicy` object with the given entry ID.
+        :rtype:
+            :class:`RetentionPolicy`
+        """
+        return self.translator.translate('retention_policy')(session=self._session, object_id=retention_id)
+
+    def file_version_retention(self, retention_id):
+        """
+        Initialize a :class:`FileVersionRetention` object, whose box id is retention_id.
+
+        :param retention_id:
+            The box ID of the :class:`FileVersionRetention` object.
+        :type retention_id:
+            `unicode`
+        :return:
+            A :class:`FileVersionRetention` object with the given retention ID.
+        :rtype:
+            :class:`FileVersionRetention`
+        """
+        return self.translator.translate('file_version_retention')(session=self._session, object_id=retention_id)
+
+    def retention_policy_assignment(self, assignment_id):
+        """
+        Initialize a :class:`RetentionPolicyAssignment` object, whose box id is assignment_id.
+
+        :param assignment_id:
+            The box ID of the :class:`RetentionPolicyAssignment` object.
+        :type assignment_id:
+            `unicode`
+        :return:
+            A :class:`RetentionPolicyAssignment` object with the given assignment ID.
+        :rtype:
+            :class:`RetentionPolicyAssignment`
+        """
+        return self.translator.translate('retention_policy_assignment')(session=self._session, object_id=assignment_id)
+
+    def create_retention_policy(
+            self,
+            policy_name,
+            disposition_action,
+            retention_length,
+            can_owner_extend_retention=None,
+            are_owners_notified=None,
+            custom_notification_recipients=None,
+    ):
+        """
+        Create a retention policy for the given enterprise.
+
+        :param policy_name:
+            The name of the retention policy.
+        :type policy_name:
+            `unicode`
+        :param retention_length:
+            The amount of time in days to apply the retention policy to the selected content.
+            The retention_length should be set to float('inf') for indefinite policies.
+        :type retention_length:
+            `int` or float('inf')
+        :param disposition_action:
+            For `finite` policy can be set to `permanently delete` or `remove retention`.
+            For `indefinite` policy this must be set to `remove_retention`
+        :type disposition_action:
+            `unicode`
+        :param can_owner_extend_retention:
+            The owner of a file will be allowed to extend the retention if set to true.
+        :type can_owner_extend_retention:
+            `boolean` or None
+        :param are_owners_notified:
+            The owner or co-owner will get notified when a file is nearing expiration.
+        :type are_owners_notified:
+            `boolean` or None
+        :param custom_notification_recipients:
+            A custom list of user mini objects that should be notified when a file is nearing expiration.
+        :type custom_notification_recipients:
+            `list` of :class:`User` objects
+        :return:
+            The newly created Retention Policy
+        :rtype:
+            :class:`RetentionPolicy`
+        """
+        url = self.get_url('retention_policies')
+        user_list = []
+        retention_attributes = {
+            'policy_name': policy_name,
+            'disposition_action': disposition_action,
+        }
+        if retention_length == float('inf'):
+            retention_attributes['policy_type'] = 'indefinite'
+        else:
+            retention_attributes['policy_type'] = 'finite'
+            retention_attributes['retention_length'] = retention_length
+        if can_owner_extend_retention is not None:
+            retention_attributes['can_owner_extend_retention'] = can_owner_extend_retention
+        if are_owners_notified is not None:
+            retention_attributes['are_owners_notified'] = are_owners_notified
+        if custom_notification_recipients is not None:
+            user_list = [{'type': user.object_type, 'id': user.object_id} for user in custom_notification_recipients]
+            retention_attributes['custom_notification_recipients'] = user_list
+        box_response = self._session.post(url, data=json.dumps(retention_attributes))
+        response = box_response.json()
+        return self.translator.translate(response['type'])(session=self._session, object_id=response['id'], response_object=response)
+
+    def get_retention_policies(
+            self,
+            policy_name=None,
+            policy_type=None,
+            user=None,
+            limit=None,
+            marker=None,
+            fields=None,
+    ):
+        """
+        Get the entries in the retention policy using marker-based paging.
+
+        :param policy_name:
+            The name of the retention policy.
+        :type policy_name:
+            `unicode` or None
+        :param policy_type:
+            Set to either `finite` or `indefinite`
+        :type policy_type:
+            `unicode` or None
+        :param user:
+            A user to filter the retention policies.
+        :type user:
+            :class:`User` or None
+        :param limit:
+            The maximum number of entries to return per page. If not specified, then will use the server-side default.
+        :type limit:
+            `int` or None
+        :param marker:
+            The paging marker to start paging from
+        :type marker:
+            `unicode` or None
+        :param fields:
+            List of fields to request
+        :type fields:
+            `Iterable` of `unicode`
+        :returns:
+            An iterator of the entries in the retention policy
+        :rtype:
+            :class:`BoxObjectCollection`
+        """
+        additional_params = {}
+        if policy_name is not None:
+            additional_params['policy_name'] = policy_name
+        if policy_type is not None:
+            additional_params['policy_type'] = policy_type
+        if user is not None:
+            additional_params['created_by_user_id'] = user.object_id
+        return MarkerBasedObjectCollection(
+            session=self._session,
+            url=self._session.get_url('retention_policies'),
+            additional_params=additional_params,
+            limit=limit,
+            marker=marker,
+            fields=fields,
+            return_full_pages=False,
+        )
+
+    def get_file_version_retentions(
+            self,
+            target_file=None,
+            file_version=None,
+            policy=None,
+            disposition_action=None,
+            disposition_before=None,
+            disposition_after=None,
+            limit=None,
+            marker=None,
+            fields=None,
+    ):
+        """
+        Get the entries in the file version retention.
+
+        :param target_file:
+            The file to filter the file version.
+        :type target_file:
+            :class:`File` or None
+        :param file_version:
+            A file version to filter the file version retentions by.
+        :type file_version:
+            :class:`FileVersion` or None
+        :param policy:
+            A policy to filter the file version retentions by.
+        :type policy:
+            :class:`RetentionPolicy` or None
+        :param disposition_action:
+            Can be set to `permanently_delete` or `remove_retention`.
+        :type disposition_action:
+            `unicode` or None
+        :param disposition_before:
+            A date time filter for disposition action.
+        :type disposition_before:
+            `unicode` or None
+        :param disposition_after:
+            A date time filter for disposition action.
+        :type disposition_after:
+            `unicode` or None
+        :param limit:
+            The maximum number of entries to return per page. If not specified, then will use the server-side default.
+        :type limit:
+            `int` or None
+        :param marker:
+            The paging marker to start paging from
+        :type marker:
+            `unicode` or None
+        :param fields:
+            List of fields to request
+        :type fields:
+            `Iterable` of `unicode`
+        :returns:
+           An iterator of the entries in the file version retention.
+        :rtype:
+            :class:`BoxObjectCollection`
+        """
+        additional_params = {}
+        if target_file is not None:
+            additional_params['file_id'] = target_file.object_id
+        if file_version is not None:
+            additional_params['file_version_id'] = file_version.object_id
+        if policy is not None:
+            additional_params['policy_id'] = policy.object_id
+        if disposition_action is not None:
+            additional_params['disposition_action'] = disposition_action
+        if disposition_before is not None:
+            additional_params['disposition_before'] = disposition_before
+        if disposition_after is not None:
+            additional_params['disposition_after'] = disposition_after
+        return MarkerBasedObjectCollection(
+            session=self._session,
+            url=self._session.get_url('file_version_retentions'),
+            additional_params=additional_params,
+            limit=limit,
+            marker=marker,
+            fields=fields,
+            return_full_pages=False,
+        )
+
     def web_link(self, web_link_id):
         """
         Initialize a :class: `WebLink` object, whose box id is web_link_id.
diff --git a/boxsdk/object/__init__.py b/boxsdk/object/__init__.py
index f680836..912f37c 100644
--- a/boxsdk/object/__init__.py
+++ b/boxsdk/object/__init__.py
@@ -13,6 +13,7 @@
     'events',
     'event',
     'file',
+    'file_version_retention',
     'folder',
     'group',
     'group_membership',
@@ -20,6 +21,8 @@
     'legal_hold_policy',
     'legal_hold_policy_assignment',
     'recent_item',
+    'retention_policy',
+    'retention_policy_assignment',
     'search',
     'user',
     'watermark',
diff --git a/boxsdk/object/file_version_retention.py b/boxsdk/object/file_version_retention.py
new file mode 100644
index 0000000..36dcd1f
--- /dev/null
+++ b/boxsdk/object/file_version_retention.py
@@ -0,0 +1,9 @@
+# coding: utf-8
+from __future__ import unicode_literals, absolute_import
+
+from .base_object import BaseObject
+
+
+class FileVersionRetention(BaseObject):
+    """Represents a Box file version retention."""
+    _item_type = 'file_version_retention'
diff --git a/boxsdk/object/retention_policy.py b/boxsdk/object/retention_policy.py
new file mode 100644
index 0000000..8126371
--- /dev/null
+++ b/boxsdk/object/retention_policy.py
@@ -0,0 +1,88 @@
+# coding: utf-8
+from __future__ import unicode_literals, absolute_import
+import json
+
+from .base_object import BaseObject
+from ..pagination.marker_based_object_collection import MarkerBasedObjectCollection
+
+
+class RetentionPolicy(BaseObject):
+    """Represents a Box retention policy."""
+    _item_type = 'retention_policy'
+
+    def get_url(self, *args):
+        """
+        Returns the url for this retention policy.
+        """
+        return self._session.get_url('retention_policies', self._object_id, *args)
+
+    def assign(self, assignee, fields=None):
+        """Assign a retention policy to a Box item
+
+        :param assignee:
+            The item to assign the retention policy on.
+        :type assignee:
+            :class:`Folder`, :class:`Enterprise`, or :class:`MetadataTemplate`
+        :param fields:
+            List of fields to request.
+        :type fields:
+            `Iterable` of `unicode`
+        :returns:
+            A :class:`RetentionPolicyAssignment` object.
+        :rtype:
+            :class:`RetentionPolicyAssignment`
+        """
+        url = self._session.get_url('retention_policy_assignments')
+        body = {
+            'policy_id': self.object_id,
+            'assign_to': {
+                'type': assignee.object_type,
+                'id': assignee.object_id,
+            }
+        }
+        params = {}
+        if fields is not None:
+            params['fields'] = ','.join(fields)
+        response = self._session.post(url, data=json.dumps(body), params=params).json()
+        return self.translator.translate(response['type'])(
+            session=self._session,
+            object_id=response['id'],
+            response_object=response,
+        )
+
+    def assignments(self, assignment_type=None, limit=None, marker=None, fields=None):
+        """Get the assignments for the retention policy.
+
+        :param assignment_type:
+            The type of retention policy assignment to retrieve. Can be set to 'folder', 'enterprise', or 'metadata_template'.
+        :type assignment_type:
+            `unicode` or None.
+        :param limit:
+            The maximum number of items to return.
+        :type limit:
+            `int` or None
+        :param marker:
+            The position marker at which to begin the response.
+        :type marker:
+            `unicode` or None
+        :param fields:
+            List of fields to request.
+        :type fields:
+            `Iterable` of `unicode`
+        :returns:
+            An iterable of assignments in the retention policy.
+        :rtype:
+            :class:`BoxObjectCollection`
+        """
+        additional_params = {}
+        if assignment_type is not None:
+            additional_params['assignment_type'] = assignment_type
+        return MarkerBasedObjectCollection(
+            session=self._session,
+            url=self.get_url('assignments'),
+            additional_params=additional_params,
+            limit=limit,
+            marker=marker,
+            fields=fields,
+            return_full_pages=False,
+        )
diff --git a/boxsdk/object/retention_policy_assignment.py b/boxsdk/object/retention_policy_assignment.py
new file mode 100644
index 0000000..a8f05bd
--- /dev/null
+++ b/boxsdk/object/retention_policy_assignment.py
@@ -0,0 +1,8 @@
+# coding: utf-8
+from __future__ import unicode_literals, absolute_import
+from .base_object import BaseObject
+
+
+class RetentionPolicyAssignment(BaseObject):
+    """Represents a Box retention policy assignment."""
+    _item_type = 'retention_policy_assignment'
diff --git a/docs/retention_policy.md b/docs/retention_policy.md
new file mode 100644
index 0000000..63bf70a
--- /dev/null
+++ b/docs/retention_policy.md
@@ -0,0 +1,137 @@
+Retention Policies
+==================
+
+A retention policy blocks permanent deletion of content for a specified amount of time. Admins can create retention policies and then later assign them to specific folders or their entire enterprise.
+
+<!-- START doctoc generated TOC please keep comment here to allow auto update -->
+<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
+
+
+- [Create Retention Policy](#create-retention-policy)
+- [Get Retention Policy](#get-retention-policy)
+- [Get Retention Policies](#get-retention-policies)
+- [Update Retention Policy](#update-retention-policy)
+- [Assign Retention Policy](#assign-retention-policy)
+- [Get Retention Policy Assignment](#get-retention-policy-assignment)
+- [Get Retention Policy Assignments](#get-retention-policy-assignments)
+- [Get File Version Retentions](#get-file-version-retentions)
+- [Get Information about a File Version Retention](#get-information-about-a-file-version-retention)
+
+<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+
+Create Retention Policy
+-----------------------
+
+You can call `client.create_retention_policy(policy_name, disposition_action, retention_length, can_owner_extend_retention=None, are_owners_notified=None, custom_notification_recipients=None)` to let you create a new indefinite retention policy.
+
+```python
+policy_name = 'Test Indefinite Policy Name'
+disposition_action = 'remove_retention'
+indefinite_retention_policy = client.create_retention_policy(policy_name, disposition_action, float('inf'))
+```
+
+Alternatively, if you want to create a finite retention policy, you can do so by calling `client.create_retention_policy(policy_name, disposition_action, retention_length=5)`
+
+```python
+policy_name = 'Test Finite Policy Name'
+disposition_action = 'remove_retention'
+retention_length = 5
+finite_retention_policy = client.create_retention_policy(policy_name=policy_name, disposition_action=disposition_action, retention_length=retention_length)
+```
+
+Get Retention Policy
+--------------------
+
+Calling `client.get(fields=None, headers=None)` will return a retention policy object.
+
+```python
+policy_id = '1234'
+retention_policy = client.retention_policy(policy_id).get()
+```
+
+Get Retention Policies
+----------------------
+
+Calling `client.get_retention_policies(policy_name=None, policy_type=None, user=None, limit=None, marker=None, fields=None)` will return all retention policies for the enterprise.
+
+```python
+retention_policies = client.get_retention_policies()
+for policy in retention_policies:
+    # Do something
+```
+
+Update Retention Policy
+-----------------------
+
+Calling `retention_policy.update_info(data, params=None, headers=None, **kwargs)` will return the updated retention policy object.
+
+```python
+policy_id = '1234'
+policy_update = {'policy_name': 'New Policy Name',}
+updated_retention_policy = client.retention_policy(policy_id).update_info(policy_update)
+```
+
+Assign Retention Policy
+-----------------------
+
+To create a new retention policy assignment call `retention_policy.assign(assignee, fields=None)` to assign the policy to a specific enterprise, 
+folder, or metadata_template If assigining to an enterprise you will not have to provide the ID.
+
+```python
+policy_id = '1234'
+folder = client.folder('1111')
+retention_policy = client.retention_policy(policy_id)
+policy_assignment = retention_policy.assign(folder)
+```
+
+Get Retention Policy Assignment
+-------------------------------
+
+Calling `retention_policy_assignment.get(fields=None, headers=None)` will return the retention policy assignment object.
+
+```python
+assignment_id = '1234'
+assignment = client.retention_policy_assignment(assignment_id).get()
+```
+
+Get Retention Policy Assignments
+--------------------------------
+
+Calling `retention_policy.assigments(assignment_type=None, limit=None, marker=None, fields=None)` will return all retention policy assignments for the enterprise. It is possible to specify maximum number of items per single reponse with
+`retention_policy.assignments(limit=10)`.
+
+```python
+policy_id = '1234'
+assignments = client.retention_policy(policy_id).assignments(limit=10)
+for assignment in assignments:
+    # Do something
+```
+
+Alternatively, you can also specify the `type` of assignment to retrieve with `retention_policy.assignments(assignment_type='folder')`.
+
+```python
+policy_id = '1234'
+assignments = client.retention_policy(policy_id).assignments(assignment_type='folder', limit=10)
+for assignment in assignments:
+    # Do something
+```
+
+Get File Version Retentions
+---------------------------
+
+Calling `client.get_file_version_retentions(target_file=None, file_version=None, policy_id=None, disposition_action=None, disposition_before=None, disposition_after=None, limit=None, marker=None, fields=None)` will return an iterable of file version retentions for the enterprise.
+
+```python
+retentions = client.get_file_version_retentions()
+for retention in retentions:
+    # Do something
+```
+
+Get Information about a File Version Retention
+----------------------------------------------
+
+Calling `file_version_retention.get(fields=None, headers=None)` will return information about the specific file version retention.
+
+```python
+retention_info = client.file_version_retention('1234').get()
+```
\ No newline at end of file


Test patch:
diff --git a/test/unit/client/test_client.py b/test/unit/client/test_client.py
index 2e957ed..b9c7af1 100644
--- a/test/unit/client/test_client.py
+++ b/test/unit/client/test_client.py
@@ -24,6 +24,8 @@
 from boxsdk.object.group import Group
 from boxsdk.object.user import User
 from boxsdk.object.group_membership import GroupMembership
+from boxsdk.object.retention_policy import RetentionPolicy
+from boxsdk.object.file_version_retention import FileVersionRetention
 from boxsdk.object.legal_hold_policy import LegalHoldPolicy
 from boxsdk.pagination.marker_based_object_collection import MarkerBasedObjectCollection
 
@@ -102,6 +104,33 @@ def group_id_2():
     return 202
 
 
+@pytest.fixture()
+def mock_user(mock_box_session):
+    user = User(mock_box_session, '12345')
+    return user
+
+
+@pytest.fixture()
+def mock_user_list(mock_box_session):
+    user_list = []
+    first_user = User(mock_box_session, '33333')
+    second_user = User(mock_box_session, '44444')
+    user_list = [first_user, second_user]
+    return user_list
+
+
+@pytest.fixture()
+def mock_file(mock_box_session):
+    test_file = File(mock_box_session, '11111')
+    return test_file
+
+
+@pytest.fixture()
+def mock_retention_policy(mock_box_session):
+    retention_policy = RetentionPolicy(mock_box_session, '22222')
+    return retention_policy
+
+
 @pytest.fixture(scope='module')
 def groups_response(group_id_1, group_id_2):
     # pylint:disable=redefined-outer-name
@@ -497,6 +526,164 @@ def test_create_enterprise_user_returns_the_correct_user_object(mock_client, moc
     assert new_user.name == test_user_name
 
 
+def test_create_retention_policy(mock_client, mock_box_session, mock_user_list):
+    policy_name = 'Test Retention Policy'
+    policy_type = 'finite'
+    disposition_action = 'remove_retention'
+    expected_url = "{0}/retention_policies".format(API.BASE_API_URL)
+    expected_data = {
+        'policy_name': policy_name,
+        'disposition_action': disposition_action,
+        'policy_type': 'finite',
+        'retention_length': 5,
+        'can_owner_extend_retention': True,
+        'are_owners_notified': False,
+        'custom_notification_recipients': [
+            {
+                'type': mock_user_list[0].object_type,
+                'id': mock_user_list[0].object_id,
+            },
+            {
+                'type': mock_user_list[1].object_type,
+                'id': mock_user_list[1].object_id,
+            },
+        ],
+    }
+    mock_policy = {
+        'type': 'retention_policy',
+        'id': '1234',
+        'policy_name': policy_name,
+        'policy_type': policy_type,
+        'retention_length': 5,
+        'disposition_action': disposition_action,
+        'can_owner_extend_retention': False,
+        'are_owners_notified': False,
+        'custom_notification_recipients': [
+            {
+                'type': mock_user_list[0].object_type,
+                'id': mock_user_list[0].object_id,
+            },
+            {
+                'type': mock_user_list[1].object_type,
+                'id': mock_user_list[1].object_id,
+            },
+        ],
+    }
+    mock_box_session.post.return_value.json.return_value = mock_policy
+    policy = mock_client.create_retention_policy(
+        policy_name=policy_name,
+        disposition_action=disposition_action,
+        retention_length=5,
+        can_owner_extend_retention=True,
+        are_owners_notified=False,
+        custom_notification_recipients=mock_user_list
+    )
+    mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_data))
+    assert policy.object_id == mock_policy['id']
+    assert policy.object_type == mock_policy['type']
+    assert policy.policy_name == mock_policy['policy_name']
+    assert policy.disposition_action == mock_policy['disposition_action']
+    assert policy.can_owner_extend_retention == mock_policy['can_owner_extend_retention']
+    assert policy.are_owners_notified == mock_policy['are_owners_notified']
+    assert isinstance(policy, RetentionPolicy)
+
+
+def test_create_infinte_retention_policy(mock_client, mock_box_session):
+    policy_name = 'Test Retention Policy'
+    policy_type = 'indefinite'
+    disposition_action = 'remove_retention'
+    expected_url = "{0}/retention_policies".format(API.BASE_API_URL)
+    expected_data = {
+        'policy_name': policy_name,
+        'disposition_action': disposition_action,
+        'policy_type': policy_type,
+        'can_owner_extend_retention': False,
+        'are_owners_notified': False,
+    }
+    mock_policy = {
+        'type': 'retention_policy',
+        'id': '1234',
+        'policy_name': policy_name,
+        'policy_type': policy_type,
+        'disposition_action': disposition_action,
+        'can_owner_extend_retention': False,
+        'are_owners_notified': False,
+    }
+    mock_box_session.post.return_value.json.return_value = mock_policy
+    policy = mock_client.create_retention_policy(
+        policy_name=policy_name,
+        disposition_action=disposition_action,
+        retention_length=float('inf'),
+        can_owner_extend_retention=False,
+        are_owners_notified=False
+    )
+    mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_data))
+    assert policy.object_id == mock_policy['id']
+    assert policy.object_type == mock_policy['type']
+    assert policy.policy_name == mock_policy['policy_name']
+    assert policy.disposition_action == mock_policy['disposition_action']
+    assert policy.can_owner_extend_retention == mock_policy['can_owner_extend_retention']
+    assert policy.are_owners_notified == mock_policy['are_owners_notified']
+    assert isinstance(policy, RetentionPolicy)
+
+
+def test_get_retention_policies(mock_client, mock_box_session, mock_user):
+    expected_url = "{0}/retention_policies".format(API.BASE_API_URL)
+    mock_policy = {
+        'type': 'retention_policy',
+        'id': '12345',
+        'name': 'Test Retention Policy',
+    }
+    mock_box_session.get.return_value.json.return_value = {
+        'limit': 100,
+        'entries': [mock_policy],
+        'next_marker': 'testMarker',
+    }
+    policies = mock_client.get_retention_policies(policy_name='Test Name', policy_type='finite', user=mock_user)
+    policy = policies.next()
+    params = {
+        'policy_name': 'Test Name',
+        'policy_type': 'finite',
+        'created_by_user_id': '12345',
+    }
+    mock_box_session.get.assert_called_once_with(expected_url, params=params)
+    assert isinstance(policy, RetentionPolicy)
+    assert policy.id == mock_policy['id']
+    assert policy.name == mock_policy['name']
+
+
+def test_get_file_version_retentions(mock_client, mock_box_session, mock_file, mock_retention_policy):
+    expected_url = "{0}/file_version_retentions".format(API.BASE_API_URL)
+    mock_retention = {
+        'type': 'file_version_retention',
+        'id': '12345',
+    }
+    mock_box_session.get.return_value.json.return_value = {
+        'limit': 100,
+        'entries': [mock_retention],
+        'next_marker': 'testMarker',
+    }
+    retentions = mock_client.get_file_version_retentions(
+        target_file=mock_file,
+        policy=mock_retention_policy,
+        disposition_action='remove_retention',
+        disposition_before='2014-09-15T13:15:35-07:00',
+        disposition_after='2014-09-20T13:15:35-07:00',
+    )
+    retention = retentions.next()
+    params = {
+        'file_id': '11111',
+        'policy_id': '22222',
+        'disposition_action': 'remove_retention',
+        'disposition_before': '2014-09-15T13:15:35-07:00',
+        'disposition_after': '2014-09-20T13:15:35-07:00',
+    }
+    mock_box_session.get.assert_called_once_with(expected_url, params=params)
+    assert isinstance(retention, FileVersionRetention)
+    assert retention.id == mock_retention['id']
+    assert retention.type == mock_retention['type']
+
+
 def test_get_pending_collaborations(mock_client, mock_box_session):
     # pylint:disable=redefined-outer-name, protected-access
     expected_url = '{0}/collaborations'.format(API.BASE_API_URL)
diff --git a/test/unit/object/conftest.py b/test/unit/object/conftest.py
index 5416afc..9e5e973 100644
--- a/test/unit/object/conftest.py
+++ b/test/unit/object/conftest.py
@@ -11,6 +11,7 @@
 from boxsdk.object.comment import Comment
 from boxsdk.object.device_pinner import DevicePinner
 from boxsdk.object.file import File
+from boxsdk.object.file_version_retention import FileVersionRetention
 from boxsdk.object.legal_hold import LegalHold
 from boxsdk.object.folder import Folder
 from boxsdk.object.group import Group
@@ -18,6 +19,8 @@
 from boxsdk.object.legal_hold_policy import LegalHoldPolicy
 from boxsdk.object.legal_hold_policy_assignment import LegalHoldPolicyAssignment
 from boxsdk.object.user import User
+from boxsdk.object.retention_policy import RetentionPolicy
+from boxsdk.object.retention_policy_assignment import RetentionPolicyAssignment
 from boxsdk.object.search import Search
 from boxsdk.object.web_link import WebLink
 
@@ -98,6 +101,20 @@ def test_group(mock_box_session, mock_group_id):
 
 
 @pytest.fixture()
+def test_retention_policy(mock_box_session, mock_object_id):
+    return RetentionPolicy(mock_box_session, mock_object_id)
+
+
+@pytest.fixture()
+def test_file_version_retention(mock_box_session, mock_object_id):
+    return FileVersionRetention(mock_box_session, mock_object_id)
+
+
+@pytest.fixture()
+def test_retention_policy_assignment(mock_box_session, mock_object_id):
+    return RetentionPolicyAssignment(mock_box_session, mock_object_id)
+
+
 def test_group_membership(mock_box_session, mock_object_id):
     return GroupMembership(mock_box_session, mock_object_id)
 
diff --git a/test/unit/object/test_file_version_retention.py b/test/unit/object/test_file_version_retention.py
new file mode 100644
index 0000000..74a05db
--- /dev/null
+++ b/test/unit/object/test_file_version_retention.py
@@ -0,0 +1,15 @@
+from __future__ import unicode_literals, absolute_import
+
+from boxsdk.config import API
+from boxsdk.object.file_version_retention import FileVersionRetention
+
+
+def test_get(test_file_version_retention, mock_box_session):
+    expected_url = '{0}/file_version_retentions/{1}'.format(API.BASE_API_URL, test_file_version_retention.object_id)
+    mock_box_session.get.return_value.json.return_value = {
+        'type': 'file_version_retention',
+        'id': test_file_version_retention.object_id,
+    }
+    file_version_retention = test_file_version_retention.get()
+    mock_box_session.get.assert_called_once_with(expected_url, headers=None, params=None)
+    assert isinstance(file_version_retention, FileVersionRetention)
diff --git a/test/unit/object/test_retention_policy.py b/test/unit/object/test_retention_policy.py
new file mode 100644
index 0000000..f974e9d
--- /dev/null
+++ b/test/unit/object/test_retention_policy.py
@@ -0,0 +1,91 @@
+from __future__ import unicode_literals, absolute_import
+
+import json
+
+from boxsdk.config import API
+from boxsdk.object.retention_policy import RetentionPolicy
+from boxsdk.object.retention_policy_assignment import RetentionPolicyAssignment
+
+
+def test_get(test_retention_policy, mock_box_session):
+    expected_url = '{0}/retention_policies/{1}'.format(API.BASE_API_URL, test_retention_policy.object_id)
+    mock_box_session.get.return_value.json.return_value = {
+        'type': test_retention_policy.object_type,
+        'id': test_retention_policy.object_id,
+        'policy_name': 'Policy Name',
+        'policy_type': 'finite',
+        'retention_length': '10',
+        'disposition_action': 'permanently_delete',
+    }
+    retention_policy = test_retention_policy.get()
+    mock_box_session.get.assert_called_once_with(expected_url, headers=None, params=None)
+    assert isinstance(retention_policy, RetentionPolicy)
+    assert retention_policy['type'] == test_retention_policy.object_type
+    assert retention_policy['id'] == test_retention_policy.object_id
+    assert retention_policy['policy_name'] == 'Policy Name'
+
+
+def test_update(test_retention_policy, mock_box_session):
+    new_policy_name = 'New Name'
+    expected_url = '{0}/retention_policies/{1}'.format(API.BASE_API_URL, test_retention_policy.object_id)
+    mock_box_session.put.return_value.json.return_value = {
+        'type': test_retention_policy.object_type,
+        'id': test_retention_policy.object_id,
+        'policy_name': new_policy_name,
+        'policy_type': 'finite',
+        'retention_length': '10',
+    }
+    data = {
+        'policy_name': new_policy_name,
+    }
+    retention_policy = test_retention_policy.update_info(data)
+    mock_box_session.put.assert_called_once_with(expected_url, data=json.dumps(data), headers=None, params=None)
+    assert isinstance(retention_policy, RetentionPolicy)
+    assert retention_policy['type'] == test_retention_policy.object_type
+    assert retention_policy['id'] == test_retention_policy.object_id
+    assert retention_policy['policy_name'] == new_policy_name
+
+
+def test_assign(test_retention_policy, test_folder, mock_box_session):
+    policy_id = '42'
+    expected_url = mock_box_session.get_url('retention_policy_assignments')
+    expected_data = {
+        'policy_id': policy_id,
+        'assign_to': {
+            'type': test_folder.object_type,
+            'id': test_folder.object_id,
+        }
+    }
+    mock_assignment = {
+        'type': 'retention_policy_assignment',
+        'id': '1234',
+        'retention_policy': {
+            'type': 'retention_policy',
+            'id': policy_id,
+        }
+    }
+    mock_box_session.post.return_value.json.return_value = mock_assignment
+    assignment = test_retention_policy.assign(test_folder)
+    mock_box_session.post.assert_called_once_with(expected_url, data=json.dumps(expected_data), params={})
+    assert assignment.id == mock_assignment['id']
+    assert assignment.retention_policy['id'] == mock_assignment['retention_policy']['id']
+    assert isinstance(assignment, RetentionPolicyAssignment)
+
+
+def test_get_assignments(test_retention_policy, mock_box_session):
+    expected_url = test_retention_policy.get_url('assignments')
+    mock_assignment = {
+        'type': 'retention_policy_assignment',
+        'id': '12345',
+    }
+    mock_box_session.get.return_value.json.return_value = {
+        'limit': 100,
+        'entries': [mock_assignment],
+        'next_marker': 'testMarker',
+    }
+    assignments = test_retention_policy.assignments()
+    assignment = assignments.next()
+    mock_box_session.get.assert_called_once_with(expected_url, params={})
+    assert isinstance(assignment, RetentionPolicyAssignment)
+    assert assignment.id == mock_assignment['id']
+    assert assignment.type == mock_assignment['type']
diff --git a/test/unit/object/test_retention_policy_assignment.py b/test/unit/object/test_retention_policy_assignment.py
new file mode 100644
index 0000000..c662623
--- /dev/null
+++ b/test/unit/object/test_retention_policy_assignment.py
@@ -0,0 +1,17 @@
+from __future__ import unicode_literals, absolute_import
+
+from boxsdk.config import API
+from boxsdk.object.retention_policy_assignment import RetentionPolicyAssignment
+
+
+def test_get_assignment(test_retention_policy_assignment, mock_box_session):
+    expected_url = '{0}/retention_policy_assignments/{1}'.format(API.BASE_API_URL, test_retention_policy_assignment.object_id)
+    mock_box_session.get.return_value.json.return_value = {
+        'type': test_retention_policy_assignment.object_type,
+        'id': test_retention_policy_assignment.object_id,
+    }
+    retention_policy_assignment = test_retention_policy_assignment.get()
+    mock_box_session.get.assert_called_once_with(expected_url, headers=None, params=None)
+    assert isinstance(retention_policy_assignment, RetentionPolicyAssignment)
+    assert retention_policy_assignment['type'] == test_retention_policy_assignment.object_type
+    assert retention_policy_assignment['id'] == test_retention_policy_assignment.object_id