You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@libcloud.apache.org by to...@apache.org on 2015/06/14 12:52:18 UTC

[07/21] libcloud git commit: Added unit tests and common/DigitalOceanBaseDriver - Added 'common' to FIXTURES_ROOT for FileFixtures in test/common - Modified DigitalOceanBaseDriver from compute/drivers/digitalocean into common/digitalocean to support v1 a

Added unit tests and common/DigitalOceanBaseDriver - Added 'common' to FIXTURES_ROOT for FileFixtures in test/common - Modified DigitalOceanBaseDriver from compute/drivers/digitalocean into common/digitalocean to support v1 and v2 by initialization - Added dns/driver/digitalocean tests - Added common/digitalocean tests

Signed-off-by: Tomaz Muraus <to...@tomaz.me>


Project: http://git-wip-us.apache.org/repos/asf/libcloud/repo
Commit: http://git-wip-us.apache.org/repos/asf/libcloud/commit/a3a5f173
Tree: http://git-wip-us.apache.org/repos/asf/libcloud/tree/a3a5f173
Diff: http://git-wip-us.apache.org/repos/asf/libcloud/diff/a3a5f173

Branch: refs/heads/trunk
Commit: a3a5f173fe0992de263caff44bc370b051ecefb6
Parents: 66dc7cd
Author: Javier Castillo II <j....@gmail.com>
Authored: Sun Apr 12 01:57:29 2015 +0000
Committer: Tomaz Muraus <to...@tomaz.me>
Committed: Sun Jun 14 18:05:58 2015 +0800

----------------------------------------------------------------------
 libcloud/common/digitalocean.py                 | 112 +++++++++-
 libcloud/dns/drivers/digitalocean.py            |  41 +---
 .../digitalocean/_v1_events_12345670.json       |   1 +
 .../_v1_events_12345670_UNAUTHORIZED.json       |   1 +
 .../fixtures/digitalocean/_v2_account.json      |   1 +
 .../digitalocean/_v2_account_UNAUTHORIZED.json  |   1 +
 .../fixtures/digitalocean/_v2_actions.json      |   1 +
 .../digitalocean/_v2_actions_12345670.json      |   1 +
 .../digitalocean/_v2_actions_page_1.json        |   1 +
 .../digitalocean/_v2_actions_page_2.json        |   1 +
 libcloud/test/common/test_digitalocean_v1.py    |  82 +++++++
 libcloud/test/common/test_digitalocean_v2.py    | 105 +++++++++
 .../dns/fixtures/digitalocean/_v2_domains.json  |   1 +
 .../digitalocean/_v2_domains_CREATE.json        |   1 +
 .../digitalocean/_v2_domains_EMPTY.json         |   1 +
 .../digitalocean/_v2_domains_UNAUTHORIZED.json  |   1 +
 .../digitalocean/_v2_domains_testdomain.json    |   1 +
 .../_v2_domains_testdomain_NOT_FOUND.json       |   1 +
 .../_v2_domains_testdomain_records.json         |   1 +
 .../_v2_domains_testdomain_records_1234560.json |   1 +
 .../_v2_domains_testdomain_records_1234561.json |   1 +
 .../_v2_domains_testdomain_records_1234562.json |   1 +
 .../_v2_domains_testdomain_records_1234564.json |   1 +
 ...ns_testdomain_records_1234564_NOT_FOUND.json |   1 +
 ...mains_testdomain_records_1234564_UPDATE.json |   1 +
 .../_v2_domains_testdomain_records_CREATE.json  |   1 +
 libcloud/test/dns/test_digitalocean.py          | 214 +++++++++++++++++++
 libcloud/test/file_fixtures.py                  |   1 +
 28 files changed, 537 insertions(+), 40 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/common/digitalocean.py
----------------------------------------------------------------------
diff --git a/libcloud/common/digitalocean.py b/libcloud/common/digitalocean.py
index 097b207..7d46881 100644
--- a/libcloud/common/digitalocean.py
+++ b/libcloud/common/digitalocean.py
@@ -19,6 +19,7 @@ Common settings and connection objects for DigitalOcean Cloud
 
 from libcloud.utils.py3 import httplib
 
