Repository: Duke-GCB/DukeDSClient

Issue:
Make use of the improved affiliate DukeDS search
DukeDS requires users to be registered with DukeDS internal user list for them to receive project permissions or deliver a project to them. Currently DukeDSClient only registers users with DukeDS if we receive a username(Netid). Recent changes allow searching for affiliates based on their email address: https://api.dataservice.duke.edu/apiexplorer#!/auth_providers/getApiV1AuthProviders
We can use this to determine a Netid for a user's email address and register them.

Note this is similar to #209 but for affiliates and not DukeDS users.

Discussion:

Patch:
diff --git a/DukeDS/__init__.py b/DukeDS/__init__.py
index a3104a8..2a62a65 100644
--- a/DukeDS/__init__.py
+++ b/DukeDS/__init__.py
@@ -6,6 +6,9 @@ list_files = DukeDS.list_files
 download_file = DukeDS.download_file
 upload_file = DukeDS.upload_file
 delete_file = DukeDS.delete_file
+can_deliver_to_user_with_email = DukeDS.can_deliver_to_user_with_email
+can_deliver_to_user_with_username = DukeDS.can_deliver_to_user_with_username
 
 __all__ = ['list_projects', 'create_project', 'delete_project',
-           'list_files', 'download_file', 'upload_file', 'delete_file']
+           'list_files', 'download_file', 'upload_file', 'delete_file',
+           'can_deliver_to_user_with_email', 'can_deliver_to_user_with_username']
diff --git a/ddsc/core/ddsapi.py b/ddsc/core/ddsapi.py
index d0dd066..56bba2f 100644
--- a/ddsc/core/ddsapi.py
+++ b/ddsc/core/ddsapi.py
@@ -972,6 +972,18 @@ class DataServiceApi(object):
         """
         return self._get_single_item('/auth_providers/{}/'.format(auth_provider_id), {})
 
+    def get_default_auth_provider_id(self):
+        """
+        Get default auth provider.
+        :return: str: uuid of auth provider
+        """
+        response = self.get_auth_providers()
+        auth_providers = response.json()["results"]
+        for auth_provider in auth_providers:
+            if auth_provider["is_default"]:
+                return auth_provider["id"]
+        raise ValueError("Unable to find a default DukeDS auth provider.")
+
     def get_auth_provider_affiliates(self, auth_provider_id, full_name_contains=None, email=None, username=None):
         """
         List affiliates for a specific auth provider.
diff --git a/ddsc/core/userutil.py b/ddsc/core/userutil.py
new file mode 100644
index 0000000..d210c2d
--- /dev/null
+++ b/ddsc/core/userutil.py
@@ -0,0 +1,59 @@
+import logging
+
+DUKE_EMAIL_SUFFIX = "@duke.edu"
+
+
+class UserUtil(object):
+    def __init__(self, data_service, logging_func=logging.info):
+        self.data_service = data_service
+        self.auth_provider_id = data_service.get_default_auth_provider_id()
+        self.logging_func = logging_func
+
+    def find_user_by_username(self, username):
+        response = self.data_service.get_users(username=username)
+        return self._get_single_user_or_none(response, username)
+
+    def register_user_by_username(self, username):
+        response = self.data_service.auth_provider_add_user(self.auth_provider_id, username)
+        return response.json()
+
+    def find_user_by_email(self, email_address):
+        response = self.data_service.get_users(email=email_address)
+        return self._get_single_user_or_none(response, email_address)
+
+    def find_affiliate_by_email(self, email_address):
+        response = self.data_service.get_auth_provider_affiliates(self.auth_provider_id, email=email_address)
+        return self._get_single_user_or_none(response, email_address)
+
+    def find_affiliate_by_username(self, username):
+        response = self.data_service.get_auth_provider_affiliates(self.auth_provider_id, username=username)
+        return self._get_single_user_or_none(response, username)
+
+    def user_or_affiliate_exists_for_email(self, email_address):
+        if self.find_user_by_email(email_address):
+            self.logging_func("Found DukeDS user for email address {}.".format(email_address))
+            return True
+        if self.find_affiliate_by_email(email_address):
+            self.logging_func("Found affiliate for email address {}.".format(email_address))
+            return True
+        self.logging_func("No valid DukeDS user or affiliate found for email address {}.".format(email_address))
+        return False
+
+    def user_or_affiliate_exists_for_username(self, username):
+        if self.find_user_by_username(username):
+            self.logging_func("Found DukeDS user for username {}.".format(username))
+            return True
+        if self.find_affiliate_by_username(username):
+            self.logging_func("Found affiliate for username {}.".format(username))
+            return True
+        self.logging_func("No valid DukeDS user or affiliate found for username {}.".format(username))
+        return False
+
+    @staticmethod
+    def _get_single_user_or_none(response, lookup_value):
+        results = response.json()['results']
+        if not results:
+            return None
+        if len(results) == 1:
+            return results[0]
+        raise ValueError("Found multiple users for {}.".format(lookup_value))
diff --git a/ddsc/sdk/dukeds.py b/ddsc/sdk/dukeds.py
index 45fc425..ca42feb 100644
--- a/ddsc/sdk/dukeds.py
+++ b/ddsc/sdk/dukeds.py
@@ -1,6 +1,8 @@
 from __future__ import absolute_import
