From 57517adef393551d130fe3f1957cd3c6acbb4d7c Mon Sep 17 00:00:00 2001 From: Evan Cordell Date: Mon, 17 Jul 2017 17:56:32 -0400 Subject: [PATCH] Add tests for repository notification api --- endpoints/api/repositorynotification.py | 41 +++---- ...repositorynotification_models_interface.py | 4 +- .../repositorynotification_models_pre_oci.py | 30 +++-- endpoints/api/test/test_models_pre_oci.py | 106 ++++++++++++++++++ .../api/test/test_repositorynotification.py | 84 ++++++++++++++ endpoints/api/test/test_security.py | 2 +- endpoints/notificationmethod.py | 4 +- 7 files changed, 239 insertions(+), 32 deletions(-) create mode 100644 endpoints/api/test/test_models_pre_oci.py create mode 100644 endpoints/api/test/test_repositorynotification.py diff --git a/endpoints/api/repositorynotification.py b/endpoints/api/repositorynotification.py index cb612803d..10192479b 100644 --- a/endpoints/api/repositorynotification.py +++ b/endpoints/api/repositorynotification.py @@ -6,12 +6,12 @@ from flask import request from endpoints.api import (RepositoryParamResource, nickname, resource, require_repo_admin, log_action, validate_json_request, request_error, path_param, disallow_for_app_repositories) -from endpoints.exception import NotFound +from endpoints.exception import NotFound, InvalidRequest from endpoints.notificationmethod import (NotificationMethod, CannotValidateNotificationMethodException) from endpoints.notificationhelper import build_notification_data from workers.notificationworker.models_pre_oci import notification -from endpoints.api.tag_models_pre_oci import pre_oci_model as model +from endpoints.api.repositorynotification_models_pre_oci import pre_oci_model as model logger = logging.getLogger(__name__) @@ -68,7 +68,6 @@ class RepositoryNotificationList(RepositoryParamResource): except CannotValidateNotificationMethodException as ex: raise request_error(message=ex.message) - new_notification = model.create_repo_notification(namespace_name, repository_name, parsed['event'], parsed['method'], @@ -79,7 +78,7 @@ class RepositoryNotificationList(RepositoryParamResource): log_action('add_repo_notification', namespace_name, {'repo': repository_name, 'namespace': namespace_name, 'notification_id': new_notification.uuid, - 'event': parsed['event'], 'method': parsed['method']}, + 'event': new_notification.event_name, 'method': new_notification.method_name}, repo_name=repository_name) return new_notification.to_dict(), 201 @@ -104,11 +103,9 @@ class RepositoryNotification(RepositoryParamResource): @disallow_for_app_repositories def get(self, namespace_name, repository_name, uuid): """ Get information for the specified notification. """ - try: - found = model.get_repo_notification(uuid) - except model.InvalidNotificationException: + found = model.get_repo_notification(uuid) + if not found: raise NotFound() - return found.to_dict() @require_repo_admin @@ -117,9 +114,12 @@ class RepositoryNotification(RepositoryParamResource): def delete(self, namespace_name, repository_name, uuid): """ Deletes the specified notification. """ deleted = model.delete_repo_notification(namespace_name, repository_name, uuid) + if not deleted: + raise InvalidRequest("No repository notification found for: %s, %s, %s" % (namespace_name, repository_name, uuid)) + log_action('delete_repo_notification', namespace_name, {'repo': repository_name, 'namespace': namespace_name, 'notification_id': uuid, - 'event': deleted.event.name, 'method': deleted.method.name}, + 'event': deleted.event_name, 'method': deleted.method_name}, repo_name=repository_name) return 'No Content', 204 @@ -130,11 +130,13 @@ class RepositoryNotification(RepositoryParamResource): def post(self, namespace_name, repository_name, uuid): """ Resets repository notification to 0 failures. """ reset = model.reset_notification_number_of_failures(namespace_name, repository_name, uuid) - if reset is not None: - log_action('reset_repo_notification', namespace_name, - {'repo': repository_name, 'namespace': namespace_name, 'notification_id': uuid, - 'event': reset.event.name, 'method': reset.method.name}, - repo_name=repository_name) + if not reset: + raise InvalidRequest("No repository notification found for: %s, %s, %s" % (namespace_name, repository_name, uuid)) + + log_action('reset_repo_notification', namespace_name, + {'repo': repository_name, 'namespace': namespace_name, 'notification_id': uuid, + 'event': reset.event_name, 'method': reset.method_name}, + repo_name=repository_name) return 'No Content', 204 @@ -147,10 +149,11 @@ class TestRepositoryNotification(RepositoryParamResource): @require_repo_admin @nickname('testRepoNotification') @disallow_for_app_repositories - def post(self, namespace, repository, uuid): + def post(self, namespace_name, repository_name, uuid): """ Queues a test notification for this repository. """ - test_note = model.get_repo_notification(uuid) + test_note = model.queue_test_notification(uuid) + if not test_note: - raise NotFound() - model.queue_test_notification(test_note) - return {} + raise InvalidRequest("No repository notification found for: %s, %s, %s" % (namespace_name, repository_name, uuid)) + + return {}, 200 diff --git a/endpoints/api/repositorynotification_models_interface.py b/endpoints/api/repositorynotification_models_interface.py index 9237cfeda..f0f4a72af 100644 --- a/endpoints/api/repositorynotification_models_interface.py +++ b/endpoints/api/repositorynotification_models_interface.py @@ -27,13 +27,11 @@ class RepositoryNotification( :type number_of_failures: int """ def to_dict(self): - config = {} try: config = json.loads(self.config_json) except: config = {} - event_config = {} try: event_config = json.loads(self.event_config_json) except: @@ -77,5 +75,5 @@ class RepoNotificationInterface(object): pass @abstractmethod - def queue_test_notification(self, notification): + def queue_test_notification(self, uuid): pass diff --git a/endpoints/api/repositorynotification_models_pre_oci.py b/endpoints/api/repositorynotification_models_pre_oci.py index e1a60c5e8..e1d4c37e9 100644 --- a/endpoints/api/repositorynotification_models_pre_oci.py +++ b/endpoints/api/repositorynotification_models_pre_oci.py @@ -2,12 +2,13 @@ import json from app import notification_queue from data import model +from data.model import InvalidNotificationException from endpoints.api.repositorynotification_models_interface import RepoNotificationInterface, RepositoryNotification from endpoints.notificationevent import NotificationEvent from endpoints.notificationhelper import build_notification_data -class PreOCIModel(RepoNotificationInterface): +class RepoNotificationPreOCIModel(RepoNotificationInterface): def create_repo_notification(self, namespace_name, repository_name, event_name, method_name, method_config, event_config, title=None): repository = model.repository.get_repository(namespace_name, repository_name) @@ -23,25 +24,40 @@ class PreOCIModel(RepoNotificationInterface): for n in model.notification.list_repo_notifications(namespace_name, repository_name, event_name)] def get_repo_notification(self, uuid): - return self._notification(model.notification.get_repo_notification(uuid)) + try: + found = model.notification.get_repo_notification(uuid) + except InvalidNotificationException: + return None + return self._notification(found) def delete_repo_notification(self, namespace_name, repository_name, uuid): - return self._notification( - self.model.notification.delete_repo_notification(namespace_name, repository_name, uuid)) + try: + found = model.notification.delete_repo_notification(namespace_name, repository_name, uuid) + except InvalidNotificationException: + return None + return self._notification(found) def reset_notification_number_of_failures(self, namespace_name, repository_name, uuid): return self._notification( model.notification.reset_notification_number_of_failures(namespace_name, repository_name, uuid)) - def queue_test_notification(self, notification): + def queue_test_notification(self, uuid): + try: + notification = model.notification.get_repo_notification(uuid) + except InvalidNotificationException: + return None + event_info = NotificationEvent.get_event(notification.event.name) sample_data = event_info.get_sample_data(notification) notification_data = build_notification_data(notification, sample_data) - notification_queue.put([notification.repository.namespace_user.username, notification, + notification_queue.put([notification.repository.namespace_user.username, notification.uuid, notification.event.name], json.dumps(notification_data)) + return self._notification(notification) def _notification(self, notification): + if not notification: + return None return RepositoryNotification(uuid=notification.uuid, title=notification.title, event_name=notification.event.name, @@ -52,4 +68,4 @@ class PreOCIModel(RepoNotificationInterface): -pre_oci_model = PreOCIModel() \ No newline at end of file +pre_oci_model = RepoNotificationPreOCIModel() \ No newline at end of file diff --git a/endpoints/api/test/test_models_pre_oci.py b/endpoints/api/test/test_models_pre_oci.py new file mode 100644 index 000000000..6e011d514 --- /dev/null +++ b/endpoints/api/test/test_models_pre_oci.py @@ -0,0 +1,106 @@ +import pytest +from endpoints.api.tag_models_interface import RepositoryTagHistory, Tag +from mock import Mock + +from data import model +from endpoints.api.tag_models_pre_oci import pre_oci_model + +EMPTY_REPOSITORY = 'empty_repository' +EMPTY_NAMESPACE = 'empty_namespace' +BAD_REPOSITORY_NAME = 'bad_repository_name' +BAD_NAMESPACE_NAME = 'bad_namespace_name' + + +@pytest.fixture +def get_monkeypatch(monkeypatch): + return monkeypatch + + +def mock_out_get_repository(monkeypatch, namespace_name, repository_name): + def return_none(namespace_name, repository_name): + return None + + def return_repository(namespace_name, repository_name): + return 'repository' + + if namespace_name == BAD_NAMESPACE_NAME or repository_name == BAD_REPOSITORY_NAME: + return_function = return_none + else: + return_function = return_repository + + monkeypatch.setattr(model.repository, 'get_repository', return_function) + + +def create_mock_tag(name, reversion, lifetime_start_ts, lifetime_end_ts, mock_id, docker_image_id, + manifest_list): + tag_mock = Mock() + tag_mock.name = name + image_mock = Mock() + image_mock.docker_image_id = docker_image_id + tag_mock.image = image_mock + tag_mock.reversion = reversion + tag_mock.lifetime_start_ts = lifetime_start_ts + tag_mock.lifetime_end_ts = lifetime_end_ts + tag_mock.id = mock_id + tag_mock.manifest_list = manifest_list + tag = Tag(name=name, reversion=reversion, image=image_mock, docker_image_id=docker_image_id, + lifetime_start_ts=lifetime_start_ts, lifetime_end_ts=lifetime_end_ts, + manifest_list=manifest_list) + return tag_mock, tag + + +first_mock, first_tag = create_mock_tag('tag1', 'rev1', 'start1', 'end1', 'id1', + 'docker_image_id1', []) +second_mock, second_tag = create_mock_tag('tag2', 'rev2', 'start2', 'end2', 'id2', + 'docker_image_id2', ['manifest']) + + +def mock_out_list_repository_tag_history(monkeypatch, namespace_name, repository_name, page, size, + specific_tag): + def list_empty_tag_history(repository, page, size, specific_tag): + return [], {}, False + + def list_filled_tag_history(repository, page, size, specific_tag): + tags = [first_mock, second_mock] + return tags, { + first_mock.id: first_mock.manifest_list, + second_mock.id: second_mock.manifest_list + }, len(tags) > size + + def list_only_second_tag(repository, page, size, specific_tag): + tags = [second_mock] + return tags, {second_mock.id: second_mock.manifest_list}, len(tags) > size + + if namespace_name == EMPTY_NAMESPACE or repository_name == EMPTY_REPOSITORY: + return_function = list_empty_tag_history + else: + if specific_tag == 'tag2': + return_function = list_only_second_tag + else: + return_function = list_filled_tag_history + + monkeypatch.setattr(model.tag, 'list_repository_tag_history', return_function) + + +@pytest.mark.parametrize( + 'expected, namespace_name, repository_name, page, size, specific_tag', [ + (None, BAD_NAMESPACE_NAME, 'repository_name', 1, 100, None), + (None, 'namespace_name', BAD_REPOSITORY_NAME, 1, 100, None), + (RepositoryTagHistory(tags=[], more=False), EMPTY_NAMESPACE, EMPTY_REPOSITORY, 1, 100, None), + (RepositoryTagHistory(tags=[first_tag, second_tag], more=False), 'namespace', 'repository', 1, + 100, None), + (RepositoryTagHistory(tags=[first_tag, second_tag], more=True), 'namespace', 'repository', 1, + 1, None), + (RepositoryTagHistory(tags=[second_tag], more=False), 'namespace', 'repository', 1, 100, + 'tag2'), + ]) +def test_list_repository_tag_history(expected, namespace_name, repository_name, page, size, + specific_tag, get_monkeypatch): + mock_out_get_repository(get_monkeypatch, namespace_name, repository_name) + mock_out_list_repository_tag_history(get_monkeypatch, namespace_name, repository_name, page, + size, specific_tag) + assert pre_oci_model.list_repository_tag_history(namespace_name, repository_name, page, size, + specific_tag) == expected + + + diff --git a/endpoints/api/test/test_repositorynotification.py b/endpoints/api/test/test_repositorynotification.py new file mode 100644 index 000000000..ceccf5a19 --- /dev/null +++ b/endpoints/api/test/test_repositorynotification.py @@ -0,0 +1,84 @@ +import pytest + +from mock import Mock, MagicMock + +from endpoints.api.test.shared import conduct_api_call +from endpoints.api.repositorynotification import RepositoryNotificationList, RepositoryNotification, TestRepositoryNotification +from endpoints.test.shared import client_with_identity +import endpoints.api.repositorynotification_models_interface as iface +from test.fixtures import * + +@pytest.fixture() +def authd_client(client): + with client_with_identity('devtable', client) as cl: + yield cl + +def mock_get_notification(uuid): + mock_notification = MagicMock(iface.RepositoryNotification) + if uuid == 'exists': + mock_notification.return_value = iface.RepositoryNotification( + 'exists', + 'title', + 'event_name', + 'method_name', + 'config_json', + 'event_config_json', + 2, + ) + else: + mock_notification.return_value = None + return mock_notification + +@pytest.mark.parametrize('namespace,repository,body,expected_code',[ + ('devtable', 'simple', dict(config={'url': 'http://example.com'}, event='repo_push', + method='webhook', eventConfig={}, title='test'), 201) +]) +def test_create_repo_notification(namespace, repository, body, expected_code, authd_client): + params = {'repository': namespace + '/' + repository} + conduct_api_call(authd_client, RepositoryNotificationList, 'POST', params, body, expected_code=expected_code) + +@pytest.mark.parametrize('namespace,repository,expected_code',[ + ('devtable', 'simple', 200) +]) +def test_list_repo_notifications(namespace, repository, expected_code, authd_client): + params = {'repository': namespace + '/' + repository} + resp = conduct_api_call(authd_client, RepositoryNotificationList, 'GET', params, expected_code=expected_code).json + assert len(resp['notifications']) > 0 + +@pytest.mark.parametrize('namespace,repository,uuid,expected_code',[ + ('devtable', 'simple', 'exists', 200), + ('devtable', 'simple', 'not found', 404), +]) +def test_get_repo_notification(namespace, repository, uuid, expected_code, authd_client, monkeypatch): + monkeypatch.setattr('endpoints.api.repositorynotification.model.get_repo_notification', mock_get_notification(uuid)) + params = {'repository': namespace + '/' + repository, 'uuid': uuid} + conduct_api_call(authd_client, RepositoryNotification, 'GET', params, expected_code=expected_code) + +@pytest.mark.parametrize('namespace,repository,uuid,expected_code',[ + ('devtable', 'simple', 'exists', 204), + ('devtable', 'simple', 'not found', 400), +]) +def test_delete_repo_notification(namespace, repository, uuid, expected_code, authd_client, monkeypatch): + monkeypatch.setattr('endpoints.api.repositorynotification.model.delete_repo_notification', mock_get_notification(uuid)) + params = {'repository': namespace + '/' + repository, 'uuid': uuid} + conduct_api_call(authd_client, RepositoryNotification, 'DELETE', params, expected_code=expected_code) + + +@pytest.mark.parametrize('namespace,repository,uuid,expected_code',[ + ('devtable', 'simple', 'exists', 204), + ('devtable', 'simple', 'not found', 400), +]) +def test_reset_repo_noticiation(namespace, repository, uuid, expected_code, authd_client, monkeypatch): + monkeypatch.setattr('endpoints.api.repositorynotification.model.reset_notification_number_of_failures', mock_get_notification(uuid)) + params = {'repository': namespace + '/' + repository, 'uuid': uuid} + conduct_api_call(authd_client, RepositoryNotification, 'POST', params, expected_code=expected_code) + + +@pytest.mark.parametrize('namespace,repository,uuid,expected_code',[ + ('devtable', 'simple', 'exists', 200), + ('devtable', 'simple', 'not found', 400), +]) +def test_test_repo_notification(namespace, repository, uuid, expected_code, authd_client, monkeypatch): + monkeypatch.setattr('endpoints.api.repositorynotification.model.queue_test_notification', mock_get_notification(uuid)) + params = {'repository': namespace + '/' + repository, 'uuid': uuid} + conduct_api_call(authd_client, TestRepositoryNotification, 'POST', params, expected_code=expected_code) diff --git a/endpoints/api/test/test_security.py b/endpoints/api/test/test_security.py index 68039aed7..a2dee07f2 100644 --- a/endpoints/api/test/test_security.py +++ b/endpoints/api/test/test_security.py @@ -59,7 +59,7 @@ NOTIFICATION_PARAMS = {'namespace': 'devtable', 'repository': 'devtable/simple', (RepositoryNotification, 'POST', NOTIFICATION_PARAMS, {}, None, 403), (RepositoryNotification, 'POST', NOTIFICATION_PARAMS, {}, 'freshuser', 403), (RepositoryNotification, 'POST', NOTIFICATION_PARAMS, {}, 'reader', 403), - (RepositoryNotification, 'POST', NOTIFICATION_PARAMS, {}, 'devtable', 204), + (RepositoryNotification, 'POST', NOTIFICATION_PARAMS, {}, 'devtable', 400), (RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, None, 403), (RepositoryTrust, 'POST', REPO_PARAMS, {'trust_enabled': True}, 'freshuser', 403), diff --git a/endpoints/notificationmethod.py b/endpoints/notificationmethod.py index f13423b7f..8725d9601 100644 --- a/endpoints/notificationmethod.py +++ b/endpoints/notificationmethod.py @@ -133,8 +133,8 @@ class QuayNotificationMethod(NotificationMethod): return # Lookup the target user or team to which we'll send the notification. - config_data = notification_obj.method_config_dict - status, err_message, target_users = self.find_targets(repository, config_data) + config_data = json.loads(notification_obj.config_json) + status, err_message, target_users = self.find_targets(repository.namespace_user.username, repository.name, config_data) if not status: raise NotificationMethodPerformException(err_message)