+from libcloud.common.base import BaseDriver
 from libcloud.common.base import ConnectionUserAndKey, ConnectionKey
 from libcloud.common.base import JsonResponse
 from libcloud.common.types import InvalidCredsError
@@ -26,12 +27,11 @@ from libcloud.common.types import InvalidCredsError
 __all__ = [
     'DigitalOcean_v1_Response',
     'DigitalOcean_v1_Connection',
-    'DigitalOcean_v2_Response'
+    'DigitalOcean_v2_Response',
     'DigitalOcean_v2_Connection',
+    'DigitalOceanBaseDriver'
 ]
 
-AUTH_URL = 'https://api.digitalocean.com'
-
 
 class DigitalOcean_v1_Response(JsonResponse):
     def parse_error(self):
@@ -118,3 +118,109 @@ class DigitalOceanConnection(DigitalOcean_v2_Connection):
 
 class DigitalOceanResponse(DigitalOcean_v2_Response):
     pass
+
+
+class DigitalOceanBaseDriver(BaseDriver):
+    """
+    DigitalOcean BaseDriver
+    """
+    name = 'DigitalOcean'
+    website = 'https://www.digitalocean.com'
+
+    def __new__(cls, key, secret=None, api_version='v2', **kwargs):
+        if cls is DigitalOceanBaseDriver:
+            if api_version == 'v1' or secret != None:
+                cls = DigitalOcean_v1_BaseDriver
+            elif api_version == 'v2':
+                cls = DigitalOcean_v2_BaseDriver
+            else:
+                raise NotImplementedError('Unsupported API version: %s' %
+                                          (api_version))
+        return super(DigitalOceanBaseDriver, cls).__new__(cls, **kwargs)
+
+    def ex_account_info(self):
+        raise NotImplementedError(
+            'ex_account_info not implemented for this driver')
+
+    def ex_list_events(self):
+        raise NotImplementedError(
+            'ex_list_events not implemented for this driver')
+
+    def ex_get_event(self, event_id):
+        raise NotImplementedError(
+            'ex_get_event not implemented for this driver')
+
+    def _paginated_request(self, event_id):
+        raise NotImplementedError(
+            '_paginated_requests not implemented for this driver')
+
+
+class DigitalOcean_v1_BaseDriver(DigitalOceanBaseDriver):
+    """
+    DigitalOcean BaseDriver using v1 of the API.
+    """
+    connectionCls = DigitalOcean_v1_Connection
+
+    def ex_get_event(self, event_id):
+        """
+        Get an event object
+
+        :param      event_id: Event id (required)
+        :type       event_id: ``str``
+        """
+        return self.connection.request('/v1/events/%s' % event_id).object
+
+
+class DigitalOcean_v2_BaseDriver(DigitalOceanBaseDriver):
+    """
+    DigitalOcean BaseDriver using v2 of the API.
+    """
+    connectionCls = DigitalOcean_v2_Connection
+
+    def ex_account_info(self):
+        return self.connection.request('/v2/account').object['account']
+
+    def ex_list_events(self):
+        return self._paginated_request('/v2/actions', 'actions')
+
+    def ex_get_event(self, event_id):
+        """
+        Get an event object
+
+        :param      event_id: Event id (required)
+        :type       event_id: ``str``
+        """
+        params = {}
+        return self.connection.request('/v2/actions/%s' % event_id,
+                                       params=params).object['action']
+
+    def _paginated_request(self, url, obj):
+        """
+        Perform multiple calls in order to have a full list of elements when
+        the API responses are paginated.
+
+        :param url: API endpoint
+        :type url: ``str``
+
+        :param obj: Result object key
+        :type obj: ``str``
+
+        :return: ``list`` of API response objects
+        """
+        params = {}
+        data = self.connection.request(url)
+        try:
+            pages = data.object['links']['pages']['last'].split('=')[-1]
+            values = data.object[obj]
+            for page in range(2, int(pages) + 1):
+                params.update({'page': page})
+                new_data = self.connection.request(url, params=params)
+
+                more_values = new_data.object[obj]
+                for value in more_values:
+                    values.append(value)
+            data = values
+        except KeyError:  # No pages.
+            data = data.object[obj]
+
+        return data

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/dns/drivers/digitalocean.py
----------------------------------------------------------------------
diff --git a/libcloud/dns/drivers/digitalocean.py b/libcloud/dns/drivers/digitalocean.py
index 7e70f11..81e313f 100644
--- a/libcloud/dns/drivers/digitalocean.py
+++ b/libcloud/dns/drivers/digitalocean.py
@@ -22,14 +22,15 @@ __all__ = [
 
 from libcloud.utils.py3 import httplib
 
-from libcloud.common.digitalocean import DigitalOceanConnection, DigitalOceanResponse
+from libcloud.common.digitalocean import DigitalOcean_v2_BaseDriver
+from libcloud.common.digitalocean import DigitalOceanConnection
+from libcloud.common.digitalocean import DigitalOceanResponse
 from libcloud.dns.types import Provider, RecordType
 from libcloud.dns.types import ZoneDoesNotExistError, RecordDoesNotExistError
 from libcloud.dns.base import DNSDriver, Zone, Record
 
 
-class DigitalOceanDNSDriver(DNSDriver):
-    connectionCls = DigitalOceanConnection
+class DigitalOceanDNSDriver(DigitalOcean_v2_BaseDriver, DNSDriver):
     type = Provider.DIGITAL_OCEAN
     name = "DigitalOcean"
     website = 'https://www.digitalocean.com'
@@ -275,40 +276,6 @@ class DigitalOceanDNSDriver(DNSDriver):
                                       method='DELETE')
         return res.status == httplib.NO_CONTENT
 