+import logging
 from ddsc.sdk.client import Client, FileUpload, PathToFiles, ItemNotFound, DuplicateNameError
 from ddsc.config import create_config
+from ddsc.core.userutil import UserUtil
 
 
 class DukeDS(object):
@@ -74,6 +76,26 @@ class DukeDS(object):
         """
         Session().delete_file(project_name, remote_path)
 
+    @staticmethod
+    def can_deliver_to_user_with_email(email_address, logging_func=logging.info):
+        """
+        Determine if we can deliver a project to a user
+        :param email_address: str: email address to lookup
+        :param logging_func: func(str): function that will receive log messages
+        :return: boolean: True if the specified user can receive deliveries
+        """
+        return Session().can_deliver_to_user_with_email(email_address, logging_func)
+
+    @staticmethod
+    def can_deliver_to_user_with_username(username, logging_func=logging.info):
+        """
+        Determine if we can deliver a project to a user
+        :param username: str: username to lookup
+        :param logging_func: func(str): function that will receive log messages
+        :return: boolean: True if the specified user can receive deliveries
+        """
+        return Session().can_deliver_to_user_with_username(username, logging_func)
+
 
 class Session(object):
     """
@@ -196,3 +218,13 @@ class Session(object):
         project = self._get_or_create_project(project_name)
         remote_file = project.get_child_for_path(remote_path)
         remote_file.delete()
+
+    def can_deliver_to_user_with_email(self, email_address, logging_func):
+        data_service = self.client.dds_connection.data_service
+        dds_user_util = UserUtil(data_service, logging_func=logging_func)
+        return dds_user_util.user_or_affiliate_exists_for_email(email_address)
+
+    def can_deliver_to_user_with_username(self, username, logging_func):
+        data_service = self.client.dds_connection.data_service
+        dds_user_util = UserUtil(data_service, logging_func=logging_func)
+        return dds_user_util.user_or_affiliate_exists_for_username(username)


Test patch:
diff --git a/ddsc/core/remotestore.py b/ddsc/core/remotestore.py
index 38668b0..d717cde 100644
--- a/ddsc/core/remotestore.py
+++ b/ddsc/core/remotestore.py
@@ -2,6 +2,7 @@ import os
 from ddsc.core.ddsapi import DataServiceApi, DataServiceError, DataServiceAuth
 from ddsc.core.util import KindType
 from ddsc.core.localstore import HashUtil
+from ddsc.core.userutil import UserUtil
 
 FETCH_ALL_USERS_PAGE_SIZE = 25
 DOWNLOAD_FILE_CHUNK_SIZE = 20 * 1024 * 1024
@@ -86,7 +87,7 @@ class RemoteStore(object):
     def lookup_or_register_user_by_email_or_username(self, email, username):
         """
         Lookup user by email or username. Only fill in one field.
-        For username it will try to register if not found.
+        For both cases it will try to register if not found.
         :param email: str: email address of the user
         :param username: netid of the user to find
         :return: RemoteUser
@@ -94,8 +95,7 @@ class RemoteStore(object):
         if username:
             return self.get_or_register_user_by_username(username)
         else:
-            # API doesn't support registering user by email address yet
-            return self.lookup_user_by_email(email)
+            return self.get_or_register_user_by_email(email)
 
     def lookup_user_by_name(self, full_name):
         """
@@ -116,51 +116,33 @@ class RemoteStore(object):
             raise NotFoundError("User not found:" + full_name)
         return user
 
