From cbbbaf00332c6180b21d0d2331678db2e05d76c3 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 10:37:50 -0700 Subject: [PATCH 01/24] Add package and repository Add a `package` property to DCOS API Session. This returns a new `Package` object that maps directly to the "/package" context. A `repository` property is available on Package that maps to "/package/repository" and provides methods to interact with the repository context. --- dcos_test_utils/dcos_api.py | 6 ++ dcos_test_utils/package.py | 144 ++++++++++++++++++++++++++++++++++++ integration_test.py | 22 ++++++ 3 files changed, 172 insertions(+) diff --git a/dcos_test_utils/dcos_api.py b/dcos_test_utils/dcos_api.py index 0df3625..382b88c 100644 --- a/dcos_test_utils/dcos_api.py +++ b/dcos_test_utils/dcos_api.py @@ -522,6 +522,12 @@ def cosmos(self): default_url=self.default_url.copy(path="package"), session=self.copy().session) + @property + def package(self): + return package.Package( + default_url=self.default_url.copy(path="package"), + session=self.copy().session) + @property def health(self): """ Property which returns a :class:`dcos_test_utils.diagnostics.Diagnostics` diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 59e3cce..8cf6d07 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -99,3 +99,147 @@ def list_packages(self): """ self._update_headers('list') return self._post('/list', {}) + + +class Repository(Cosmos): + def __init__(self, default_url, session=None): + super().__init__(default_url, session=session) + + def add(self, name: str, uri: str, index: int = None) -> dict: + """Add a package repository. If `index` is 0 it will be added to + the top of the repository list. + + :param name: Repository name + :type name: str + :param uri: Repository URI + :type uri: str + :param index: Position in repository list starting at 0 + (lower is higher priority) + :type index: int + :return: JSON response + :rtype: dict + """ + params = { + 'uri': uri, + 'name': name, + } + if index is not None and type(index) == int: + params['index'] = index + + self._update_headers('add') + r = self._post('/add', params) + return r.json() + + def delete(self, name: str = None, uri: str = None) -> dict: + """Delete the package repository with given name. + + :param name: Repository name + :type name: str + :param uri: Repository URI + :type uri: str + :return: JSON response + :rtype: dict + """ + params = {} + if name: + params['name'] = name + if uri: + params['uri'] = uri + + self._update_headers('delete') + r = self.post('/delete', params) + return r.json() + + def list(self) -> dict: + """Get list of package repositories. + + :return: JSON response + :rtype: dict + """ + self._update_headers('list') + r = self.post('/list', {}) + return r.json() + + +class Package(Cosmos): + def __init__(self, default_url, session=None): + super().__init__(default_url, session=session) + self._versions = { + 'request_version': '2', + 'response_version': '2', + } + + def list(self, app_id: str = None, package_name: str = None) -> dict: + """List installed packages. + + :param app_id: App Id (optional) + :type app_id: str + :param package_name: Package name (optional) + :type package_name: str + :return: installed packages + :rtype: dict + """ + params = {} + if app_id: + params['appId'] = app_id + if package_name: + params['packageName'] = package_name + + self._update_headers('list', **self._versions) + r = self._post('/list', params) + return r.json() + + def install(self, package_name: str, package_version: str = None, + options: dict = None, app_id: str = None) -> dict: + """Install a Universe package. + + :param package_name: Package name + :type package_name: str + :param package_version: Package version string (optional) + :type package_version: str + :param options: Package installation options (optional) + :type options: dict + :param app_id: App ID (optional) + :type app_id: str + :return: JSON response + :rtype: dict + """ + params = { + 'packageName': package_name, + } + if package_version is not None: + params['packageVersion'] = package_version + if options and type(options) == dict: + params['options'] = options + if app_id: + params['appId'] = app_id + + self._update_headers('install', **self._versions) + r = self._post('/install', params) + return r.json() + + def uninstall(self, package_name: str, app_id: str = None) -> dict: + """Uninstall a Universe package + + :param package_name: Package name + :type package_name: str + :param app_id: App ID (optional) + :type app_id: str + :return: JSON response + :rtype: dict + """ + params = { + 'packageName': package_name, + } + if app_id: + params['appId'] = app_id + + self._update_headers('uninstall', **self._versions) + r = self._post('/uninstall', params) + return r.json() + + @property + def repository(self): + return Repository( + default_url=self.default_url.copy(path='/repository'), + session=self.session.copy()) diff --git a/integration_test.py b/integration_test.py index 6b3763f..b336fca 100644 --- a/integration_test.py +++ b/integration_test.py @@ -93,3 +93,25 @@ def test_jobs(dcos_api_session): assert False except HTTPError as http_e: assert http_e.response.status_code == 404 + + +def test_packages(dcos_api_session): + pack_api = dcos_api_session.package + install_resp = pack_api.install('hello-world', + package_version='2.1.0-0.31.2') + installed_id = install_resp['appId'] + dcos_api_session.marathon.poll_marathon_for_app_deployment( + installed_id, 1, True, False) + packs = pack_api.list() + found = [p for p in packs['packages'] + if p['appId'] == installed_id] + assert found + pack_api.uninstall('hello-world', app_id=installed_id) + packs = pack_api.list() + found = [p for p in packs['packages'] + if p['appId'] == installed_id] + assert len(found) == 0 + + +def test_repository(dcos_api_session): + dcos_api_session.package.repository.list() From 61cc84740e1b8fdc1ceba8c04bd8d98ea307422b Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 11:14:14 -0700 Subject: [PATCH 02/24] Change request header version and content for package and repository --- dcos_test_utils/package.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 8cf6d07..8e2a4dc 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -126,7 +126,7 @@ def add(self, name: str, uri: str, index: int = None) -> dict: if index is not None and type(index) == int: params['index'] = index - self._update_headers('add') + self._update_headers('repository.add') r = self._post('/add', params) return r.json() @@ -146,7 +146,7 @@ def delete(self, name: str = None, uri: str = None) -> dict: if uri: params['uri'] = uri - self._update_headers('delete') + self._update_headers('repository.delete') r = self.post('/delete', params) return r.json() @@ -156,7 +156,7 @@ def list(self) -> dict: :return: JSON response :rtype: dict """ - self._update_headers('list') + self._update_headers('repository.list') r = self.post('/list', {}) return r.json() @@ -165,7 +165,7 @@ class Package(Cosmos): def __init__(self, default_url, session=None): super().__init__(default_url, session=session) self._versions = { - 'request_version': '2', + 'request_version': '1', 'response_version': '2', } @@ -242,4 +242,4 @@ def uninstall(self, package_name: str, app_id: str = None) -> dict: def repository(self): return Repository( default_url=self.default_url.copy(path='/repository'), - session=self.session.copy()) + session=self.session) From c723819ba0bd9e8378451df81b8ea83dd097a9f1 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 11:35:44 -0700 Subject: [PATCH 03/24] Use correct "post" method and correct wait for deployment method Repository was using the wrong "post" method. Integration test for repository was using copy/pasted code that called a nonexistant method on `marathon`. --- dcos_test_utils/package.py | 4 ++-- integration_test.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 8e2a4dc..2933856 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -147,7 +147,7 @@ def delete(self, name: str = None, uri: str = None) -> dict: params['uri'] = uri self._update_headers('repository.delete') - r = self.post('/delete', params) + r = self._post('/delete', params) return r.json() def list(self) -> dict: @@ -157,7 +157,7 @@ def list(self) -> dict: :rtype: dict """ self._update_headers('repository.list') - r = self.post('/list', {}) + r = self._post('/list', {}) return r.json() diff --git a/integration_test.py b/integration_test.py index b336fca..33c7511 100644 --- a/integration_test.py +++ b/integration_test.py @@ -100,7 +100,7 @@ def test_packages(dcos_api_session): install_resp = pack_api.install('hello-world', package_version='2.1.0-0.31.2') installed_id = install_resp['appId'] - dcos_api_session.marathon.poll_marathon_for_app_deployment( + dcos_api_session.marathon.wait_for_app_deployment( installed_id, 1, True, False) packs = pack_api.list() found = [p for p in packs['packages'] From b97f3aaf3fbe8b88bc4da33193b9a8f151968367 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 13:35:43 -0700 Subject: [PATCH 04/24] Add tests for Repository Move mocks to a helpers file so that both Jobs and Repository tests can use them. --- tests/helpers.py | 69 ++++++++++++++++++++++++++++++++++++++++ tests/test_jobs.py | 65 +------------------------------------- tests/test_package.py | 73 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 143 insertions(+), 64 deletions(-) create mode 100644 tests/helpers.py create mode 100644 tests/test_package.py diff --git a/tests/helpers.py b/tests/helpers.py new file mode 100644 index 0000000..31a494f --- /dev/null +++ b/tests/helpers.py @@ -0,0 +1,69 @@ +from requests import HTTPError + + +class MockResponse: + def __init__(self, json: dict, status_code: int): + self._json = json + self._status_code = status_code + + def json(self): + return self._json + + def raise_for_status(self): + """Throw an HTTPErrer based on status code.""" + if self._status_code >= 400: + raise HTTPError('Throwing test error', response=self) + + @property + def status_code(self): + return self._status_code + + @property + def text(self): + return str(self._json) + + +class MockEmitter: + """Emulates a Session and responds with a queued response for each + request made. If no responses are queued, the request will raise + an error. + + A history of requests are held in a request cache and can be + reviewed. + """ + + def __init__(self, mock_responses: list): + self._mock_responses = mock_responses + self._request_cache = list() + + def request(self, *args, **kwargs): + self._request_cache.append((args, kwargs)) + return self._mock_responses.pop(0) + + def queue(self, response: list): + """Add responses to the response queue. + + :param response: A list of responses + :type response: list + """ + self._mock_responses.extend(response) + + @property + def headers(self): + return dict() + + @property + def cookies(self): + return dict() + + @property + def debug_cache(self): + """Return the list of requests made during this session. + + Items in the cache are formatted: + ((Method, URL), params) + + :return: a list of requests made to this session + :rtype: list + """ + return self._request_cache diff --git a/tests/test_jobs.py b/tests/test_jobs.py index f932e69..90f33ac 100644 --- a/tests/test_jobs.py +++ b/tests/test_jobs.py @@ -4,70 +4,7 @@ from dcos_test_utils.helpers import Url from dcos_test_utils.jobs import Jobs - - -class MockResponse: - def __init__(self, json: dict, status_code: int): - self._json = json - self._status_code = status_code - - def json(self): - return self._json - - def raise_for_status(self): - """Throw an HTTPErrer based on status code.""" - if self._status_code >= 400: - raise HTTPError('Throwing test error', response=self) - - @property - def status_code(self): - return self._status_code - - -class MockEmitter: - """Emulates a Session and responds with a queued response for each - request made. If no responses are queued, the request will raise - an error. - - A history of requests are held in a request cache and can be - reviewed. - """ - - def __init__(self, mock_responses: list): - self._mock_responses = mock_responses - self._request_cache = list() - - def request(self, *args, **kwargs): - self._request_cache.append((args, kwargs)) - return self._mock_responses.pop(0) - - def queue(self, response: list): - """Add responses to the response queue. - - :param response: A list of responses - :type response: list - """ - self._mock_responses.extend(response) - - @property - def headers(self): - return dict() - - @property - def cookies(self): - return dict() - - @property - def debug_cache(self): - """Return the list of requests made during this session. - - Items in the cache are formatted: - ((Method, URL), params) - - :return: a list of requests made to this session - :rtype: list - """ - return self._request_cache +from helpers import MockEmitter, MockResponse @pytest.fixture diff --git a/tests/test_package.py b/tests/test_package.py new file mode 100644 index 0000000..c5096fe --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,73 @@ +import pytest +import requests + +from dcos_test_utils.helpers import Url +from dcos_test_utils.package import Package +from helpers import MockEmitter, MockResponse + + +@pytest.fixture +def replay_session(monkeypatch): + """A mocked session that sends back the configured responses in + order. + """ + mock_session = MockEmitter(list()) + + monkeypatch.setattr(requests, 'Session', + lambda *args, **kwargs: mock_session) + + yield mock_session + + print('Actual session requests:') + print(mock_session.debug_cache) + + +@pytest.fixture +def mock_url(): + """A URL that is accepted by DcosApiSession.""" + return Url('https', 'localhost', 'package', '', '', port=443) + + +def test_repo_list(mock_url, replay_session): + exp_url = 'https://localhost:443/package/repository/list' + replay_session.queue([MockResponse({}, 200)]) + r = Package(default_url=mock_url).repository.list() + assert r == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), {'json': {}})) + + +def test_repo_add(mock_url, replay_session): + exp_url = 'https://localhost:443/package/repository/add' + replay_session.queue(( + MockResponse({}, 201), + MockResponse({}, 201) + )) + r = Package(default_url=mock_url).repository.add( + 'http://example.com/repo', + 'my_repo', 0) + assert r == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), + {'json': {'index': 0, 'name': 'http://example.com/repo', + 'uri': 'my_repo'}}) + ) + r = Package(default_url=mock_url).repository.add( + 'http://example.com/repo', + 'my_repo') + assert r == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), + {'json': {'name': 'http://example.com/repo', 'uri': 'my_repo'}}) + ) + + +def test_repo_delete(mock_url, replay_session): + exp_url = 'https://localhost:443/package/repository/delete' + kwargs = {'name': 'my_repo', 'uri': 'http://example.com/repo'} + replay_session.queue([MockResponse({}, 201)]) + r = Package(default_url=mock_url).repository.delete(**kwargs) + assert r == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), {'json': kwargs}) + ) From 015da5904d86d0a8fd6156f1492f8f3ad1fad285 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 13:36:30 -0700 Subject: [PATCH 05/24] Fix the repository path --- dcos_test_utils/package.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 2933856..0f2315a 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -240,6 +240,7 @@ def uninstall(self, package_name: str, app_id: str = None) -> dict: @property def repository(self): + repo_path = '{}/repository'.format(self.default_url.path) return Repository( - default_url=self.default_url.copy(path='/repository'), + default_url=self.default_url.copy(path=repo_path), session=self.session) From 8c961ffe461a09c4e9d6288afe914409805c95b5 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 13:49:44 -0700 Subject: [PATCH 06/24] Improve integration tests for packages --- integration_test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/integration_test.py b/integration_test.py index 33c7511..8dcad0c 100644 --- a/integration_test.py +++ b/integration_test.py @@ -101,7 +101,7 @@ def test_packages(dcos_api_session): package_version='2.1.0-0.31.2') installed_id = install_resp['appId'] dcos_api_session.marathon.wait_for_app_deployment( - installed_id, 1, True, False) + installed_id, 1, True, False, 300) packs = pack_api.list() found = [p for p in packs['packages'] if p['appId'] == installed_id] @@ -114,4 +114,6 @@ def test_packages(dcos_api_session): def test_repository(dcos_api_session): - dcos_api_session.package.repository.list() + listing = dcos_api_session.package.repository.list() + assert 'repositories' in listing + assert len(listing['repositories']) > 0 From 873492f265705dbdc9bf0c12f51759d8d72ae0ab Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 14:22:11 -0700 Subject: [PATCH 07/24] Package list operation only accepts v1 --- dcos_test_utils/package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 0f2315a..4096d32 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -185,7 +185,7 @@ def list(self, app_id: str = None, package_name: str = None) -> dict: if package_name: params['packageName'] = package_name - self._update_headers('list', **self._versions) + self._update_headers('list') r = self._post('/list', params) return r.json() From 173a7faa7b98629b329fc3b1dacb26aa81f0d589 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 15:10:57 -0700 Subject: [PATCH 08/24] Improvements to package and tests Add more unit tests to verify URL and params. All package requests go through a single method instead of repeating the same code again and again. The default versions for endpoints is now 'v1' instead of 'v2'. The `install` path sets it to 'v2'. --- dcos_test_utils/package.py | 116 +++++++++++++++++++++++++++---------- tests/test_package.py | 60 +++++++++++++++++++ 2 files changed, 147 insertions(+), 29 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 4096d32..add5330 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -166,37 +166,53 @@ def __init__(self, default_url, session=None): super().__init__(default_url, session=session) self._versions = { 'request_version': '1', - 'response_version': '2', + 'response_version': '1', } - def list(self, app_id: str = None, package_name: str = None) -> dict: + def _make_request(self, resource: str, params: dict, + versions: dict = None) -> dict: + """Every request to /packaging has the same couple of steps. + This puts them in one place instead of duplicating efforts. + + :param versions: Request and Response versions + :type versions: dict + :param resource: Package resource (list, install, etc) + :type resource: str + :param params: POST parameters for the resource + :type params: dict + :return: JSON response + :rtype: dict + """ + kw = versions if versions else self._versions + self._update_headers(resource, **kw) + r = self._post("/{}".format(resource), params) + return r.json() + + def list(self, name: str = None, app_id: str = None) -> dict: """List installed packages. :param app_id: App Id (optional) :type app_id: str - :param package_name: Package name (optional) - :type package_name: str + :param name: Package name (optional) + :type name: str :return: installed packages :rtype: dict """ params = {} if app_id: params['appId'] = app_id - if package_name: - params['packageName'] = package_name - - self._update_headers('list') - r = self._post('/list', params) - return r.json() + if name: + params['packageName'] = name + return self._make_request('list', params) - def install(self, package_name: str, package_version: str = None, + def install(self, name: str, version: str = None, options: dict = None, app_id: str = None) -> dict: """Install a Universe package. - :param package_name: Package name - :type package_name: str - :param package_version: Package version string (optional) - :type package_version: str + :param name: Package name + :type name: str + :param version: Package version string (optional) + :type version: str :param options: Package installation options (optional) :type options: dict :param app_id: App ID (optional) @@ -205,38 +221,80 @@ def install(self, package_name: str, package_version: str = None, :rtype: dict """ params = { - 'packageName': package_name, + 'packageName': name, } - if package_version is not None: - params['packageVersion'] = package_version + if version is not None: + params['packageVersion'] = version if options and type(options) == dict: params['options'] = options if app_id: params['appId'] = app_id + return self._make_request('install', params, + {'response_version': '2', }) - self._update_headers('install', **self._versions) - r = self._post('/install', params) - return r.json() - - def uninstall(self, package_name: str, app_id: str = None) -> dict: + def uninstall(self, name: str, app_id: str = None) -> dict: """Uninstall a Universe package - :param package_name: Package name - :type package_name: str + :param name: Package name + :type name: str :param app_id: App ID (optional) :type app_id: str :return: JSON response :rtype: dict """ params = { - 'packageName': package_name, + 'packageName': name, } if app_id: params['appId'] = app_id + return self._make_request('uninstall', params) - self._update_headers('uninstall', **self._versions) - r = self._post('/uninstall', params) - return r.json() + def describe(self, name: str, version: str = None) -> dict: + """Show information about a package. + + :param name: Package name + :type name: str + :param version: Specific package version (optional) + :type version: str + :return: JSON response + :rtype: dict + """ + params = { + 'packageName': name, + } + if version: + params['packageVersion'] = version + return self._make_request('describe', params) + + def search(self, query: str = None) -> dict: + """List all packages with a given partial (query). + + :param query: Partial package name + :type query: str + :return: JSON response + :rtype: dict + """ + params = {} + if query: + params['query'] = query + return self._make_request('search', params) + + def list_versions(self, name: str, + include_versions: bool = False) -> dict: + """List all available versions for a given package. + + :param name: Package name + :type name: str + :param include_versions: Include version details + :type include_versions: bool + :return: JSON response + :rtype: dict + """ + params = { + 'packageName': name, + 'includePackageVersions': include_versions, + } + return self._make_request('list-versions', params) @property def repository(self): diff --git a/tests/test_package.py b/tests/test_package.py index c5096fe..c9a7a68 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -71,3 +71,63 @@ def test_repo_delete(mock_url, replay_session): assert replay_session.debug_cache[0] == ( (('POST', exp_url), {'json': kwargs}) ) + + +def test_package_list(mock_url, replay_session): + exp_url = 'https://localhost:443/package/list' + replay_session.queue([MockResponse({}, 200)]) + p = Package(default_url=mock_url).list() + assert p == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), {'json': {}})) + + +def test_package_install(mock_url, replay_session): + exp_url = 'https://localhost:443/package/install' + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + MockResponse({}, 200), MockResponse({}, 200), + )) + p = Package(default_url=mock_url).install('hello-world') + assert p == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), {'json': {'packageName': 'hello-world'}})) + p = Package(default_url=mock_url).install('hello-world', '23') + assert p == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'packageVersion': '23'}})) + p = Package(default_url=mock_url).install('hello-world', '23', {'a': 'b'}) + assert p == {} + assert replay_session.debug_cache[2] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'packageVersion': '23', + 'options': {'a': 'b'}}})) + p = Package(default_url=mock_url).install('hello-world', '23', + {'a': 'b'}, 'myapp1') + assert p == {} + assert replay_session.debug_cache[3] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'packageVersion': '23', + 'options': {'a': 'b'}, + 'appId': 'myapp1'}})) + + +def test_package_uninstall(mock_url, replay_session): + exp_url = 'https://localhost:443/package/uninstall' + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + )) + p = Package(default_url=mock_url).uninstall('hello-world') + assert p == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), {'json': {'packageName': 'hello-world'}})) + p = Package(default_url=mock_url).uninstall('hello-world', 'myapp1') + assert p == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'appId': 'myapp1'}})) From ca4c8aa9d295ebbcb78ef311686f499812f9c0ea Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 15:13:32 -0700 Subject: [PATCH 09/24] alignment --- integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.py b/integration_test.py index 8dcad0c..614c0f3 100644 --- a/integration_test.py +++ b/integration_test.py @@ -98,7 +98,7 @@ def test_jobs(dcos_api_session): def test_packages(dcos_api_session): pack_api = dcos_api_session.package install_resp = pack_api.install('hello-world', - package_version='2.1.0-0.31.2') + version='2.1.0-0.31.2') installed_id = install_resp['appId'] dcos_api_session.marathon.wait_for_app_deployment( installed_id, 1, True, False, 300) From 01dd786b499166ede86bc40debe31d03c05b4338 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 16:12:50 -0700 Subject: [PATCH 10/24] Do not wait for package to uninstall --- integration_test.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/integration_test.py b/integration_test.py index 614c0f3..bfda999 100644 --- a/integration_test.py +++ b/integration_test.py @@ -107,10 +107,6 @@ def test_packages(dcos_api_session): if p['appId'] == installed_id] assert found pack_api.uninstall('hello-world', app_id=installed_id) - packs = pack_api.list() - found = [p for p in packs['packages'] - if p['appId'] == installed_id] - assert len(found) == 0 def test_repository(dcos_api_session): From f146e2b8d2678f9fef90b1cb1c4599fd572e16a1 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 16:53:24 -0700 Subject: [PATCH 11/24] Update response version for list and describe --- dcos_test_utils/package.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index add5330..0390254 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -203,7 +203,8 @@ def list(self, name: str = None, app_id: str = None) -> dict: params['appId'] = app_id if name: params['packageName'] = name - return self._make_request('list', params) + return self._make_request('list', params, + {'response_version': '2'}) def install(self, name: str, version: str = None, options: dict = None, app_id: str = None) -> dict: @@ -264,7 +265,8 @@ def describe(self, name: str, version: str = None) -> dict: } if version: params['packageVersion'] = version - return self._make_request('describe', params) + return self._make_request('describe', params, + {'response_version': '3'}) def search(self, query: str = None) -> dict: """List all packages with a given partial (query). From 0f4e3ffca48287e94720f8f729b5992ebadb7723 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 28 Mar 2018 17:17:49 -0700 Subject: [PATCH 12/24] Walk back version changes --- dcos_test_utils/package.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index 0390254..add5330 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -203,8 +203,7 @@ def list(self, name: str = None, app_id: str = None) -> dict: params['appId'] = app_id if name: params['packageName'] = name - return self._make_request('list', params, - {'response_version': '2'}) + return self._make_request('list', params) def install(self, name: str, version: str = None, options: dict = None, app_id: str = None) -> dict: @@ -265,8 +264,7 @@ def describe(self, name: str, version: str = None) -> dict: } if version: params['packageVersion'] = version - return self._make_request('describe', params, - {'response_version': '3'}) + return self._make_request('describe', params) def search(self, query: str = None) -> dict: """List all packages with a given partial (query). From d86341f396cf1fbe4a881988d744918e07223197 Mon Sep 17 00:00:00 2001 From: Colin C Date: Fri, 30 Mar 2018 16:39:32 -0700 Subject: [PATCH 13/24] Raise a TypeError when inputs are not supported types --- dcos_test_utils/package.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index add5330..b581b42 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -123,7 +123,12 @@ def add(self, name: str, uri: str, index: int = None) -> dict: 'uri': uri, 'name': name, } - if index is not None and type(index) == int: + + if index is not None: + idx_type = type(index) + if idx_type != int: + raise TypeError('index of type {} is not supported ' + '- must be int'.format(idx_type)) params['index'] = index self._update_headers('repository.add') @@ -225,12 +230,18 @@ def install(self, name: str, version: str = None, } if version is not None: params['packageVersion'] = version - if options and type(options) == dict: + + if options is not None: + opt_type = type(options) + if opt_type != dict: + raise TypeError('options of type {} is not supported ' + '- must be dict'.format(opt_type)) params['options'] = options + if app_id: params['appId'] = app_id return self._make_request('install', params, - {'response_version': '2', }) + {'response_version': '2'}) def uninstall(self, name: str, app_id: str = None) -> dict: """Uninstall a Universe package From 76016632fb2c75d144d96edf46bea6aa6d9cc93f Mon Sep 17 00:00:00 2001 From: Colin C Date: Mon, 2 Apr 2018 15:28:39 -0700 Subject: [PATCH 14/24] Add more tests to increase coverage --- tests/test_package.py | 100 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 93 insertions(+), 7 deletions(-) diff --git a/tests/test_package.py b/tests/test_package.py index c9a7a68..a9df074 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -30,13 +30,23 @@ def mock_url(): def test_repo_list(mock_url, replay_session): exp_url = 'https://localhost:443/package/repository/list' - replay_session.queue([MockResponse({}, 200)]) + replay_session.queue((MockResponse({}, 200), )) r = Package(default_url=mock_url).repository.list() assert r == {} assert replay_session.debug_cache[0] == ( (('POST', exp_url), {'json': {}})) +def test_repo_add_typeerror(mock_url, replay_session): + replay_session.queue(( + MockResponse({}, 201), + MockResponse({}, 201) + )) + with pytest.raises(TypeError): + Package(default_url=mock_url).repository.add( + 'http://example.com/repo', 'my_repo', 'a') + + def test_repo_add(mock_url, replay_session): exp_url = 'https://localhost:443/package/repository/add' replay_session.queue(( @@ -44,8 +54,7 @@ def test_repo_add(mock_url, replay_session): MockResponse({}, 201) )) r = Package(default_url=mock_url).repository.add( - 'http://example.com/repo', - 'my_repo', 0) + 'http://example.com/repo', 'my_repo', 0) assert r == {} assert replay_session.debug_cache[0] == ( (('POST', exp_url), @@ -53,12 +62,12 @@ def test_repo_add(mock_url, replay_session): 'uri': 'my_repo'}}) ) r = Package(default_url=mock_url).repository.add( - 'http://example.com/repo', - 'my_repo') + 'http://example.com/repo', 'my_repo') assert r == {} assert replay_session.debug_cache[1] == ( (('POST', exp_url), - {'json': {'name': 'http://example.com/repo', 'uri': 'my_repo'}}) + {'json': {'name': 'http://example.com/repo', + 'uri': 'my_repo'}}) ) @@ -75,12 +84,25 @@ def test_repo_delete(mock_url, replay_session): def test_package_list(mock_url, replay_session): exp_url = 'https://localhost:443/package/list' - replay_session.queue([MockResponse({}, 200)]) + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + MockResponse({}, 200), MockResponse({}, 200), + )) p = Package(default_url=mock_url).list() assert p == {} assert replay_session.debug_cache[0] == ( (('POST', exp_url), {'json': {}})) + p = Package(default_url=mock_url).list(name='a') + assert p == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), {'json': {'packageName': 'a'}})) + + p = Package(default_url=mock_url).list(app_id='b') + assert p == {} + assert replay_session.debug_cache[2] == ( + (('POST', exp_url), {'json': {'appId': 'b'}})) + def test_package_install(mock_url, replay_session): exp_url = 'https://localhost:443/package/install' @@ -116,6 +138,15 @@ def test_package_install(mock_url, replay_session): 'appId': 'myapp1'}})) +def test_package_install_typeerror(mock_url, replay_session): + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + )) + with pytest.raises(TypeError): + Package(default_url=mock_url).install('hello-world', + options='a') + + def test_package_uninstall(mock_url, replay_session): exp_url = 'https://localhost:443/package/uninstall' replay_session.queue(( @@ -131,3 +162,58 @@ def test_package_uninstall(mock_url, replay_session): (('POST', exp_url), {'json': {'packageName': 'hello-world', 'appId': 'myapp1'}})) + + +def test_package_list_versions(mock_url, replay_session): + exp_url = 'https://localhost:443/package/list-versions' + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + )) + p = Package(default_url=mock_url).list_versions('hello-world') + assert p == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'includePackageVersions': False}}) + ) + p = Package(default_url=mock_url).list_versions('hello-world', True) + assert p == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'includePackageVersions': True}}) + ) + + +def test_package_describe(mock_url, replay_session): + exp_url = 'https://localhost:443/package/describe' + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + )) + p = Package(default_url=mock_url).describe('hello-world') + assert p == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world'}})) + p = Package(default_url=mock_url).describe('hello-world', 'v3') + assert p == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), + {'json': {'packageName': 'hello-world', + 'packageVersion': 'v3'}})) + + +def test_package_search(mock_url, replay_session): + exp_url = 'https://localhost:443/package/search' + replay_session.queue(( + MockResponse({}, 200), MockResponse({}, 200), + )) + p = Package(default_url=mock_url).search('hello-world') + assert p == {} + assert replay_session.debug_cache[0] == ( + (('POST', exp_url), + {'json': {'query': 'hello-world'}})) + p = Package(default_url=mock_url).search() + assert p == {} + assert replay_session.debug_cache[1] == ( + (('POST', exp_url), {'json': {}})) From e1e3dfbae3296915588382f02cb4476c17a15f0b Mon Sep 17 00:00:00 2001 From: Colin C Date: Tue, 3 Apr 2018 11:30:39 -0700 Subject: [PATCH 15/24] Add more package calls to integration test --- integration_test.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integration_test.py b/integration_test.py index bfda999..d16fdce 100644 --- a/integration_test.py +++ b/integration_test.py @@ -108,6 +108,10 @@ def test_packages(dcos_api_session): assert found pack_api.uninstall('hello-world', app_id=installed_id) + pack_api.desrcibe('hello-world') + pack_api.search('hello*') + pack_api.list_versions('hello-world') + def test_repository(dcos_api_session): listing = dcos_api_session.package.repository.list() From 4eac21850dd9c879714f4d1e1a7af286fa6fec65 Mon Sep 17 00:00:00 2001 From: Colin C Date: Tue, 3 Apr 2018 11:31:00 -0700 Subject: [PATCH 16/24] Add type hint to package property --- dcos_test_utils/dcos_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dcos_test_utils/dcos_api.py b/dcos_test_utils/dcos_api.py index 382b88c..08666ed 100644 --- a/dcos_test_utils/dcos_api.py +++ b/dcos_test_utils/dcos_api.py @@ -523,7 +523,7 @@ def cosmos(self): session=self.copy().session) @property - def package(self): + def package(self) -> package.Package: return package.Package( default_url=self.default_url.copy(path="package"), session=self.copy().session) From e11a0eefb14fc5ff4126da2ce93df1efbdd0953f Mon Sep 17 00:00:00 2001 From: Colin C Date: Tue, 3 Apr 2018 12:59:01 -0700 Subject: [PATCH 17/24] Fix typo --- integration_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration_test.py b/integration_test.py index d16fdce..e8766c4 100644 --- a/integration_test.py +++ b/integration_test.py @@ -108,7 +108,7 @@ def test_packages(dcos_api_session): assert found pack_api.uninstall('hello-world', app_id=installed_id) - pack_api.desrcibe('hello-world') + pack_api.describe('hello-world') pack_api.search('hello*') pack_api.list_versions('hello-world') From 9b5a5eeab5f3a8b03e2425949d81f31b5cd6c544 Mon Sep 17 00:00:00 2001 From: Colin C Date: Tue, 3 Apr 2018 14:11:37 -0700 Subject: [PATCH 18/24] Use v2 for package describe response --- dcos_test_utils/package.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index b581b42..b38997b 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -275,7 +275,8 @@ def describe(self, name: str, version: str = None) -> dict: } if version: params['packageVersion'] = version - return self._make_request('describe', params) + return self._make_request('describe', params, + {'response_version': '2'}) def search(self, query: str = None) -> dict: """List all packages with a given partial (query). From bbeb2b66db3804f45b100cf0a512cc0ccbefc9d9 Mon Sep 17 00:00:00 2001 From: Colin C Date: Tue, 3 Apr 2018 14:28:10 -0700 Subject: [PATCH 19/24] Use Jenkins for package tests --- integration_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/integration_test.py b/integration_test.py index e8766c4..4aaf615 100644 --- a/integration_test.py +++ b/integration_test.py @@ -108,9 +108,9 @@ def test_packages(dcos_api_session): assert found pack_api.uninstall('hello-world', app_id=installed_id) - pack_api.describe('hello-world') - pack_api.search('hello*') - pack_api.list_versions('hello-world') + pack_api.describe('jenkins') + pack_api.search('jenk*') + pack_api.list_versions('jenkins') def test_repository(dcos_api_session): From 016771be8be0f803eaec4268c81d764c29be5416 Mon Sep 17 00:00:00 2001 From: Colin C Date: Wed, 4 Apr 2018 12:34:54 -0700 Subject: [PATCH 20/24] Add to repo integration test --- integration_test.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/integration_test.py b/integration_test.py index 4aaf615..3923f0e 100644 --- a/integration_test.py +++ b/integration_test.py @@ -114,6 +114,14 @@ def test_packages(dcos_api_session): def test_repository(dcos_api_session): - listing = dcos_api_session.package.repository.list() - assert 'repositories' in listing + repo_name = 'my-universe' + repo = dcos_api_session.package.repository + + listing = repo.list() assert len(listing['repositories']) > 0 + repo.add(repo_name, + 'http://myuniverse.marathon.mesos:8500/repo', 0) + new_listing = repo.list()['repositories'] + my_uni = [x for x in new_listing if x['name'] == repo_name] + assert my_uni + repo.delete(repo_name) From 9e579292abbb8174b3725069c3634fc4af711b71 Mon Sep 17 00:00:00 2001 From: Colin C Date: Thu, 5 Apr 2018 12:00:25 -0700 Subject: [PATCH 21/24] Repo integration test now uses existing repos --- integration_test.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/integration_test.py b/integration_test.py index 3923f0e..1c851ab 100644 --- a/integration_test.py +++ b/integration_test.py @@ -114,14 +114,13 @@ def test_packages(dcos_api_session): def test_repository(dcos_api_session): - repo_name = 'my-universe' repo = dcos_api_session.package.repository - listing = repo.list() - assert len(listing['repositories']) > 0 - repo.add(repo_name, - 'http://myuniverse.marathon.mesos:8500/repo', 0) - new_listing = repo.list()['repositories'] - my_uni = [x for x in new_listing if x['name'] == repo_name] - assert my_uni - repo.delete(repo_name) + listings = repo.list()['repositories'] + assert len(listings) > 0 + old_name, old_uri = listings[0]['name'], listings[0]['uri'] + + repo.delete(old_name) + repo.add(old_name, old_uri, 0) + listings = repo.list()['repositories'] + assert [x for x in listings if x['name'] == old_name] From 374892def66a43fc6564aef82f3e696f0bd6320f Mon Sep 17 00:00:00 2001 From: Colin C Date: Thu, 5 Apr 2018 12:01:12 -0700 Subject: [PATCH 22/24] name is required for repo delete --- dcos_test_utils/package.py | 140 +++++++++++++++++++------------------ 1 file changed, 72 insertions(+), 68 deletions(-) diff --git a/dcos_test_utils/package.py b/dcos_test_utils/package.py index b38997b..0320a2c 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -109,15 +109,13 @@ def add(self, name: str, uri: str, index: int = None) -> dict: """Add a package repository. If `index` is 0 it will be added to the top of the repository list. - :param name: Repository name - :type name: str - :param uri: Repository URI - :type uri: str - :param index: Position in repository list starting at 0 - (lower is higher priority) - :type index: int - :return: JSON response - :rtype: dict + Args: + name: Repository name + uri: Repository URI + index: Position in repository list, starting at 0 + + Returns: + dict: JSON response """ params = { 'uri': uri, @@ -135,19 +133,17 @@ def add(self, name: str, uri: str, index: int = None) -> dict: r = self._post('/add', params) return r.json() - def delete(self, name: str = None, uri: str = None) -> dict: + def delete(self, name: str, uri: str = None) -> dict: """Delete the package repository with given name. - :param name: Repository name - :type name: str - :param uri: Repository URI - :type uri: str - :return: JSON response - :rtype: dict + Args: + name: Repository name + uri: Repository URI + + Returns: + dict: JSON response """ - params = {} - if name: - params['name'] = name + params = {'name': name, } if uri: params['uri'] = uri @@ -158,8 +154,8 @@ def delete(self, name: str = None, uri: str = None) -> dict: def list(self) -> dict: """Get list of package repositories. - :return: JSON response - :rtype: dict + Returns: + dict: JSON response """ self._update_headers('repository.list') r = self._post('/list', {}) @@ -179,14 +175,13 @@ def _make_request(self, resource: str, params: dict, """Every request to /packaging has the same couple of steps. This puts them in one place instead of duplicating efforts. - :param versions: Request and Response versions - :type versions: dict - :param resource: Package resource (list, install, etc) - :type resource: str - :param params: POST parameters for the resource - :type params: dict - :return: JSON response - :rtype: dict + Args: + resource: Request and Response versions + params: Package resources (list, install, etc) + versions: POST parameters for the resource + + Returns: + dict: JSON response """ kw = versions if versions else self._versions self._update_headers(resource, **kw) @@ -196,12 +191,12 @@ def _make_request(self, resource: str, params: dict, def list(self, name: str = None, app_id: str = None) -> dict: """List installed packages. - :param app_id: App Id (optional) - :type app_id: str - :param name: Package name (optional) - :type name: str - :return: installed packages - :rtype: dict + Args: + name: Package name + app_id: App ID (optional) + + Returns: + dict: Installed packages """ params = {} if app_id: @@ -214,16 +209,14 @@ def install(self, name: str, version: str = None, options: dict = None, app_id: str = None) -> dict: """Install a Universe package. - :param name: Package name - :type name: str - :param version: Package version string (optional) - :type version: str - :param options: Package installation options (optional) - :type options: dict - :param app_id: App ID (optional) - :type app_id: str - :return: JSON response - :rtype: dict + Args: + name: Package name + version: Package version (optional) + options: Installation options (optional) + app_id: App ID (optional) + + Returns: + dict: JSON response """ params = { 'packageName': name, @@ -246,12 +239,12 @@ def install(self, name: str, version: str = None, def uninstall(self, name: str, app_id: str = None) -> dict: """Uninstall a Universe package - :param name: Package name - :type name: str - :param app_id: App ID (optional) - :type app_id: str - :return: JSON response - :rtype: dict + Args: + name: Package name + app_id: App ID (optional) + + Returns: + dict: JSON response """ params = { 'packageName': name, @@ -263,12 +256,12 @@ def uninstall(self, name: str, app_id: str = None) -> dict: def describe(self, name: str, version: str = None) -> dict: """Show information about a package. - :param name: Package name - :type name: str - :param version: Specific package version (optional) - :type version: str - :return: JSON response - :rtype: dict + Args: + name: Package name + version: Package version + + Returns: + dict: JSON response """ params = { 'packageName': name, @@ -281,10 +274,11 @@ def describe(self, name: str, version: str = None) -> dict: def search(self, query: str = None) -> dict: """List all packages with a given partial (query). - :param query: Partial package name - :type query: str - :return: JSON response - :rtype: dict + Args: + query: Partial package query + + Returns: + dict: JSON response """ params = {} if query: @@ -295,12 +289,16 @@ def list_versions(self, name: str, include_versions: bool = False) -> dict: """List all available versions for a given package. - :param name: Package name - :type name: str - :param include_versions: Include version details - :type include_versions: bool - :return: JSON response - :rtype: dict + Args: + name: Package name + include_versions: Include version details + + Returns: + JSON response + + Raises: + HTTPError + """ params = { 'packageName': name, @@ -309,7 +307,13 @@ def list_versions(self, name: str, return self._make_request('list-versions', params) @property - def repository(self): + def repository(self) -> Repository: + """Cosmos Repository + + Returns: + Repository: Cosmos Repository + + """ repo_path = '{}/repository'.format(self.default_url.path) return Repository( default_url=self.default_url.copy(path=repo_path), From c29cf767d2df05025265dc7bd3c4a26cce5a9c0e Mon Sep 17 00:00:00 2001 From: Colin C Date: Tue, 10 Apr 2018 15:01:06 -0700 Subject: [PATCH 23/24] Check repo listings after each command --- integration_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration_test.py b/integration_test.py index 1c851ab..588e474 100644 --- a/integration_test.py +++ b/integration_test.py @@ -121,6 +121,8 @@ def test_repository(dcos_api_session): old_name, old_uri = listings[0]['name'], listings[0]['uri'] repo.delete(old_name) + listings = repo.list()['repositories'] + assert [x for x in listings if x['name'] == old_name] == [] repo.add(old_name, old_uri, 0) listings = repo.list()['repositories'] assert [x for x in listings if x['name'] == old_name] From 28684388fc88e360471d19fae4f7f0b02b5ec24c Mon Sep 17 00:00:00 2001 From: Colin C Date: Mon, 16 Apr 2018 14:52:56 -0700 Subject: [PATCH 24/24] Wrap debug output in try/catch so it always gets printed --- tests/test_package.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_package.py b/tests/test_package.py index a9df074..c4a1231 100644 --- a/tests/test_package.py +++ b/tests/test_package.py @@ -16,10 +16,11 @@ def replay_session(monkeypatch): monkeypatch.setattr(requests, 'Session', lambda *args, **kwargs: mock_session) - yield mock_session - - print('Actual session requests:') - print(mock_session.debug_cache) + try: + yield mock_session + finally: + print('Actual session requests:') + print(mock_session.debug_cache) @pytest.fixture