diff --git a/dcos_test_utils/dcos_api.py b/dcos_test_utils/dcos_api.py index 0df3625..08666ed 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) -> package.Package: + 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..0320a2c 100644 --- a/dcos_test_utils/package.py +++ b/dcos_test_utils/package.py @@ -99,3 +99,222 @@ 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. + + Args: + name: Repository name + uri: Repository URI + index: Position in repository list, starting at 0 + + Returns: + dict: JSON response + """ + params = { + 'uri': uri, + 'name': name, + } + + 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') + r = self._post('/add', params) + return r.json() + + def delete(self, name: str, uri: str = None) -> dict: + """Delete the package repository with given name. + + Args: + name: Repository name + uri: Repository URI + + Returns: + dict: JSON response + """ + params = {'name': name, } + if uri: + params['uri'] = uri + + self._update_headers('repository.delete') + r = self._post('/delete', params) + return r.json() + + def list(self) -> dict: + """Get list of package repositories. + + Returns: + dict: JSON response + """ + self._update_headers('repository.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': '1', + 'response_version': '1', + } + + 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. + + 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) + r = self._post("/{}".format(resource), params) + return r.json() + + def list(self, name: str = None, app_id: str = None) -> dict: + """List installed packages. + + Args: + name: Package name + app_id: App ID (optional) + + Returns: + dict: Installed packages + """ + params = {} + if app_id: + params['appId'] = app_id + if name: + params['packageName'] = name + return self._make_request('list', params) + + def install(self, name: str, version: str = None, + options: dict = None, app_id: str = None) -> dict: + """Install a Universe package. + + Args: + name: Package name + version: Package version (optional) + options: Installation options (optional) + app_id: App ID (optional) + + Returns: + dict: JSON response + """ + params = { + 'packageName': name, + } + if version is not None: + params['packageVersion'] = version + + 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'}) + + def uninstall(self, name: str, app_id: str = None) -> dict: + """Uninstall a Universe package + + Args: + name: Package name + app_id: App ID (optional) + + Returns: + dict: JSON response + """ + params = { + 'packageName': name, + } + if app_id: + params['appId'] = app_id + return self._make_request('uninstall', params) + + def describe(self, name: str, version: str = None) -> dict: + """Show information about a package. + + Args: + name: Package name + version: Package version + + Returns: + dict: JSON response + """ + params = { + 'packageName': name, + } + if version: + params['packageVersion'] = version + return self._make_request('describe', params, + {'response_version': '2'}) + + def search(self, query: str = None) -> dict: + """List all packages with a given partial (query). + + Args: + query: Partial package query + + Returns: + dict: JSON response + """ + 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. + + Args: + name: Package name + include_versions: Include version details + + Returns: + JSON response + + Raises: + HTTPError + + """ + params = { + 'packageName': name, + 'includePackageVersions': include_versions, + } + return self._make_request('list-versions', params) + + @property + 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), + session=self.session) diff --git a/integration_test.py b/integration_test.py index 6b3763f..588e474 100644 --- a/integration_test.py +++ b/integration_test.py @@ -93,3 +93,36 @@ 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', + 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) + 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) + + pack_api.describe('jenkins') + pack_api.search('jenk*') + pack_api.list_versions('jenkins') + + +def test_repository(dcos_api_session): + repo = dcos_api_session.package.repository + + listings = repo.list()['repositories'] + assert len(listings) > 0 + 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] 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..c4a1231 --- /dev/null +++ b/tests/test_package.py @@ -0,0 +1,220 @@ +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) + + try: + yield mock_session + finally: + 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_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(( + 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}) + ) + + +def test_package_list(mock_url, replay_session): + exp_url = 'https://localhost:443/package/list' + 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' + 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_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(( + 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'}})) + + +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': {}}))