-    def lookup_user_by_username(self, username):
-        """
-        Finds the single user who has this username or raises ValueError.
-        :param username: str username we are looking for
-        :return: RemoteUser: user we found
-        """
-        matches = self.fetch_users(username=username)
-        if not matches:
-            raise NotFoundError('Username not found: {}.'.format(username))
-        if len(matches) > 1:
-            raise ValueError('Multiple users with same username found: {}.'.format(username))
-        return matches[0]
-
     def get_or_register_user_by_username(self, username):
         """
         Try to lookup user by username. If not found try registering the user.
         :param username: str: username to lookup
         :return: RemoteUser: user we found
         """
-        try:
-            return self.lookup_user_by_username(username)
-        except NotFoundError:
-            return self.register_user_by_username(username)
-
-    def register_user_by_username(self, username):
-        """
-        Tries to register user with the first non-deprecated auth provider.
-        Raises ValueError if the data service doesn't have any non-deprecated providers.
-        :param username: str: netid of the user we are trying to register
-        :return: RemoteUser: user that was created for our netid
-        """
-        current_providers = [prov.id for prov in self.get_auth_providers() if not prov.is_deprecated]
-        if not current_providers:
-            raise ValueError("Unable to register user: no non-deprecated providers found!")
-        auth_provider_id = current_providers[0]
-        return self._register_user_by_username(auth_provider_id, username)
+        util = UserUtil(self.data_service)
+        user_json = util.find_user_by_username(username)
+        if not user_json:
+            user_json = util.register_user_by_username(username)
+        return RemoteUser(user_json)
 
-    def _register_user_by_username(self, auth_provider_id, username):
+    def get_or_register_user_by_email(self, email):
         """
-        Tries to register a user who has a valid netid but isn't registered with DukeDS yet under auth_provider_id.
-        :param auth_provider_id: str: id from RemoteAuthProvider to use for registering
-        :param username: str: netid of the user we are trying to register
-        :return: RemoteUser: user that was created for our netid
+        Try to lookup user by email. If not found try registering the user.
+        Raises ValueError when unable to find/register a user for the email.
+        :param email: str: email to lookup or register a user for
+        :return: RemoteUser: user we found
         """
-        user_json = self.data_service.auth_provider_add_user(auth_provider_id, username).json()
+        util = UserUtil(self.data_service)
+        user_json = util.find_user_by_email(email)
+        if not user_json:
+            affiliate = util.find_affiliate_by_email(email)
+            if affiliate:
+                user_json = util.register_user_by_username(affiliate['uid'])
+            else:
+                raise ValueError("Unable to find or register a user with email {}".format(email))
         return RemoteUser(user_json)
 
     def get_auth_providers(self):
@@ -174,19 +156,6 @@ class RemoteStore(object):
             providers.append(RemoteAuthProvider(data))
         return providers
 
