Revert "Remove vendored libjuju"
This reverts commit 9d18c22a0dc9e295adda50601fc5e2f45d2c9b8a.
Change-Id: I7dbf291ccd750c5f836ff80c642be492434ab3ac
Signed-off-by: Adam Israel <adam.israel@canonical.com>
diff --git a/modules/libjuju/tests/unit/__init__.py b/modules/libjuju/tests/unit/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/modules/libjuju/tests/unit/__init__.py
diff --git a/modules/libjuju/tests/unit/test_client.py b/modules/libjuju/tests/unit/test_client.py
new file mode 100644
index 0000000..1d18bf9
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_client.py
@@ -0,0 +1,26 @@
+"""
+Tests for generated client code
+
+"""
+
+import mock
+from juju.client import client
+
+
+def test_basics():
+ assert client.CLIENTS
+ for i in range(1, 5): # Assert versions 1-4 in client dict
+ assert str(i) in client.CLIENTS
+
+
+def test_from_connection():
+ connection = mock.Mock()
+ connection.facades = {"Action": 2}
+ action_facade = client.ActionFacade.from_connection(connection)
+ assert action_facade
+
+
+def test_to_json():
+ uml = client.UserModelList([client.UserModel()])
+ assert uml.to_json() == ('{"user-models": [{"last-connection": null, '
+ '"model": null}]}')
diff --git a/modules/libjuju/tests/unit/test_connection.py b/modules/libjuju/tests/unit/test_connection.py
new file mode 100644
index 0000000..0925d84
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_connection.py
@@ -0,0 +1,65 @@
+import asyncio
+import json
+from collections import deque
+
+import mock
+from juju.client.connection import Connection
+from websockets.exceptions import ConnectionClosed
+
+import pytest
+
+from .. import base
+
+
+class WebsocketMock:
+ def __init__(self, responses):
+ super().__init__()
+ self.responses = deque(responses)
+ self.open = True
+
+ async def send(self, message):
+ pass
+
+ async def recv(self):
+ if not self.responses:
+ await asyncio.sleep(1) # delay to give test time to finish
+ raise ConnectionClosed(0, 'ran out of responses')
+ return json.dumps(self.responses.popleft())
+
+ async def close(self):
+ self.open = False
+
+
+@pytest.mark.asyncio
+async def test_out_of_order(event_loop):
+ ws = WebsocketMock([
+ {'request-id': 1},
+ {'request-id': 3},
+ {'request-id': 2},
+ ])
+ expected_responses = [
+ {'request-id': 1},
+ {'request-id': 2},
+ {'request-id': 3},
+ ]
+ minimal_facades = [{'name': 'Pinger', 'versions': [1]}]
+ con = None
+ try:
+ with \
+ mock.patch('websockets.connect', base.AsyncMock(return_value=ws)), \
+ mock.patch(
+ 'juju.client.connection.Connection.login',
+ base.AsyncMock(return_value={'response': {
+ 'facades': minimal_facades,
+ }}),
+ ), \
+ mock.patch('juju.client.connection.Connection._get_ssl'), \
+ mock.patch('juju.client.connection.Connection._pinger', base.AsyncMock()):
+ con = await Connection.connect('0.1.2.3:999')
+ actual_responses = []
+ for i in range(3):
+ actual_responses.append(await con.rpc({'version': 1}))
+ assert actual_responses == expected_responses
+ finally:
+ if con:
+ await con.close()
diff --git a/modules/libjuju/tests/unit/test_constraints.py b/modules/libjuju/tests/unit/test_constraints.py
new file mode 100644
index 0000000..3c52090
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_constraints.py
@@ -0,0 +1,57 @@
+#
+# Test our constraints parser
+#
+
+import unittest
+
+from juju import constraints
+
+
+class TestConstraints(unittest.TestCase):
+
+ def test_mem_regex(self):
+ m = constraints.MEM
+ self.assertTrue(m.match("10G"))
+ self.assertTrue(m.match("1G"))
+ self.assertFalse(m.match("1Gb"))
+ self.assertFalse(m.match("a1G"))
+ self.assertFalse(m.match("1000"))
+
+ def test_normalize_key(self):
+ _ = constraints.normalize_key
+
+ self.assertEqual(_("test-key"), "test_key")
+ self.assertEqual(_("test-key "), "test_key")
+ self.assertEqual(_(" test-key"), "test_key")
+ self.assertEqual(_("TestKey"), "test_key")
+ self.assertEqual(_("testKey"), "test_key")
+
+ def test_normalize_val(self):
+ _ = constraints.normalize_value
+
+ self.assertEqual(_("10G"), 10 * 1024)
+ self.assertEqual(_("10M"), 10)
+ self.assertEqual(_("10"), 10)
+ self.assertEqual(_("foo,bar"), "foo,bar")
+
+ def test_normalize_list_val(self):
+ _ = constraints.normalize_list_value
+
+ self.assertEqual(_("foo"), ["foo"])
+ self.assertEqual(_("foo,bar"), ["foo", "bar"])
+
+ def test_parse_constraints(self):
+ _ = constraints.parse
+
+ self.assertEqual(
+ _("mem=10G"),
+ {"mem": 10 * 1024}
+ )
+
+ self.assertEqual(
+ _("mem=10G foo=bar,baz tags=tag1 spaces=space1,space2"),
+ {"mem": 10 * 1024,
+ "foo": "bar,baz",
+ "tags": ["tag1"],
+ "spaces": ["space1", "space2"]}
+ )
diff --git a/modules/libjuju/tests/unit/test_controller.py b/modules/libjuju/tests/unit/test_controller.py
new file mode 100644
index 0000000..b95b5ee
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_controller.py
@@ -0,0 +1,140 @@
+import asynctest
+import mock
+from pathlib import Path
+from tempfile import NamedTemporaryFile
+
+from juju.controller import Controller
+from juju.client import client
+
+from .. import base
+
+
+class TestControllerConnect(asynctest.TestCase):
+ @asynctest.patch('juju.client.connector.Connector.connect_controller')
+ async def test_no_args(self, mock_connect_controller):
+ c = Controller()
+ await c.connect()
+ mock_connect_controller.assert_called_once_with(None)
+
+ @asynctest.patch('juju.client.connector.Connector.connect_controller')
+ async def test_with_controller_name(self, mock_connect_controller):
+ c = Controller()
+ await c.connect(controller_name='foo')
+ mock_connect_controller.assert_called_once_with('foo')
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_no_auth(self, mock_connect):
+ c = Controller()
+ with self.assertRaises(TypeError):
+ await c.connect(endpoint='0.1.2.3:4566')
+ self.assertEqual(mock_connect.call_count, 0)
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_userpass(self, mock_connect):
+ c = Controller()
+ with self.assertRaises(TypeError):
+ await c.connect(endpoint='0.1.2.3:4566', username='dummy')
+ await c.connect(endpoint='0.1.2.3:4566',
+ username='user',
+ password='pass')
+ mock_connect.assert_called_once_with(endpoint='0.1.2.3:4566',
+ username='user',
+ password='pass')
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_bakery_client(self, mock_connect):
+ c = Controller()
+ await c.connect(endpoint='0.1.2.3:4566', bakery_client='bakery')
+ mock_connect.assert_called_once_with(endpoint='0.1.2.3:4566',
+ bakery_client='bakery')
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_macaroons(self, mock_connect):
+ c = Controller()
+ await c.connect(endpoint='0.1.2.3:4566',
+ macaroons=['macaroon'])
+ mock_connect.assert_called_with(endpoint='0.1.2.3:4566',
+ macaroons=['macaroon'])
+ await c.connect(endpoint='0.1.2.3:4566',
+ bakery_client='bakery',
+ macaroons=['macaroon'])
+ mock_connect.assert_called_with(endpoint='0.1.2.3:4566',
+ bakery_client='bakery',
+ macaroons=['macaroon'])
+
+ @asynctest.patch('juju.client.connector.Connector.connect_controller')
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_posargs(self, mock_connect, mock_connect_controller):
+ c = Controller()
+ await c.connect('foo')
+ mock_connect_controller.assert_called_once_with('foo')
+ with self.assertRaises(TypeError):
+ await c.connect('endpoint', 'user')
+ await c.connect('endpoint', 'user', 'pass')
+ mock_connect.assert_called_once_with(endpoint='endpoint',
+ username='user',
+ password='pass')
+ await c.connect('endpoint', 'user', 'pass', 'cacert', 'bakery',
+ 'macaroons', 'loop', 'max_frame_size')
+ mock_connect.assert_called_with(endpoint='endpoint',
+ username='user',
+ password='pass',
+ cacert='cacert',
+ bakery_client='bakery',
+ macaroons='macaroons',
+ loop='loop',
+ max_frame_size='max_frame_size')
+
+ @asynctest.patch('juju.client.client.CloudFacade')
+ async def test_file_cred_v2(self, mock_cf):
+ with NamedTemporaryFile() as tempfile:
+ tempfile.close()
+ temppath = Path(tempfile.name)
+ temppath.write_text('cred-test')
+ cred = client.CloudCredential(auth_type='jsonfile',
+ attrs={'file': tempfile.name})
+ jujudata = mock.MagicMock()
+ c = Controller(jujudata=jujudata)
+ c._connector = base.AsyncMock()
+ up_creds = base.AsyncMock()
+ cloud_facade = mock_cf.from_connection()
+ cloud_facade.version = 2
+ cloud_facade.UpdateCredentials = up_creds
+ await c.add_credential(
+ name='name',
+ credential=cred,
+ cloud='cloud',
+ owner='owner',
+ )
+ assert up_creds.called
+ new_cred = up_creds.call_args[0][0][0].credential
+ assert cred.attrs['file'] == tempfile.name
+ assert new_cred.attrs['file'] == 'cred-test'
+
+ @asynctest.patch('juju.client.client.CloudFacade')
+ async def test_file_cred_v3(self, mock_cf):
+ with NamedTemporaryFile() as tempfile:
+ tempfile.close()
+ temppath = Path(tempfile.name)
+ temppath.write_text('cred-test')
+ cred = client.CloudCredential(auth_type='jsonfile',
+ attrs={'file': tempfile.name})
+ jujudata = mock.MagicMock()
+ c = Controller(jujudata=jujudata)
+ c._connector = base.AsyncMock()
+ up_creds = base.AsyncMock()
+ cloud_facade = mock_cf.from_connection()
+ cloud_facade.version = 3
+ cloud_facade.UpdateCredentialsCheckModels = up_creds
+ await c.add_credential(
+ name='name',
+ credential=cred,
+ cloud='cloud',
+ owner='owner',
+ force=True,
+ )
+ assert up_creds.called
+ assert up_creds.call_args[1]['force']
+ new_cred = up_creds.call_args[1]['credentials'][0].credential
+ assert cred.attrs['file'] == tempfile.name
+ assert new_cred.attrs['file'] == 'cred-test'
diff --git a/modules/libjuju/tests/unit/test_gocookies.py b/modules/libjuju/tests/unit/test_gocookies.py
new file mode 100644
index 0000000..033a0e9
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_gocookies.py
@@ -0,0 +1,244 @@
+"""
+Tests for the gocookies code.
+"""
+import os
+import shutil
+import tempfile
+import unittest
+import urllib.request
+
+import pyrfc3339
+from juju.client.gocookies import GoCookieJar
+
+# cookie_content holds the JSON contents of a Go-produced
+# cookie file (reformatted so it's not all on one line but
+# otherwise unchanged).
+cookie_content = """
+[
+ {
+ "CanonicalHost": "bar.com",
+ "Creation": "2017-11-17T08:53:55.088820092Z",
+ "Domain": "bar.com",
+ "Expires": "2345-11-15T18:16:08Z",
+ "HostOnly": true,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088822562Z",
+ "Name": "bar",
+ "Path": "/",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088822562Z",
+ "Value": "bar-value"
+ },
+ {
+ "CanonicalHost": "x.foo.com",
+ "Creation": "2017-11-17T08:53:55.088814857Z",
+ "Domain": "x.foo.com",
+ "Expires": "2345-11-15T18:16:05Z",
+ "HostOnly": true,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088884015Z",
+ "Name": "foo",
+ "Path": "/path",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088814857Z",
+ "Value": "foo-path-value"
+ },
+ {
+ "CanonicalHost": "x.foo.com",
+ "Creation": "2017-11-17T08:53:55.088814857Z",
+ "Domain": "foo.com",
+ "Expires": "2345-11-15T18:16:06Z",
+ "HostOnly": false,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088919437Z",
+ "Name": "foo4",
+ "Path": "/path",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088814857Z",
+ "Value": "foo4-value"
+ },
+ {
+ "CanonicalHost": "x.foo.com",
+ "Creation": "2017-11-17T08:53:55.088790709Z",
+ "Domain": "x.foo.com",
+ "Expires": "2345-11-15T18:16:01Z",
+ "HostOnly": true,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088884015Z",
+ "Name": "foo",
+ "Path": "/",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088790709Z",
+ "Value": "foo-value"
+ },
+ {
+ "CanonicalHost": "x.foo.com",
+ "Creation": "2017-11-17T08:53:55.088790709Z",
+ "Domain": "foo.com",
+ "Expires": "2345-11-15T18:16:02Z",
+ "HostOnly": false,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088919437Z",
+ "Name": "foo1",
+ "Path": "/",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088790709Z",
+ "Value": "foo1-value"
+ },
+ {
+ "CanonicalHost": "x.foo.com",
+ "Creation": "2017-11-17T08:53:55.088790709Z",
+ "Domain": "x.foo.com",
+ "Expires": "2345-11-15T18:16:03Z",
+ "HostOnly": true,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088850252Z",
+ "Name": "foo2",
+ "Path": "/",
+ "Persistent": true,
+ "Secure": true,
+ "Updated": "2017-11-17T08:53:55.088790709Z",
+ "Value": "foo2-value"
+ },
+ {
+ "CanonicalHost": "x.foo.com",
+ "Creation": "2017-11-17T08:53:55.088790709Z",
+ "Domain": "foo.com",
+ "Expires": "2345-11-15T18:16:04Z",
+ "HostOnly": false,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088919437Z",
+ "Name": "foo3",
+ "Path": "/",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088790709Z",
+ "Value": "foo3-value"
+ }
+]
+"""
+
+# cookie_content_queries holds a set of queries
+# that were automatically generated by running
+# the queries on the above cookie_content data
+# and printing the results.
+cookie_content_queries = [
+ ('http://x.foo.com', [
+ ('foo', 'foo-value'),
+ ('foo1', 'foo1-value'),
+ ('foo3', 'foo3-value'),
+ ]),
+ ('https://x.foo.com', [
+ ('foo', 'foo-value'),
+ ('foo1', 'foo1-value'),
+ ('foo2', 'foo2-value'),
+ ('foo3', 'foo3-value'),
+ ]),
+ ('http://arble.foo.com', [
+ ('foo1', 'foo1-value'),
+ ('foo3', 'foo3-value'),
+ ]),
+ ('http://arble.com', [
+ ]),
+ ('http://x.foo.com/path/x', [
+ ('foo', 'foo-path-value'),
+ ('foo4', 'foo4-value'),
+ ('foo', 'foo-value'),
+ ('foo1', 'foo1-value'),
+ ('foo3', 'foo3-value'),
+ ]),
+ ('http://arble.foo.com/path/x', [
+ ('foo4', 'foo4-value'),
+ ('foo1', 'foo1-value'),
+ ('foo3', 'foo3-value'),
+ ]),
+ ('http://foo.com/path/x', [
+ ('foo4', 'foo4-value'),
+ ('foo1', 'foo1-value'),
+ ('foo3', 'foo3-value'),
+ ]),
+]
+
+
+class TestGoCookieJar(unittest.TestCase):
+ def setUp(self):
+ self.dir = tempfile.mkdtemp()
+
+ def tearDown(self):
+ shutil.rmtree(self.dir)
+
+ def test_readcookies(self):
+ jar = self.load_jar(cookie_content)
+ self.assert_jar_queries(jar, cookie_content_queries)
+
+ def test_roundtrip(self):
+ jar = self.load_jar(cookie_content)
+ filename2 = os.path.join(self.dir, 'cookies2')
+ jar.save(filename=filename2)
+ jar = GoCookieJar()
+ jar.load(filename=filename2)
+ self.assert_jar_queries(jar, cookie_content_queries)
+
+ def test_expiry_time(self):
+ content = '''[
+ {
+ "CanonicalHost": "bar.com",
+ "Creation": "2017-11-17T08:53:55.088820092Z",
+ "Domain": "bar.com",
+ "Expires": "2345-11-15T18:16:08Z",
+ "HostOnly": true,
+ "HttpOnly": false,
+ "LastAccess": "2017-11-17T08:53:55.088822562Z",
+ "Name": "bar",
+ "Path": "/",
+ "Persistent": true,
+ "Secure": false,
+ "Updated": "2017-11-17T08:53:55.088822562Z",
+ "Value": "bar-value"
+ }
+ ]'''
+ jar = self.load_jar(content)
+ got_expires = tuple(jar)[0].expires
+ want_expires = int(pyrfc3339.parse('2345-11-15T18:16:08Z').timestamp())
+ self.assertEqual(got_expires, want_expires)
+
+ def load_jar(self, content):
+ filename = os.path.join(self.dir, 'cookies')
+ with open(filename, 'x') as f:
+ f.write(content)
+ jar = GoCookieJar()
+ jar.load(filename=filename)
+ return jar
+
+ def assert_jar_queries(self, jar, queries):
+ '''Assert that all the given queries (see cookie_content_queries)
+ are satisfied when run on the given cookie jar.
+ :param jar CookieJar: the cookie jar to query
+ :param queries: the queries to run.
+ '''
+ for url, want_cookies in queries:
+ req = urllib.request.Request(url)
+ jar.add_cookie_header(req)
+ # We can't use SimpleCookie to find out what cookies
+ # have been presented, because SimpleCookie
+ # only allows one cookie with a given name,
+ # so we naively parse the cookies ourselves, which
+ # is OK because we know we don't have to deal
+ # with any complex cases.
+
+ cookie_header = req.get_header('Cookie')
+ got_cookies = []
+ if cookie_header is not None:
+ got_cookies = [
+ tuple(part.split('='))
+ for part in cookie_header.split('; ')
+ ]
+ got_cookies.sort()
+ want_cookies = list(want_cookies)
+ want_cookies.sort()
+ self.assertEqual(got_cookies, want_cookies, msg='query {}; got {}; want {}'.format(url, got_cookies, want_cookies))
diff --git a/modules/libjuju/tests/unit/test_loop.py b/modules/libjuju/tests/unit/test_loop.py
new file mode 100644
index 0000000..9043df6
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_loop.py
@@ -0,0 +1,32 @@
+import asyncio
+import unittest
+
+import juju.loop
+
+
+class TestLoop(unittest.TestCase):
+ def setUp(self):
+ # new event loop for each test
+ policy = asyncio.get_event_loop_policy()
+ self.loop = policy.new_event_loop()
+ policy.set_event_loop(self.loop)
+
+ def tearDown(self):
+ self.loop.close()
+
+ def test_run(self):
+ assert asyncio.get_event_loop() == self.loop
+
+ async def _test():
+ return 'success'
+ self.assertEqual(juju.loop.run(_test()), 'success')
+
+ def test_run_interrupt(self):
+ async def _test():
+ juju.loop.run._sigint = True
+ self.assertRaises(KeyboardInterrupt, juju.loop.run, _test())
+
+ def test_run_exception(self):
+ async def _test():
+ raise ValueError()
+ self.assertRaises(ValueError, juju.loop.run, _test())
diff --git a/modules/libjuju/tests/unit/test_model.py b/modules/libjuju/tests/unit/test_model.py
new file mode 100644
index 0000000..2753d85
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_model.py
@@ -0,0 +1,264 @@
+import unittest
+
+import mock
+
+import asynctest
+
+from juju.client.jujudata import FileJujuData
+from juju.model import Model
+
+
+def _make_delta(entity, type_, data=None):
+ from juju.client.client import Delta
+ from juju.delta import get_entity_delta
+
+ delta = Delta([entity, type_, data])
+ return get_entity_delta(delta)
+
+
+class TestObserver(unittest.TestCase):
+ def _make_observer(self, *args):
+ from juju.model import _Observer
+ return _Observer(*args)
+
+ def test_cares_about_id(self):
+ id_ = 'foo'
+
+ o = self._make_observer(
+ None, None, None, id_, None)
+
+ delta = _make_delta(
+ 'application', 'change', dict(name=id_))
+
+ self.assertTrue(o.cares_about(delta))
+
+ def test_cares_about_type(self):
+ type_ = 'application'
+
+ o = self._make_observer(
+ None, type_, None, None, None)
+
+ delta = _make_delta(
+ type_, 'change', dict(name='foo'))
+
+ self.assertTrue(o.cares_about(delta))
+
+ def test_cares_about_action(self):
+ action = 'change'
+
+ o = self._make_observer(
+ None, None, action, None, None)
+
+ delta = _make_delta(
+ 'application', action, dict(name='foo'))
+
+ self.assertTrue(o.cares_about(delta))
+
+ def test_cares_about_predicate(self):
+ def predicate(delta):
+ return delta.data.get('fizz') == 'bang'
+
+ o = self._make_observer(
+ None, None, None, None, predicate)
+
+ delta = _make_delta(
+ 'application', 'change', dict(fizz='bang'))
+
+ self.assertTrue(o.cares_about(delta))
+
+
+class TestModelState(unittest.TestCase):
+ def test_apply_delta(self):
+ from juju.model import Model
+ from juju.application import Application
+
+ model = Model()
+ model._connector = mock.MagicMock()
+ delta = _make_delta('application', 'add', dict(name='foo'))
+
+ # test add
+ prev, new = model.state.apply_delta(delta)
+ self.assertEqual(
+ len(model.state.state[delta.entity][delta.get_id()]), 1)
+ self.assertIsNone(prev)
+ self.assertIsInstance(new, Application)
+
+ # test remove
+ delta.type = 'remove'
+ prev, new = model.state.apply_delta(delta)
+ # length of the entity history deque is now 3:
+ # - 1 for the first delta
+ # - 1 for the second delta
+ # - 1 for the None sentinel appended after the 'remove'
+ self.assertEqual(
+ len(model.state.state[delta.entity][delta.get_id()]), 3)
+ self.assertIsInstance(new, Application)
+ # new object is falsy because its data is None
+ self.assertFalse(new)
+ self.assertIsInstance(prev, Application)
+ self.assertTrue(prev)
+
+
+def test_get_series():
+ from juju.model import Model
+ model = Model()
+ entity = {
+ 'Meta': {
+ 'supported-series': {
+ 'SupportedSeries': [
+ 'xenial',
+ 'trusty',
+ ],
+ },
+ },
+ }
+ assert model._get_series('cs:trusty/ubuntu', entity) == 'trusty'
+ assert model._get_series('xenial/ubuntu', entity) == 'xenial'
+ assert model._get_series('~foo/xenial/ubuntu', entity) == 'xenial'
+ assert model._get_series('~foo/ubuntu', entity) == 'xenial'
+ assert model._get_series('ubuntu', entity) == 'xenial'
+ assert model._get_series('cs:ubuntu', entity) == 'xenial'
+
+
+class TestContextManager(asynctest.TestCase):
+ @asynctest.patch('juju.model.Model.disconnect')
+ @asynctest.patch('juju.model.Model.connect')
+ async def test_normal_use(self, mock_connect, mock_disconnect):
+ from juju.model import Model
+
+ async with Model() as model:
+ self.assertTrue(isinstance(model, Model))
+
+ self.assertTrue(mock_connect.called)
+ self.assertTrue(mock_disconnect.called)
+
+ @asynctest.patch('juju.model.Model.disconnect')
+ @asynctest.patch('juju.model.Model.connect')
+ async def test_exception(self, mock_connect, mock_disconnect):
+ from juju.model import Model
+
+ class SomeException(Exception):
+ pass
+
+ with self.assertRaises(SomeException):
+ async with Model():
+ raise SomeException()
+
+ self.assertTrue(mock_connect.called)
+ self.assertTrue(mock_disconnect.called)
+
+ async def test_no_current_connection(self):
+ from juju.model import Model
+ from juju.errors import JujuConnectionError
+
+ class NoControllerJujuData(FileJujuData):
+ def current_controller(self):
+ return ""
+
+ with self.assertRaises(JujuConnectionError):
+ async with Model(jujudata=NoControllerJujuData()):
+ pass
+
+
+@asynctest.patch('juju.model.Model._after_connect')
+class TestModelConnect(asynctest.TestCase):
+ @asynctest.patch('juju.client.connector.Connector.connect_model')
+ async def test_no_args(self, mock_connect_model, _):
+ m = Model()
+ await m.connect()
+ mock_connect_model.assert_called_once_with(None)
+
+ @asynctest.patch('juju.client.connector.Connector.connect_model')
+ async def test_with_model_name(self, mock_connect_model, _):
+ m = Model()
+ await m.connect(model_name='foo')
+ mock_connect_model.assert_called_once_with('foo')
+
+ @asynctest.patch('juju.client.connector.Connector.connect_model')
+ async def test_with_endpoint_but_no_uuid(self, mock_connect_model, _):
+ m = Model()
+ with self.assertRaises(TypeError):
+ await m.connect(endpoint='0.1.2.3:4566')
+ self.assertEqual(mock_connect_model.call_count, 0)
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_uuid_no_auth(self, mock_connect, _):
+ m = Model()
+ with self.assertRaises(TypeError):
+ await m.connect(endpoint='0.1.2.3:4566', uuid='some-uuid')
+ self.assertEqual(mock_connect.call_count, 0)
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_uuid_with_userpass(self, mock_connect, _):
+ m = Model()
+ with self.assertRaises(TypeError):
+ await m.connect(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ username='user')
+ await m.connect(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ username='user',
+ password='pass')
+ mock_connect.assert_called_once_with(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ username='user',
+ password='pass')
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_uuid_with_bakery(self, mock_connect, _):
+ m = Model()
+ await m.connect(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ bakery_client='bakery')
+ mock_connect.assert_called_once_with(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ bakery_client='bakery')
+
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_endpoint_and_uuid_with_macaroon(self, mock_connect, _):
+ m = Model()
+ with self.assertRaises(TypeError):
+ await m.connect(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ username='user')
+ await m.connect(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ macaroons=['macaroon'])
+ mock_connect.assert_called_with(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ macaroons=['macaroon'])
+ await m.connect(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ bakery_client='bakery',
+ macaroons=['macaroon'])
+ mock_connect.assert_called_with(endpoint='0.1.2.3:4566',
+ uuid='some-uuid',
+ bakery_client='bakery',
+ macaroons=['macaroon'])
+
+ @asynctest.patch('juju.client.connector.Connector.connect_model')
+ @asynctest.patch('juju.client.connector.Connector.connect')
+ async def test_with_posargs(self, mock_connect, mock_connect_model, _):
+ m = Model()
+ await m.connect('foo')
+ mock_connect_model.assert_called_once_with('foo')
+ with self.assertRaises(TypeError):
+ await m.connect('endpoint', 'uuid')
+ with self.assertRaises(TypeError):
+ await m.connect('endpoint', 'uuid', 'user')
+ await m.connect('endpoint', 'uuid', 'user', 'pass')
+ mock_connect.assert_called_once_with(endpoint='endpoint',
+ uuid='uuid',
+ username='user',
+ password='pass')
+ await m.connect('endpoint', 'uuid', 'user', 'pass', 'cacert', 'bakery',
+ 'macaroons', 'loop', 'max_frame_size')
+ mock_connect.assert_called_with(endpoint='endpoint',
+ uuid='uuid',
+ username='user',
+ password='pass',
+ cacert='cacert',
+ bakery_client='bakery',
+ macaroons='macaroons',
+ loop='loop',
+ max_frame_size='max_frame_size')
diff --git a/modules/libjuju/tests/unit/test_overrides.py b/modules/libjuju/tests/unit/test_overrides.py
new file mode 100644
index 0000000..a5835ff
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_overrides.py
@@ -0,0 +1,76 @@
+from juju.client.overrides import Binary, Number # noqa
+
+import pytest
+
+
+# test cases ported from:
+# https://github.com/juju/version/blob/master/version_test.go
+@pytest.mark.parametrize("input,expected", (
+ (None, Number(major=0, minor=0, patch=0, tag='', build=0)),
+ (Number(major=1, minor=0, patch=0), Number(major=1, minor=0, patch=0)),
+ ({'major': 1, 'minor': 0, 'patch': 0}, Number(major=1, minor=0, patch=0)),
+ ("0.0.1", Number(major=0, minor=0, patch=1)),
+ ("0.0.2", Number(major=0, minor=0, patch=2)),
+ ("0.1.0", Number(major=0, minor=1, patch=0)),
+ ("0.2.3", Number(major=0, minor=2, patch=3)),
+ ("1.0.0", Number(major=1, minor=0, patch=0)),
+ ("10.234.3456", Number(major=10, minor=234, patch=3456)),
+ ("10.234.3456.1", Number(major=10, minor=234, patch=3456, build=1)),
+ ("10.234.3456.64", Number(major=10, minor=234, patch=3456, build=64)),
+ ("10.235.3456", Number(major=10, minor=235, patch=3456)),
+ ("1.21-alpha1", Number(major=1, minor=21, patch=1, tag="alpha")),
+ ("1.21-alpha1.1", Number(major=1, minor=21, patch=1, tag="alpha",
+ build=1)),
+ ("1.21-alpha10", Number(major=1, minor=21, patch=10, tag="alpha")),
+ ("1.21.0", Number(major=1, minor=21)),
+ ("1234567890.2.1", TypeError),
+ ("0.2..1", TypeError),
+ ("1.21.alpha1", TypeError),
+ ("1.21-alpha", TypeError),
+ ("1.21-alpha1beta", TypeError),
+ ("1.21-alpha-dev", TypeError),
+ ("1.21-alpha_dev3", TypeError),
+ ("1.21-alpha123dev3", TypeError),
+))
+def test_number(input, expected):
+ if expected is TypeError:
+ with pytest.raises(expected):
+ Number.from_json(input)
+ else:
+ result = Number.from_json(input)
+ assert result == expected
+ if isinstance(input, str):
+ assert result.to_json() == input
+
+
+# test cases ported from:
+# https://github.com/juju/version/blob/master/version_test.go
+@pytest.mark.parametrize("input,expected", (
+ (None, Binary(Number(), None, None)),
+ (Binary(Number(1), 'trusty', 'amd64'), Binary(Number(1),
+ 'trusty', 'amd64')),
+ ({'number': {'major': 1},
+ 'series': 'trusty',
+ 'arch': 'amd64'}, Binary(Number(1), 'trusty', 'amd64')),
+ ("1.2.3-trusty-amd64", Binary(Number(1, 2, 3, "", 0),
+ "trusty", "amd64")),
+ ("1.2.3.4-trusty-amd64", Binary(Number(1, 2, 3, "", 4),
+ "trusty", "amd64")),
+ ("1.2-alpha3-trusty-amd64", Binary(Number(1, 2, 3, "alpha", 0),
+ "trusty", "amd64")),
+ ("1.2-alpha3.4-trusty-amd64", Binary(Number(1, 2, 3, "alpha", 4),
+ "trusty", "amd64")),
+ ("1.2.3", TypeError),
+ ("1.2-beta1", TypeError),
+ ("1.2.3--amd64", TypeError),
+ ("1.2.3-trusty-", TypeError),
+))
+def test_binary(input, expected):
+ if expected is TypeError:
+ with pytest.raises(expected):
+ Binary.from_json(input)
+ else:
+ result = Binary.from_json(input)
+ assert result == expected
+ if isinstance(input, str):
+ assert result.to_json() == input
diff --git a/modules/libjuju/tests/unit/test_placement.py b/modules/libjuju/tests/unit/test_placement.py
new file mode 100644
index 0000000..5a933ec
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_placement.py
@@ -0,0 +1,20 @@
+#
+# Test our placement helper
+#
+
+import unittest
+
+from juju import placement
+
+
+class TestPlacement(unittest.TestCase):
+
+ def test_parse_both_specified(self):
+ res = placement.parse("foo:bar")
+ self.assertEqual(res[0].scope, "foo")
+ self.assertEqual(res[0].directive, "bar")
+
+ def test_parse_machine(self):
+ res = placement.parse("22")
+ self.assertEqual(res[0].scope, "#")
+ self.assertEqual(res[0].directive, "22")
diff --git a/modules/libjuju/tests/unit/test_registration_string.py b/modules/libjuju/tests/unit/test_registration_string.py
new file mode 100644
index 0000000..f4fea44
--- /dev/null
+++ b/modules/libjuju/tests/unit/test_registration_string.py
@@ -0,0 +1,18 @@
+#
+# Test our placement helper
+#
+
+import unittest
+
+from juju.utils import generate_user_controller_access_token
+
+
+class TestRegistrationString(unittest.TestCase):
+ def test_generate_user_controller_access_token(self):
+ controller_name = "localhost-localhost"
+ endpoints = ["192.168.1.1:17070", "192.168.1.2:17070", "192.168.1.3:17070"]
+ username = "test-01234"
+ secret_key = "paNZrqOw51ONk1kTER6rkm4hdPcg5VgC/dzXYxtUZaM="
+ reg_string = generate_user_controller_access_token(username, endpoints, secret_key, controller_name)
+ assert reg_string == b"MH4TCnRlc3QtMDEyMzQwORMRMTkyLjE2OC4xLjE6MTcwNzATETE5Mi4xNjguMS4yOjE3MDcwExExOTIuMTY4" \
+ b"LjEuMzoxNzA3MAQgpaNZrqOw51ONk1kTER6rkm4hdPcg5VgC_dzXYxtUZaMTE2xvY2FsaG9zdC1sb2NhbGhvc3QA"