-# TODO: If there is a way to push this into libcloud.common.digitalocean
-#       instead of having it in libcloud.dns.digitalocean and
-#       libcloud.compute.digitalocean
-    def _paginated_request(self, url, obj):
-        """
-        Perform multiple calls in order to have a full list of elements when
-        the API responses are paginated.
-
-        :param url: API endpoint
-        :type url: ``str``
-
-        :param obj: Result object key
-        :type obj: ``str``
-
-        :return: ``list`` of API response objects
-        """
-        params = {}
-        data = self.connection.request(url)
-        try:
-            pages = data.object['links']['pages']['last'].split('=')[-1]
-            values = data.object[obj]
-            for page in range(2, int(pages) + 1):
-                params.update({'page': page})
-                new_data = self.connection.request(url, params=params)
-
-                more_values = new_data.object[obj]
-                for value in more_values:
-                    values.append(value)
-            data = values
-        except KeyError:  # No pages.
-            data = data.object[obj]
-
-        return data
-
     def _to_record(self, data, zone=None):
         extra = {'port' : data['port'], 'priority' : data['priority'],
                  'weight' : data['weight']}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670.json b/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670.json
new file mode 100644
index 0000000..6ff04b5
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670.json
@@ -0,0 +1 @@
+{"status":"OK","event":{"id":12345670,"event_type_id":1,"percentage":"100","droplet_id":1234560,"action_status":"done"}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670_UNAUTHORIZED.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670_UNAUTHORIZED.json b/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670_UNAUTHORIZED.json
new file mode 100644
index 0000000..0994c93
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v1_events_12345670_UNAUTHORIZED.json
@@ -0,0 +1 @@
+{"status":"ERROR","error_message":"Access Denied","message":"Access Denied"}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v2_account.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v2_account.json b/libcloud/test/common/fixtures/digitalocean/_v2_account.json
new file mode 100644
index 0000000..37083cf
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v2_account.json
@@ -0,0 +1 @@
+{"account":{"droplet_limit":10,"email":"user@domain.tld","uuid":"a1234567890b1234567890c1234567890d12345","email_verified":true}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v2_account_UNAUTHORIZED.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v2_account_UNAUTHORIZED.json b/libcloud/test/common/fixtures/digitalocean/_v2_account_UNAUTHORIZED.json
new file mode 100644
index 0000000..415d9ef
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v2_account_UNAUTHORIZED.json
@@ -0,0 +1 @@
+{"id":"unauthorized","message":"Unable to authenticate you."}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v2_actions.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v2_actions.json b/libcloud/test/common/fixtures/digitalocean/_v2_actions.json
new file mode 100644
index 0000000..3c36117
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v2_actions.json
@@ -0,0 +1 @@
+{"actions":[],"links":{},"meta":{"total":0}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v2_actions_12345670.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v2_actions_12345670.json b/libcloud/test/common/fixtures/digitalocean/_v2_actions_12345670.json
new file mode 100644
index 0000000..9631675
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v2_actions_12345670.json
@@ -0,0 +1 @@
+{"action":{"id":12345670,"status":"completed","type":"power_on","started_at":"2015-03-28T10:57:40Z","completed_at":"2015-03-28T10:57:42Z","resource_id":1234560,"resource_type":"droplet","region":{"name":"New York 3","slug":"nyc3","sizes":["512mb","1gb","2gb","4gb","8gb","16gb","32gb","48gb","64gb"],"features":["virtio","private_networking","backups","ipv6","metadata"],"available":true},"region_slug":"nyc3"}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_1.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_1.json b/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_1.json
new file mode 100644
index 0000000..b9ebd61
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_1.json
@@ -0,0 +1 @@
+{"actions":[{"id":12345671,"status":"completed","type":"create","started_at":"2015-04-10T14:09:37Z","completed_at":"2015-04-10T14:10:03Z","resource_id":1234561,"resource_type":"droplet","region":{"name":"Frankfurt 1","slug":"fra1","sizes":["512mb","1gb","2gb","4gb","8gb","16gb","32gb","48gb","64gb"],"features":["virtio","private_networking","backups","ipv6","metadata"],"available":true},"region_slug":"fra1"}],"links":{"pages":{"last":"https://api.digitalocean.com/v2/actions/?page=2","next":"https://api.digitalocean.com/v2/actions/?page=2"}},"meta":{"total":2}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_2.json
----------------------------------------------------------------------
diff --git a/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_2.json b/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_2.json
new file mode 100644
index 0000000..3ddfad2
--- /dev/null
+++ b/libcloud/test/common/fixtures/digitalocean/_v2_actions_page_2.json
@@ -0,0 +1 @@
+{"actions":[{"id":12345670,"status":"completed","type":"create","started_at":"2015-04-10T14:09:12Z","completed_at":"2015-04-10T14:09:38Z","resource_id":1234560,"resource_type":"droplet","region":{"name":"Frankfurt 1","slug":"fra1","sizes":["512mb","1gb","2gb","4gb","8gb","16gb","32gb","48gb","64gb"],"features":["virtio","private_networking","backups","ipv6","metadata"],"available":true},"region_slug":"fra1"}],"links":{"pages":{"first":"https://api.digitalocean.com/v2/actions/?page=1","prev":"https://api.digitalocean.com/v2/actions/?page=1"}},"meta":{"total":2}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/test_digitalocean_v1.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_digitalocean_v1.py b/libcloud/test/common/test_digitalocean_v1.py
new file mode 100644
index 0000000..e10f54d
--- /dev/null
+++ b/libcloud/test/common/test_digitalocean_v1.py
@@ -0,0 +1,82 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+
+import os
+import sys
+import unittest
+
+from libcloud.common.types import InvalidCredsError
+from libcloud.common.digitalocean import DigitalOceanBaseDriver
+from libcloud.dns.types import RecordType
+from libcloud.test import LibcloudTestCase, MockHttpTestCase
+from libcloud.test.file_fixtures import FileFixtures
+from libcloud.test.secrets import DIGITALOCEAN_v1_PARAMS
+from libcloud.utils.py3 import httplib
+
+
+class DigitalOceanTests(LibcloudTestCase):
+
+    def setUp(self):
+        DigitalOceanBaseDriver.connectionCls.conn_classes = \
+            (None, DigitalOceanMockHttp)
+        DigitalOceanMockHttp.type = None
+        self.driver = DigitalOceanBaseDriver(*DIGITALOCEAN_v1_PARAMS)
+
+    def test_authentication(self):
+        DigitalOceanMockHttp.type = 'UNAUTHORIZED'
+        self.assertRaises(InvalidCredsError, self.driver.ex_get_event, '12345670')
+
+    def test_ex_account_info(self):
+        self.assertRaises(NotImplementedError, self.driver.ex_account_info)
+
+    def test_ex_list_events(self):
+        self.assertRaises(NotImplementedError, self.driver.ex_list_events)
+
+    def test_ex_get_event(self):
+        action = self.driver.ex_get_event('12345670')
+        self.assertEqual(action["status"], "OK")
+        self.assertEqual(action["event"]["id"], 12345670)
+        self.assertEqual(action["event"]["event_type_id"], 1)
+
+    def test__paginated_request(self):
+        self.assertRaises(NotImplementedError, self.driver._paginated_request, '/v1/anything')
+
+
+class DigitalOceanMockHttp(MockHttpTestCase):
+    fixtures = FileFixtures('common', 'digitalocean')
+
+    response = {
+        None: httplib.OK,
+        'CREATE': httplib.CREATED,
+        'DELETE': httplib.NO_CONTENT,
+	'EMPTY': httplib.OK,
+        'NOT_FOUND': httplib.NOT_FOUND,
+        'UNAUTHORIZED': httplib.UNAUTHORIZED,
+        'UPDATE': httplib.OK
+    }
+
+    def _v1_events_12345670_UNAUTHORIZED(self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v1_events_12345670_UNAUTHORIZED.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v1_events_12345670(self, method, url, body, headers):
+        body = self.fixtures.load('_v1_events_12345670.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/common/test_digitalocean_v2.py
----------------------------------------------------------------------
diff --git a/libcloud/test/common/test_digitalocean_v2.py b/libcloud/test/common/test_digitalocean_v2.py
new file mode 100644
index 0000000..c530b5b
--- /dev/null
+++ b/libcloud/test/common/test_digitalocean_v2.py
@@ -0,0 +1,105 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+
+import os
+import sys
+import unittest
+
+from libcloud.common.types import InvalidCredsError
+from libcloud.common.digitalocean import DigitalOceanBaseDriver
+from libcloud.dns.types import RecordType
+from libcloud.test import LibcloudTestCase, MockHttpTestCase
+from libcloud.test.file_fixtures import FileFixtures
+from libcloud.test.secrets import DIGITALOCEAN_v2_PARAMS
+from libcloud.utils.py3 import httplib
+
+
+class DigitalOceanTests(LibcloudTestCase):
+
+    def setUp(self):
+        DigitalOceanBaseDriver.connectionCls.conn_classes = \
+            (None, DigitalOceanMockHttp)
+        DigitalOceanMockHttp.type = None
+        self.driver = DigitalOceanBaseDriver(*DIGITALOCEAN_v2_PARAMS)
+
+    def test_authentication(self):
+        DigitalOceanMockHttp.type = 'UNAUTHORIZED'
+        self.assertRaises(InvalidCredsError, self.driver.ex_account_info)
+
+    def test_ex_account_info(self):
+        account_info = self.driver.ex_account_info()
+        self.assertEqual(account_info['uuid'], 'a1234567890b1234567890c1234567890d12345')
+        self.assertTrue(account_info['email_verified'])
+        self.assertEqual(account_info['email'], 'user@domain.tld')
+        self.assertEqual(account_info['droplet_limit'], 10)
+
+    def test_ex_list_events(self):
+        events = self.driver.ex_list_events()
+        self.assertEqual(events, [])
+
+    def test_ex_get_event(self):
+        action = self.driver.ex_get_event('12345670')
+        self.assertEqual(action["id"], 12345670)
+        self.assertEqual(action["status"], "completed")
+        self.assertEqual(action["type"], "power_on")
+
+    def test__paginated_request(self):
+        DigitalOceanMockHttp.type = 'page_1'
+        actions = self.driver._paginated_request('/v2/actions', 'actions')
+        self.assertEqual(actions[0]['id'], 12345671)
+        self.assertEqual(actions[0]['status'], 'completed')
+
+
+class DigitalOceanMockHttp(MockHttpTestCase):
+    fixtures = FileFixtures('common', 'digitalocean')
+
+    response = {
+        None: httplib.OK,
+        'CREATE': httplib.CREATED,
+        'DELETE': httplib.NO_CONTENT,
+	'EMPTY': httplib.OK,
+        'NOT_FOUND': httplib.NOT_FOUND,
+        'UNAUTHORIZED': httplib.UNAUTHORIZED,
+        'UPDATE': httplib.OK
+    }
+
+    def _v2_account(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_account.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_account_UNAUTHORIZED(self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_account_UNAUTHORIZED.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_actions(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_actions.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_actions_12345670(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_actions_12345670.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_actions_page_1(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_actions_page_1.json')
+        return (self.response[None], body, {},
+                    httplib.responses[self.response[None]])
+
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains.json
new file mode 100644
index 0000000..f00d3a3
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains.json
@@ -0,0 +1 @@
+{"domains":[{"name":"testdomain","ttl":1800,"zone_file":"$ORIGIN testdomain.\n$TTL 1800\ntestdomain. IN SOA ns1.digitalocean.com. hostmaster.testdomain. 1428768671 10800 3600 604800 1800\ntestdomain. 1800 IN NS ns1.digitalocean.com.\ntestdomain. 1800 IN NS ns2.digitalocean.com.\ntestdomain. 1800 IN NS ns3.digitalocean.com.\ntestdomain. 1800 IN A 123.45.67.89\n"}],"links":{},"meta":{"total":1}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_CREATE.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_CREATE.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_CREATE.json
new file mode 100644
index 0000000..b5b37f0
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_CREATE.json
@@ -0,0 +1 @@
+{"domain":{"name":"testdomain","ttl":null,"zone_file":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_EMPTY.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_EMPTY.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_EMPTY.json
new file mode 100644
index 0000000..2b7209c
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_EMPTY.json
@@ -0,0 +1 @@
+{"domains":[],"links":{},"meta":{"total":0}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_UNAUTHORIZED.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_UNAUTHORIZED.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_UNAUTHORIZED.json
new file mode 100644
index 0000000..415d9ef
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_UNAUTHORIZED.json
@@ -0,0 +1 @@
+{"id":"unauthorized","message":"Unable to authenticate you."}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain.json
new file mode 100644
index 0000000..cf6cf37
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain.json
@@ -0,0 +1 @@
+{"domain":{"name":"testdomain","ttl":1800,"zone_file":"$ORIGIN testdomain.\n$TTL 1800\ntestdomain. IN SOA ns1.digitalocean.com. hostmaster.testdomain. 1428768671 10800 3600 604800 1800\ntestdomain. 1800 IN NS ns1.digitalocean.com.\ntestdomain. 1800 IN NS ns2.digitalocean.com.\ntestdomain. 1800 IN NS ns3.digitalocean.com.\ntestdomain. 1800 IN A 123.45.67.89\n"}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_NOT_FOUND.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_NOT_FOUND.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_NOT_FOUND.json
new file mode 100644
index 0000000..fa61040
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_NOT_FOUND.json
@@ -0,0 +1 @@
+{"id":"not_found","message":"The resource you were accessing could not be found."}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records.json
new file mode 100644
index 0000000..53093f6
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records.json
@@ -0,0 +1 @@
+{"domain_records":[{"id":1234560,"type":"NS","name":"@","data":"ns1.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1234561,"type":"NS","name":"@","data":"ns2.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1234562,"type":"NS","name":"@","data":"ns3.digitalocean.com","priority":null,"port":null,"weight":null},{"id":1234564,"type":"A","name":"@","data":"123.45.67.89","priority":null,"port":null,"weight":null}],"links":{},"meta":{"total":4}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234560.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234560.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234560.json
new file mode 100644
index 0000000..4e629dd
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234560.json
@@ -0,0 +1 @@
+{"domain_record":{"id":1234560,"type":"NS","name":"@","data":"ns1.digitalocean.com","priority":null,"port":null,"weight":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234561.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234561.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234561.json
new file mode 100644
index 0000000..4218337
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234561.json
@@ -0,0 +1 @@
+{"domain_record":{"id":1234561,"type":"NS","name":"@","data":"ns2.digitalocean.com","priority":null,"port":null,"weight":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234562.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234562.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234562.json
new file mode 100644
index 0000000..307218d
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234562.json
@@ -0,0 +1 @@
+{"domain_record":{"id":1234563,"type":"NS","name":"@","data":"ns3.digitalocean.com","priority":null,"port":null,"weight":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564.json
new file mode 100644
index 0000000..8d34cca
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564.json
@@ -0,0 +1 @@
+{"domain_record":{"id":1234564,"type":"A","name":"@","data":"123.45.67.89","priority":null,"port":null,"weight":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_NOT_FOUND.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_NOT_FOUND.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_NOT_FOUND.json
new file mode 100644
index 0000000..fa61040
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_NOT_FOUND.json
@@ -0,0 +1 @@
+{"id":"not_found","message":"The resource you were accessing could not be found."}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_UPDATE.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_UPDATE.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_UPDATE.json
new file mode 100644
index 0000000..8401c43
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_1234564_UPDATE.json
@@ -0,0 +1 @@
+{"domain_record":{"id":1234564,"type":"A","name":"@","data":"234.56.78.90","priority":null,"port":null,"weight":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_CREATE.json
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_CREATE.json b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_CREATE.json
new file mode 100644
index 0000000..4c032ab
--- /dev/null
+++ b/libcloud/test/dns/fixtures/digitalocean/_v2_domains_testdomain_records_CREATE.json
@@ -0,0 +1 @@
+{"domain_record":{"id":1234565,"type":"A","name":"sub","data":"234.56.78.90","priority":null,"port":null,"weight":null}}

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/dns/test_digitalocean.py
----------------------------------------------------------------------
diff --git a/libcloud/test/dns/test_digitalocean.py b/libcloud/test/dns/test_digitalocean.py
new file mode 100644
index 0000000..7557db0
--- /dev/null
+++ b/libcloud/test/dns/test_digitalocean.py
@@ -0,0 +1,214 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+
+import sys
+import unittest
+
+from libcloud.common.types import InvalidCredsError
+from libcloud.dns.drivers.digitalocean import DigitalOceanDNSDriver
+from libcloud.dns.types import RecordType
+from libcloud.test import LibcloudTestCase, MockHttpTestCase
+from libcloud.test.file_fixtures import DNSFileFixtures
+from libcloud.test.secrets import DIGITALOCEAN_v2_PARAMS
+from libcloud.utils.py3 import httplib
+
+
+class DigitalOceanDNSTests(LibcloudTestCase):
+
+    def setUp(self):
+        DigitalOceanDNSDriver.connectionCls.conn_classes = \
+            (None, DigitalOceanDNSMockHttp)
+        DigitalOceanDNSMockHttp.type = None
+        self.driver = DigitalOceanDNSDriver(*DIGITALOCEAN_v2_PARAMS)
+
+    def test_authentication(self):
+        DigitalOceanDNSMockHttp.type = 'UNAUTHORIZED'
+        self.assertRaises(InvalidCredsError, self.driver.list_zones)
+
+    def test_list_zones(self):
+        zones = self.driver.list_zones()
+        self.assertTrue(len(zones) >= 1)
+
+    def test_get_zone(self):
+        zone = self.driver.get_zone('testdomain')
+        self.assertEqual(zone.id, 'testdomain')
+
+    def test_get_zone_not_found(self):
+        DigitalOceanDNSMockHttp.type = 'NOT_FOUND'
+        self.assertRaises(Exception, self.driver.get_zone, 'testdomain')
+
+    def test_list_records(self):
+        zone = self.driver.get_zone('testdomain')
+        records = self.driver.list_records(zone)
+        self.assertTrue(len(records) >= 1)
+
+    def test_get_record(self):
+        record = self.driver.get_record('testdomain', '1234564')
+        self.assertEqual(record.id, '1234564')
+        self.assertEqual(record.type, RecordType.A)
+        self.assertEqual(record.data, '123.45.67.89')
+
+    def test_get_record_not_found(self):
+        DigitalOceanDNSMockHttp.type = 'NOT_FOUND'
+        self.assertRaises(Exception, self.driver.get_zone, 'testdomain')
+
+    def test_create_zone(self):
+        DigitalOceanDNSMockHttp.type = 'CREATE'
+        zone = self.driver.create_zone('testdomain')
+        self.assertEqual(zone.id, 'testdomain')
+
+    def test_create_record(self):
+        zone = self.driver.get_zone('testdomain')
+
+        DigitalOceanDNSMockHttp.type = 'CREATE'
+        record = self.driver.create_record('sub', zone, RecordType.A, '234.56.78.90')
+        self.assertEqual(record.id, '1234565')
+        self.assertEqual(record.type, RecordType.A)
+        self.assertEqual(record.data, '234.56.78.90')
+
+    def test_update_record(self):
+        record = self.driver.get_record('testdomain', '1234564')
+
+        DigitalOceanDNSMockHttp.type = 'UPDATE'
+        record = self.driver.update_record(record, data="234.56.78.90")
+        self.assertEqual(record.id, '1234564')
+        self.assertEqual(record.data, "234.56.78.90")
+
+    def test_delete_zone(self):
+        zone = self.driver.get_zone('testdomain')
+
+        DigitalOceanDNSMockHttp.type = 'DELETE'
+        self.assertTrue(self.driver.delete_zone(zone))
+
+    def test_delete_record(self):
+        record = self.driver.get_record('testdomain', '1234564')
+
+        DigitalOceanDNSMockHttp.type = 'DELETE'
+        self.assertTrue(self.driver.delete_record(record))
+
+
+class DigitalOceanDNSMockHttp(MockHttpTestCase):
+    fixtures = DNSFileFixtures('digitalocean')
+
+    response = {
+        None: httplib.OK,
+        'CREATE': httplib.CREATED,
+        'DELETE': httplib.NO_CONTENT,
+	'EMPTY': httplib.OK,
+        'NOT_FOUND': httplib.NOT_FOUND,
+        'UNAUTHORIZED': httplib.UNAUTHORIZED,
+        'UPDATE': httplib.OK
+    }
+
+    def _v2_domains(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_CREATE(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_CREATE.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_EMPTY(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_EMPTY.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_UNAUTHORIZED(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_UNAUTHORIZED.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_testdomain.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_DELETE(self, method, url, body, headers):
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_NOT_FOUND(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_testdomain_NOT_FOUND.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_testdomain_records.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_CREATE(self, method, url, body, headers):
+        body = self.fixtures.load('_v2_domains_testdomain_records_CREATE.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234560(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234560.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234561(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234561.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234562(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234562.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234563(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234563.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234564(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234564.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234564_DELETE(
+            self, method, url, body, headers):
+        self.type = 'DELETE'
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234564_NOT_FOUND(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234564_NOT_FOUND.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+    def _v2_domains_testdomain_records_1234564_UPDATE(
+            self, method, url, body, headers):
+        body = self.fixtures.load(
+                   '_v2_domains_testdomain_records_1234564_UPDATE.json')
+        return (self.response[self.type], body, {},
+                    httplib.responses[self.response[self.type]])
+
+if __name__ == '__main__':
+    sys.exit(unittest.main())

http://git-wip-us.apache.org/repos/asf/libcloud/blob/a3a5f173/libcloud/test/file_fixtures.py
----------------------------------------------------------------------
diff --git a/libcloud/test/file_fixtures.py b/libcloud/test/file_fixtures.py
index 42e3d36..a09639c 100644
--- a/libcloud/test/file_fixtures.py
+++ b/libcloud/test/file_fixtures.py
@@ -22,6 +22,7 @@ from libcloud.utils.py3 import PY3
 from libcloud.utils.py3 import u
 
 FIXTURES_ROOT = {
+    'common': 'common/fixtures',
     'compute': 'compute/fixtures',
     'storage': 'storage/fixtures',
     'loadbalancer': 'loadbalancer/fixtures',