-    def lookup_user_by_email(self, email):
-        """
-        Finds the single user who has this email or raises ValueError.
-        :param email: str email we are looking for
-        :return: RemoteUser user we found
-        """
-        matches = self.fetch_users(email=email)
-        if not matches:
-            raise NotFoundError('Email not found: {}.'.format(email))
-        if len(matches) > 1:
-            raise ValueError('Multiple users with same email found: {}.'.format(email))
-        return matches[0]
-
     def get_current_user(self):
         """
         Fetch info about the current user
diff --git a/ddsc/core/tests/test_ddsapi.py b/ddsc/core/tests/test_ddsapi.py
index 1f093d5..1355bac 100644
--- a/ddsc/core/tests/test_ddsapi.py
+++ b/ddsc/core/tests/test_ddsapi.py
@@ -247,6 +247,35 @@ class TestDataServiceApi(TestCase):
         mock_requests.get.assert_called_with('something.com/v1/auth_providers/provider_id_123/',
                                              headers=ANY, params=ANY)
 
+    def test_get_default_auth_provider_id(self):
+        mock_requests = MagicMock()
+        mock_requests.get.side_effect = [
+            fake_response(status_code=200, json_return_value={"results": [
+                {"id": "456", "is_default": False},
+                {"id": "123", "is_default": True},
+            ]})
+        ]
+        api = DataServiceApi(auth=self.create_mock_auth(config_page_size=100), url="something.com/v1",
+                             http=mock_requests)
+        provider_id = api.get_default_auth_provider_id()
+        self.assertEqual('123', provider_id)
+        mock_requests.get.assert_called_with('something.com/v1/auth_providers', headers=ANY, params=ANY)
+
+    def test_get_default_auth_provider_id_no_default_provider(self):
+        mock_requests = MagicMock()
+        mock_requests.get.side_effect = [
+            fake_response(status_code=200, json_return_value={"results": [
+                {"id": "456", "is_default": False},
+                {"id": "123", "is_default": False},
+            ]})
+        ]
+        api = DataServiceApi(auth=self.create_mock_auth(config_page_size=100), url="something.com/v1",
+                             http=mock_requests)
+        with self.assertRaises(ValueError) as raised_exception:
+            api.get_default_auth_provider_id()
+        self.assertEqual(str(raised_exception.exception), 'Unable to find a default DukeDS auth provider.')
+        mock_requests.get.assert_called_with('something.com/v1/auth_providers', headers=ANY, params=ANY)
+
     def make_mock_requests_for_auth_provider_affiliates(self):
         user = {
             "id": "abc4e9-9987-47eb-bb4e-19f0203efbf6",
diff --git a/ddsc/core/tests/test_remotestore.py b/ddsc/core/tests/test_remotestore.py
index eb3a7c8..60c3a1f 100644
--- a/ddsc/core/tests/test_remotestore.py
+++ b/ddsc/core/tests/test_remotestore.py
@@ -9,7 +9,7 @@ from ddsc.core.remotestore import RemoteAuthRole
 from ddsc.core.remotestore import RemoteProjectChildren
 from ddsc.core.remotestore import RemoteAuthProvider
 from ddsc.core.remotestore import ProjectNameOrId
-from ddsc.core.remotestore import ProjectFile, RemoteFileUrl, NotFoundError
+from ddsc.core.remotestore import ProjectFile, RemoteFileUrl
 
 
 class TestProjectFolderFile(TestCase):
@@ -559,43 +559,90 @@ class TestRemoteStore(TestCase):
         self.assertEqual(users[0], mock_remote_user.return_value)
         mock_remote_user.assert_called_with(user_dict)
 
-    def test_lookup_user_by_username(self):
-        mock_user = Mock()
-        mock_user2 = Mock()
+    def test_lookup_or_register_user_by_email_or_username_with_username(self):
         remote_store = RemoteStore(config=MagicMock())
-        remote_store.fetch_users = Mock()
-
-        remote_store.fetch_users.return_value = []
-        with self.assertRaises(NotFoundError) as raised_exception:
-            remote_store.lookup_user_by_username('joe')
-        self.assertEqual(str(raised_exception.exception), 'Username not found: joe.')
-
-        remote_store.fetch_users.return_value = [mock_user]
-        self.assertEqual(remote_store.lookup_user_by_username('joe'), mock_user)
+        remote_store.get_or_register_user_by_username = Mock()
+        remote_store.get_or_register_user_by_email = Mock()
+        result = remote_store.lookup_or_register_user_by_email_or_username(email=None, username="myuser")
+        self.assertEqual(result, remote_store.get_or_register_user_by_username.return_value)
+        remote_store.get_or_register_user_by_username.assert_called_with("myuser")
+        remote_store.get_or_register_user_by_email.assert_not_called()
+
+    def test_lookup_or_register_user_by_email_or_username_with_email(self):
+        remote_store = RemoteStore(config=MagicMock())
+        remote_store.get_or_register_user_by_username = Mock()
+        remote_store.get_or_register_user_by_email = Mock()
+        result = remote_store.lookup_or_register_user_by_email_or_username(email="fakemail@duke.edu", username=None)
+        self.assertEqual(result, remote_store.get_or_register_user_by_email.return_value)
+        remote_store.get_or_register_user_by_username.assert_not_called()
+        remote_store.get_or_register_user_by_email.assert_called_with("fakemail@duke.edu")
+
+    @patch('ddsc.core.remotestore.UserUtil', autospec=True)
+    @patch('ddsc.core.remotestore.RemoteUser')
+    def test_get_or_register_user_by_username__finds_user(self, mock_remote_user, mock_user_util):
+        mock_user_util.return_value.find_user_by_username.return_value = {"id": "123"}
 
-        remote_store.fetch_users.return_value = [mock_user, mock_user2]
-        with self.assertRaises(ValueError) as raised_exception:
-            remote_store.lookup_user_by_username('joe')
-        self.assertEqual(str(raised_exception.exception), 'Multiple users with same username found: joe.')
+        remote_store = RemoteStore(config=MagicMock())
+        result = remote_store.get_or_register_user_by_username("user123")
+        self.assertEqual(result, mock_remote_user.return_value)
+        mock_remote_user.assert_called_with({"id": "123"})
+        mock_util = mock_user_util.return_value
+        mock_util.find_user_by_username.assert_called_with("user123")
+        mock_util.register_user_by_username.assert_not_called()
+
+    @patch('ddsc.core.remotestore.UserUtil', autospec=True)
+    @patch('ddsc.core.remotestore.RemoteUser')
+    def test_get_or_register_user_by_username__registers_user(self, mock_remote_user, mock_user_util):
+        mock_user_util.return_value.find_user_by_username.return_value = None
+        mock_user_util.return_value.register_user_by_username.return_value = {"id": "456"}
 
-    def test_lookup_user_by_email(self):
-        mock_user = Mock()
-        mock_user2 = Mock()
         remote_store = RemoteStore(config=MagicMock())
-        remote_store.fetch_users = Mock()
+        result = remote_store.get_or_register_user_by_username("user123")
+        self.assertEqual(result, mock_remote_user.return_value)
+        mock_remote_user.assert_called_with({"id": "456"})
+        mock_util = mock_user_util.return_value
+        mock_util.find_user_by_username.assert_called_with("user123")
+        mock_util.register_user_by_username.assert_called_with("user123")
+
+    @patch('ddsc.core.remotestore.UserUtil', autospec=True)
+    @patch('ddsc.core.remotestore.RemoteUser')
+    def test_get_or_register_user_by_email__finds_user_by_email(self, mock_remote_user, mock_user_util):
+        mock_user_util.return_value.find_user_by_email.return_value = {"id": "123"}
 
-        remote_store.fetch_users.return_value = []
-        with self.assertRaises(NotFoundError) as raised_exception:
-            remote_store.lookup_user_by_email('joe@joe.com')
-        self.assertEqual(str(raised_exception.exception), 'Email not found: joe@joe.com.')
+        remote_store = RemoteStore(config=MagicMock())
+        result = remote_store.get_or_register_user_by_email("user@user.user")
+        self.assertEqual(result, mock_remote_user.return_value)
+        mock_remote_user.assert_called_with({"id": "123"})
+        mock_util = mock_user_util.return_value
+        mock_util.find_user_by_email.assert_called_with("user@user.user")
+        mock_util.register_user_by_username.assert_not_called()
+
+    @patch('ddsc.core.remotestore.UserUtil', autospec=True)
+    @patch('ddsc.core.remotestore.RemoteUser')
+    def test_get_or_register_user_by_email__finds_affiliate_by_email(self, mock_remote_user, mock_user_util):
+        mock_user_util.return_value.find_user_by_email.return_value = None
+        mock_user_util.return_value.find_affiliate_by_email.return_value = {"uid": "user"}
 
-        remote_store.fetch_users.return_value = [mock_user]
-        self.assertEqual(remote_store.lookup_user_by_email('joe@joe.com'), mock_user)
+        remote_store = RemoteStore(config=MagicMock())
+        result = remote_store.get_or_register_user_by_email("user@user.user")
+        self.assertEqual(result, mock_remote_user.return_value)
+        mock_remote_user.assert_called_with(mock_user_util.return_value.register_user_by_username.return_value)
+        mock_util = mock_user_util.return_value
+        mock_util.find_user_by_email.assert_called_with("user@user.user")
+        mock_util.register_user_by_username.assert_called_with("user")
+
+    @patch('ddsc.core.remotestore.UserUtil', autospec=True)
+    @patch('ddsc.core.remotestore.RemoteUser')
+    def test_get_or_register_user_by_email__unable_to_find_user(self, mock_remote_user, mock_user_util):
+        mock_user_util.return_value.find_user_by_email.return_value = None
+        mock_user_util.return_value.find_affiliate_by_email.return_value = None
 
-        remote_store.fetch_users.return_value = [mock_user, mock_user2]
+        remote_store = RemoteStore(config=MagicMock())
         with self.assertRaises(ValueError) as raised_exception:
-            remote_store.lookup_user_by_email('joe@joe.com')
-        self.assertEqual(str(raised_exception.exception), 'Multiple users with same email found: joe@joe.com.')
+            remote_store.get_or_register_user_by_email("user@user.user")
+        self.assertEqual(str(raised_exception.exception), 'Unable to find or register a user with email user@user.user')
+        mock_util = mock_user_util.return_value
+        mock_util.find_user_by_email.assert_called_with("user@user.user")
 
 
 class TestRemoteProjectChildren(TestCase):
@@ -776,48 +823,6 @@ class TestRemoteAuthProvider(TestCase):
         self.assertEqual(len(providers), 1)
         self.assertEqual(providers[0].name, "Duke Authentication Service")
 
-    @patch("ddsc.core.remotestore.DataServiceApi")
-    def test_register_user_by_username(self, mock_data_service_api):
-        providers_response = MagicMock()
-        providers_response.json.return_value = {
-            "results": [self.provider_data1]
-        }
-        add_user_response = MagicMock()
-        add_user_response.json.return_value = {
-            "id": "123",
-            "username": "joe",
-            "full_name": "Joe Shoe",
-            "email": "",
-            "first_name": "Joe",
-            "last_name": "Shoe",
-        }
-        mock_data_service_api().get_auth_providers.return_value = providers_response
-        mock_data_service_api().auth_provider_add_user.return_value = add_user_response
-        remote_store = RemoteStore(MagicMock())
-        user = remote_store.register_user_by_username("joe")
-        self.assertEqual(user.id, "123")
-        self.assertEqual(user.username, "joe")
-        mock_data_service_api().auth_provider_add_user.assert_called_with(self.provider_data1['id'], 'joe')
-
-    @patch("ddsc.core.remotestore.DataServiceApi")
-    def test_register_user_by_username_with_no_default_provider(self, mock_data_service_api):
-        providers_response = MagicMock()
-        providers_response.json.return_value = {
-            "results": []
-        }
-        add_user_response = MagicMock()
-        add_user_response.json.return_value = {
-            "id": "123",
-            "username": "joe",
-            "full_name": "Joe Shoe",
-            "email": "",
-        }
-        mock_data_service_api().get_auth_providers.return_value = providers_response
-        mock_data_service_api().auth_provider_add_user.return_value = add_user_response
-        remote_store = RemoteStore(MagicMock())
-        with self.assertRaises(ValueError):
-            remote_store.register_user_by_username("joe")
-
 
 class TestProjectNameOrId(TestCase):
     def test_constructors(self):
diff --git a/ddsc/core/tests/test_userutil.py b/ddsc/core/tests/test_userutil.py
new file mode 100644
index 0000000..680a169
--- /dev/null
+++ b/ddsc/core/tests/test_userutil.py
@@ -0,0 +1,131 @@
+from unittest import TestCase
+from mock import Mock
+from ddsc.core.userutil import UserUtil
+
+
+class UserUtilTestCase(TestCase):
+    def setUp(self):
+        self.data_service = Mock()
+        self.logging_func = Mock()
+        self.user_util = UserUtil(self.data_service, self.logging_func)
+
+    def test_constructor(self):
+        self.assertEqual(self.user_util.auth_provider_id,
+                         self.data_service.get_default_auth_provider_id.return_value)
+
+    def test_find_user_by_username(self):
+        self.data_service.get_users.return_value.json.return_value = {"results": [{"id": "123"}]}
+        result = self.user_util.find_user_by_username("fakeuser")
+        self.assertEqual(result, {"id": "123"})
+        self.data_service.get_users.assert_called_with(username="fakeuser")
+
+    def test_register_user_by_username(self):
+        self.data_service.auth_provider_add_user.return_value.json.return_value = {"id": "123"}
+        result = self.user_util.register_user_by_username("fakeuser")
+        self.assertEqual(result, {"id": "123"})
+        self.data_service.auth_provider_add_user.assert_called_with(self.user_util.auth_provider_id, "fakeuser")
+
+    def test_find_user_by_email(self):
+        self.data_service.get_users.return_value.json.return_value = {"results": [{"id": "123"}]}
+        result = self.user_util.find_user_by_email("fakeuser@duke.edu")
+        self.assertEqual(result, {"id": "123"})
+        self.data_service.get_users.assert_called_with(email="fakeuser@duke.edu")
+
+    def test_find_affiliate_by_email(self):
+        self.data_service.get_auth_provider_affiliates.return_value.json.return_value = {"results": [{"id": "123"}]}
+        result = self.user_util.find_affiliate_by_email("fakeuser@duke.edu")
+        self.assertEqual(result, {"id": "123"})
+        self.data_service.get_auth_provider_affiliates.assert_called_with(self.user_util.auth_provider_id,
+                                                                          email="fakeuser@duke.edu")
+
+    def test_find_affiliate_by_username(self):
+        self.data_service.get_auth_provider_affiliates.return_value.json.return_value = {"results": [{"id": "123"}]}
+        result = self.user_util.find_affiliate_by_username("fakeuser")
+        self.assertEqual(result, {"id": "123"})
+        self.data_service.get_auth_provider_affiliates.assert_called_with(self.user_util.auth_provider_id,
+                                                                          username="fakeuser")
+
+    def test_user_or_affiliate_exists_for_email__dds_user_exists(self):
+        self.user_util.find_user_by_email = Mock()
+        self.user_util.find_user_by_email.return_value = {"id": "123"}
+
+        self.assertTrue(self.user_util.user_or_affiliate_exists_for_email("fakeuser@duke.edu"))
+        self.user_util.find_user_by_email.assert_called_with("fakeuser@duke.edu")
+        self.logging_func.assert_called_with(
+            "Found DukeDS user for email address fakeuser@duke.edu.")
+
+    def test_user_or_affiliate_exists_for_email__valid_affiliate_exists(self):
+        self.user_util.find_user_by_email = Mock()
+        self.user_util.find_user_by_email.return_value = None
+        self.user_util.find_affiliate_by_email = Mock()
+        self.user_util.find_affiliate_by_email.return_value = {"uid": "123"}
+
+        self.assertTrue(self.user_util.user_or_affiliate_exists_for_email("fakeuser@duke.edu"))
+        self.user_util.find_user_by_email.assert_called_with("fakeuser@duke.edu")
+        self.user_util.find_affiliate_by_email.assert_called_with("fakeuser@duke.edu")
+        self.logging_func.assert_called_with(
+            "Found affiliate for email address fakeuser@duke.edu.")
+
+    def test_valid_dds_user_or_affiliate_exists_for_email__no_user_found(self):
+        self.user_util.find_user_by_email = Mock()
+        self.user_util.find_user_by_email.return_value = None
+        self.user_util.find_affiliate_by_email = Mock()
+        self.user_util.find_affiliate_by_email.return_value = None
+
+        self.assertFalse(self.user_util.user_or_affiliate_exists_for_email("fakeuser@duke.edu"))
+        self.user_util.find_user_by_email.assert_called_with("fakeuser@duke.edu")
+        self.user_util.find_affiliate_by_email.assert_called_with("fakeuser@duke.edu")
+        self.logging_func.assert_called_with(
+            "No valid DukeDS user or affiliate found for email address fakeuser@duke.edu.")
+
+    def test_user_or_affiliate_exists_for_username__dds_user_exists(self):
+        self.user_util.find_user_by_username = Mock()
+        self.user_util.find_user_by_username.return_value = {"id": "123"}
+
+        self.assertTrue(self.user_util.user_or_affiliate_exists_for_username("fakeuser"))
+        self.user_util.find_user_by_username.assert_called_with("fakeuser")
+        self.logging_func.assert_called_with(
+            "Found DukeDS user for username fakeuser.")
+
+    def test_user_or_affiliate_exists_for_username__valid_affiliate_exists(self):
+        self.user_util.find_user_by_username = Mock()
+        self.user_util.find_user_by_username.return_value = None
+        self.user_util.find_affiliate_by_username = Mock()
+        self.user_util.find_affiliate_by_username.return_value = {"uid": "123"}
+
+        self.assertTrue(self.user_util.user_or_affiliate_exists_for_username("fakeuser"))
+        self.user_util.find_user_by_username.assert_called_with("fakeuser")
+        self.user_util.find_affiliate_by_username.assert_called_with("fakeuser")
+        self.logging_func.assert_called_with(
+            "Found affiliate for username fakeuser.")
+
+    def test_valid_dds_user_or_affiliate_exists_for_username__no_user_found(self):
+        self.user_util.find_user_by_username = Mock()
+        self.user_util.find_user_by_username.return_value = None
+        self.user_util.find_affiliate_by_username = Mock()
+        self.user_util.find_affiliate_by_username.return_value = None
+
+        self.assertFalse(self.user_util.user_or_affiliate_exists_for_username("fakeuser"))
+        self.user_util.find_user_by_username.assert_called_with("fakeuser")
+        self.user_util.find_affiliate_by_username.assert_called_with("fakeuser")
+        self.logging_func.assert_called_with(
+            "No valid DukeDS user or affiliate found for username fakeuser.")
+
+    def test__get_single_user_or_none(self):
+        response = Mock()
+
+        # When no items found should return None
+        response.json.return_value = {"results": []}
+        result = self.user_util._get_single_user_or_none(response, lookup_value="fakeuser@duke.edu")
+        self.assertEqual(result, None)
+
+        # When one item found should return first (only) item
+        response.json.return_value = {"results": [{"id": "123"}]}
+        result = self.user_util._get_single_user_or_none(response, lookup_value="fakeuser@duke.edu")
+        self.assertEqual(result, {"id": "123"})
+
+        # When multiple found raise exception
+        response.json.return_value = {"results": [{"id": "123"}, {"id": "456"}]}
+        with self.assertRaises(ValueError) as raised_exception:
+            self.user_util._get_single_user_or_none(response, lookup_value="fakeuser@duke.edu")
+        self.assertEqual(str(raised_exception.exception), 'Found multiple users for fakeuser@duke.edu.')
diff --git a/ddsc/sdk/tests/test_dukeds.py b/ddsc/sdk/tests/test_dukeds.py
index 3a9ef19..002cfa1 100644
--- a/ddsc/sdk/tests/test_dukeds.py
+++ b/ddsc/sdk/tests/test_dukeds.py
@@ -1,6 +1,6 @@
 from unittest import TestCase
 from ddsc import DukeDS, ItemNotFound, DuplicateNameError
-from mock import patch, Mock
+from mock import patch, Mock, ANY
 
 
 class TestDukeDS(TestCase):
@@ -154,3 +154,43 @@ class TestDukeDS(TestCase):
 
         self.mouse_rna_project.get_child_for_path.assert_called_with('data/file1.dat')
         mock_file1.delete.assert_called()
+
+    @patch('ddsc.sdk.dukeds.Client')
+    @patch('ddsc.sdk.dukeds.UserUtil')
+    def test_can_deliver_to_user_with_email(self, mock_user_util, mock_client):
+        result = DukeDS.can_deliver_to_user_with_email(email_address='fakeuser@duke.edu')
+        mock_user_util.assert_called_with(mock_client.return_value.dds_connection.data_service, logging_func=ANY)
+        dds_user_util = mock_user_util.return_value
+        dds_user_util.user_or_affiliate_exists_for_email.assert_called_with("fakeuser@duke.edu")
+        self.assertEqual(result, dds_user_util.user_or_affiliate_exists_for_email.return_value)
+
+    @patch('ddsc.sdk.dukeds.Client')
+    @patch('ddsc.sdk.dukeds.UserUtil')
+    def test_can_deliver_to_user_with_email_with_logging_func(self, mock_user_util, mock_client):
+        mock_log_func = Mock()
+        result = DukeDS.can_deliver_to_user_with_email(email_address='fakeuser@duke.edu', logging_func=mock_log_func)
+        mock_user_util.assert_called_with(mock_client.return_value.dds_connection.data_service,
+                                          logging_func=mock_log_func)
+        dds_user_util = mock_user_util.return_value
+        dds_user_util.user_or_affiliate_exists_for_email.assert_called_with("fakeuser@duke.edu")
+        self.assertEqual(result, dds_user_util.user_or_affiliate_exists_for_email.return_value)
+
+    @patch('ddsc.sdk.dukeds.Client')
+    @patch('ddsc.sdk.dukeds.UserUtil')
+    def test_can_deliver_to_user_with_username(self, mock_user_util, mock_client):
+        result = DukeDS.can_deliver_to_user_with_username(username='fakeuser')
+        mock_user_util.assert_called_with(mock_client.return_value.dds_connection.data_service, logging_func=ANY)
+        dds_user_util = mock_user_util.return_value
+        dds_user_util.user_or_affiliate_exists_for_username.assert_called_with("fakeuser")
+        self.assertEqual(result, dds_user_util.user_or_affiliate_exists_for_username.return_value)
+
+    @patch('ddsc.sdk.dukeds.Client')
+    @patch('ddsc.sdk.dukeds.UserUtil')
+    def test_can_deliver_to_user_with_username_with_logging_func(self, mock_user_util, mock_client):
+        mock_log_func = Mock()
+        result = DukeDS.can_deliver_to_user_with_username(username='fakeuser', logging_func=mock_log_func)
+        mock_user_util.assert_called_with(mock_client.return_value.dds_connection.data_service,
+                                          logging_func=mock_log_func)
+        dds_user_util = mock_user_util.return_value
+        dds_user_util.user_or_affiliate_exists_for_username.assert_called_with("fakeuser")
+        self.assertEqual(result, dds_user_util.user_or_affiliate_exists_for_username.return_value)