diff --git a/.github/workflows/develop_push.yml b/.github/workflows/develop_push.yml new file mode 100644 index 00000000..e9f52ecf --- /dev/null +++ b/.github/workflows/develop_push.yml @@ -0,0 +1,55 @@ +name: Develop Push + +on: + push: + branches: + - develop + +jobs: + + CodeCoverage: + + runs-on: ${{ matrix.os }} + + strategy: + matrix: + python-version: ['3.12'] + os: ['ubuntu-latest'] + + env: + OS: ${{ matrix.os }} + PYTHON: ${{ matrix.python-version }} + + name: Code Coverage OS ${{ matrix.os }} - Python ${{ matrix.python-version }} + steps: + - uses: actions/checkout@v2 + + - name: Setup python + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + # Need to update if we support other OS's + - name: Cache PIP Install + uses: actions/cache@v2 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-${{ matrix.python-version }}-pip-coverage-${{ hashFiles('**/setup.py') }}-${{ hashFiles('**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-${{ matrix.python-version }}-pip-coverage + + - name: Make Deps + run: | + make deps + pip install pytest + pip install pytest-cov + + - name: Generate coverage report + continue-on-error: true + run: | + coverage run --source brewtils -m pytest test --tb=no + coverage report -m + coverage xml + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 diff --git a/CHANGELOG.rst b/CHANGELOG.rst index f349a22b..e78cf4a4 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,36 @@ Brewtils Changelog ================== +3.25.0 +------ +TBD + +- Added Topic and Subscriber models and related access methods to easy client + +3.24.4 +------ +3/11/2024 + +- Fixed bug client passed into Plugin would not initialize the commands for Remote Plugins + +3.24.3 +------ +3/8/2024 + +- Fixed bug where Self Referencing SystemClients did not support `false` as default value when not provided + +3.24.2 +------ +3/1/24 + +- Fixed bug where Self Referencing SystemClients did not inspect the command properly for default parameters + +3.24.1 +------ +2/28/2024 + +- Self Referencing SystemClient now supports default values provided through the Parameter annotation + 3.24.0 ------ 2/13/2024 diff --git a/brewtils/__version__.py b/brewtils/__version__.py index f751140c..467dab72 100644 --- a/brewtils/__version__.py +++ b/brewtils/__version__.py @@ -1,3 +1,3 @@ # -*- coding: utf-8 -*- -__version__ = "3.24.0" +__version__ = "3.24.4" diff --git a/brewtils/models.py b/brewtils/models.py index ba2bcf10..f74ea152 100644 --- a/brewtils/models.py +++ b/brewtils/models.py @@ -37,6 +37,8 @@ "Resolvable", "Role", "User", + "Subscriber", + "Topic", ] @@ -94,8 +96,11 @@ class Events(Enum): COMMAND_PUBLISHING_BLOCKLIST_SYNC = 48 COMMAND_PUBLISHING_BLOCKLIST_REMOVE = 49 COMMAND_PUBLISHING_BLOCKLIST_UPDATE = 50 + TOPIC_CREATED = 54 + TOPIC_UPDATED = 55 + TOPIC_REMOVED = 56 - # Next: 54 + # Next: 57 class BaseModel(object): @@ -1706,3 +1711,71 @@ class RemoteUserMap(BaseModel): def __init__(self, target_garden, username): self.target_garden = target_garden self.username = username + +class Subscriber(BaseModel): + schema = "SubscriberSchema" + + def __init__( + self, + garden=None, + namespace=None, + system=None, + version=None, + instance=None, + command=None, + ): + self.garden = garden + self.namespace = namespace + self.system = system + self.version = version + self.instance = instance + self.command = command + + def __str__(self): + return "%s" % self.__dict__ + + def __repr__(self): + return ( + "" + % ( + self.garden, + self.namespace, + self.system, + self.version, + self.instance, + self.command, + ) + ) + + def __eq__(self, other): + if not isinstance(other, Subscriber): + # don't attempt to compare against unrelated types + return NotImplemented + + return ( + self.garden == other.garden + and self.namespace == other.namespace + and self.system == other.system + and self.version == other.version + and self.instance == other.instance + and self.command == other.command + ) + + +class Topic(BaseModel): + schema = "TopicSchema" + + def __init__(self, id=None, name=None, subscribers=None): # noqa # shadows built-in + self.id = id + self.name = name + self.subscribers = subscribers or [] + + def __str__(self): + return "%s: %s" % (self.name, [str(s) for s in self.subscribers]) + + def __repr__(self): + return "" % ( + self.name, + self.subscribers, + ) diff --git a/brewtils/plugin.py b/brewtils/plugin.py index 9f874aa8..cbafc0a9 100644 --- a/brewtils/plugin.py +++ b/brewtils/plugin.py @@ -217,8 +217,7 @@ def __init__(self, client=None, system=None, logger=None, **kwargs): global CLIENT # Make sure this is set after self._system if client: - self._client = client - CLIENT = client + self._set_client(client) else: self._client = None @@ -272,6 +271,9 @@ def client(self, new_client): if new_client is None: return + self._set_client(new_client) + + def _set_client(self, new_client): # Several _system properties can come from the client, so update if needed if not self._system.name: self._system.name = getattr(new_client, "_bg_name") # noqa diff --git a/brewtils/request_handling.py b/brewtils/request_handling.py index 80d01b4a..8145a7f8 100644 --- a/brewtils/request_handling.py +++ b/brewtils/request_handling.py @@ -10,6 +10,7 @@ import six from requests import ConnectionError as RequestsConnectionError +from brewtils.decorators import _parse_method import brewtils.plugin from brewtils.errors import ( BGGivesUpError, @@ -62,6 +63,14 @@ def process_command(self, request): request.parent = Request(id=str(parent_request.id)) request.has_parent = True + # check for kwargs on the target command + command = _parse_method(getattr(brewtils.plugin.CLIENT, request.command, None)) + if command: + for parameter in command.parameters: + if parameter.default is not None: + if parameter.key not in request.parameters: + request.parameters[parameter.key] = parameter.default + request.status = "IN_PROGRESS" request = self._ez_client.put_request(request) diff --git a/brewtils/rest/client.py b/brewtils/rest/client.py index 81b415f1..e35597db 100644 --- a/brewtils/rest/client.py +++ b/brewtils/rest/client.py @@ -153,6 +153,7 @@ def __init__(self, *args, **kwargs): self.user_url = self.base_url + "api/v1/users/" self.admin_url = self.base_url + "api/v1/admin/" self.forward_url = self.base_url + "api/v1/forward" + self.topic_url = self.base_url + "api/v1/topics/" # Deprecated self.logging_config_url = self.base_url + "api/v1/config/logging/" @@ -943,3 +944,70 @@ def get_tokens(self, username=None, password=None): self.session.headers["Authorization"] = "Bearer " + self.access_token return response + + @enable_auth + def get_topic(self, topic_id): + # type: (str, **Any) -> Response + """Performs a GET on the Topic URL + + Args: + topic_id: Topic id + + Returns: + Requests Response object + """ + return self.session.get(self.topic_url + topic_id) + + @enable_auth + def get_topics(self): + # type: () -> Response + """Perform a GET on the Topic URL + + Returns: + Requests Response object + """ + return self.session.get(self.topic_url) + + @enable_auth + def post_topics(self, payload): + # type: (str) -> Response + """Performs a POST on the Topic URL + + Args: + payload: New Topic definition + + Returns: + Requests Response object + """ + return self.session.post( + self.topic_url, data=payload, headers=self.JSON_HEADERS + ) + + @enable_auth + def patch_topic(self, topic_id, payload): + # type: (str, str) -> Response + """Performs a PATCH on a Topic URL + + Args: + topic_id: Topic id + payload: Serialized PatchOperation + + Returns: + Requests Response object + """ + return self.session.patch( + self.topic_url + topic_id, data=payload, headers=self.JSON_HEADERS + ) + + @enable_auth + def delete_topic(self, topic_id): + # type: (str) -> Response + """Performs a DELETE on a Topic URL + + Args: + topic_id: Topic id + + Returns: + Requests Response object + """ + return self.session.delete(self.topic_url + topic_id) diff --git a/brewtils/rest/easy_client.py b/brewtils/rest/easy_client.py index ba515916..a794ea46 100644 --- a/brewtils/rest/easy_client.py +++ b/brewtils/rest/easy_client.py @@ -1154,3 +1154,79 @@ def _check_chunked_file_validity(self, file_id): return True, metadata_json else: return False, metadata_json + + @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=FetchError) + def get_topic(self, topic_id): + """Get a topic + + Args: + topic_id: Topic id + + Returns: + The Topic + + """ + return self.client.get_topic(topic_id) + + @wrap_response(parse_method="parse_topic", parse_many=True, default_exc=FetchError) + def get_topics(self): + """Get all Topics + + Returns: + List[Topics]: List of Topics + + """ + return self.client.get_topics() + + @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=SaveError) + def create_topic(self, topic): + """Create a new Topic + + Args: + system (Topic): The Topic to create + + Returns: + Topic: The newly-created topic + + """ + return self.client.post_topics(SchemaParser.serialize_topic(topic)) + + @wrap_response(return_boolean=True, raise_404=True) + def remove_topic(self, topic_id): + """Remove a unique Topic + + Args: + topic_id: Topic id + + Returns: + bool: True if removal was successful + + Raises: + NotFoundError: Couldn't find a Topic matching given parameters + + """ + return self.client.delete_topic(topic_id) + + @wrap_response(parse_method="parse_topic", parse_many=False, default_exc=SaveError) + def update_topic(self, topic_id, add=None, remove=None): + """Update a Topic + + Args: + topic_id (str): The Topic ID + add (Optional[str]): Add subscriber + remove (Optional[str]): Remove subscriber + + Returns: + Topic: The updated topic + + """ + operations = [] + + if add: + operations.append(PatchOperation("add", value=add)) + if remove: + operations.append(PatchOperation("remove", value=remove)) + + return self.client.patch_topic( + topic_id, SchemaParser.serialize_patch(operations, many=True) + ) diff --git a/brewtils/schema_parser.py b/brewtils/schema_parser.py index 3de95c74..4f302e2d 100644 --- a/brewtils/schema_parser.py +++ b/brewtils/schema_parser.py @@ -52,6 +52,8 @@ class SchemaParser(object): "RemoteRoleSchema": brewtils.models.RemoteRole, "UserSchema": brewtils.models.User, "RemoteUserMapSchema": brewtils.models.RemoteUserMap, + "SubscriberSchema": brewtils.models.Subscriber, + "TopicSchema": brewtils.models.Topic, } logger = logging.getLogger(__name__) @@ -434,6 +436,38 @@ def parse_resolvable(cls, resolvable, from_string=False, **kwargs): resolvable, brewtils.models.Resolvable, from_string=from_string, **kwargs ) + @classmethod + def parse_subscriber(cls, subscriber, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a subscriber model object + + Args: + subscriber: The raw input + from_string: True if input is a JSON string, False if a dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + A Subscriber object + """ + return cls.parse( + subscriber, brewtils.models.Subscriber, from_string=from_string, **kwargs + ) + + @classmethod + def parse_topic(cls, topic, from_string=False, **kwargs): + """Convert raw JSON string or dictionary to a subscriber model object + + Args: + topic: The raw input + from_string: True if input is a JSON string, False if a dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + A Topic object + """ + return cls.parse( + topic, brewtils.models.Topic, from_string=from_string, **kwargs + ) + @classmethod def parse( cls, @@ -931,6 +965,46 @@ def serialize_resolvable(cls, resolvable, to_string=True, **kwargs): **kwargs ) + @classmethod + def serialize_subscriber(cls, subscriber, to_string=True, **kwargs): + """Convert a subscriber model into serialized form + + Args: + subscriber: The subscriber object(s) to be serialized + to_string: True to generate a JSON-formatted string, False to generate a + dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + Serialized representation of subscriber + """ + return cls.serialize( + subscriber, + to_string=to_string, + schema_name=brewtils.models.Subscriber.schema, + **kwargs + ) + + @classmethod + def serialize_topic(cls, topic, to_string=True, **kwargs): + """Convert a topic model into serialized form + + Args: + topic: The topic object(s) to be serialized + to_string: True to generate a JSON-formatted string, False to generate a + dictionary + **kwargs: Additional parameters to be passed to the Schema (e.g. many=True) + + Returns: + Serialized representation of topic + """ + return cls.serialize( + topic, + to_string=to_string, + schema_name=brewtils.models.Topic.schema, + **kwargs + ) + @classmethod def serialize( cls, diff --git a/brewtils/schemas.py b/brewtils/schemas.py index 530343a4..e724f959 100644 --- a/brewtils/schemas.py +++ b/brewtils/schemas.py @@ -39,6 +39,8 @@ "UserSchema", "RoleSchema", "RemoteUserMapSchema", + "SubscriberSchema", + "TopicSchema", ] # This will be updated after all the schema classes are defined @@ -577,6 +579,20 @@ class RemoteUserMapSchema(BaseSchema): target_garden = fields.Str() username = fields.Str() +class SubscriberSchema(BaseSchema): + garden = fields.Str(allow_none=True) + namespace = fields.Str(allow_none=True) + system = fields.Str(allow_none=True) + version = fields.Str(allow_none=True) + instance = fields.Str(allow_none=True) + command = fields.Str(allow_none=True) + + +class TopicSchema(BaseSchema): + id = fields.Str(allow_none=True) + name = fields.Str(allow_none=True) + subscribers = fields.List(fields.Nested(SubscriberSchema, allow_none=True)) + class UserSchema(BaseSchema): id = fields.Str(allow_none=True) @@ -625,6 +641,8 @@ class UserSchema(BaseSchema): "RemoteRole": RemoteRoleSchema, "User": UserSchema, "RemoteUserMap": RemoteUserMapSchema, + "Subscriber": SubscriberSchema, + "Topic": TopicSchema, # Compatibility for the Job trigger types "interval": IntervalTriggerSchema, "date": DateTriggerSchema, diff --git a/brewtils/test/comparable.py b/brewtils/test/comparable.py index 911622fc..2123b2f2 100644 --- a/brewtils/test/comparable.py +++ b/brewtils/test/comparable.py @@ -39,6 +39,8 @@ Resolvable, Runner, System, + Subscriber, + Topic, ) __all__ = [ @@ -62,6 +64,8 @@ "assert_request_file_equal", "assert_operation_equal", "assert_runner_equal", + "assert_subscriber_equal", + "assert_topic_equal", ] @@ -198,6 +202,7 @@ def _assert_wrapper(obj1, obj2, expected_type=None, do_raise=False, **kwargs): assert_resolvable_equal = partial(_assert_wrapper, expected_type=Resolvable) assert_connection_equal = partial(_assert_wrapper, expected_type=Connection) assert_remote_user_map_equal = partial(_assert_wrapper, expected_type=RemoteUserMap) +assert_subscriber_equal = partial(_assert_wrapper, expected_type=Subscriber) def assert_command_equal(obj1, obj2, do_raise=False): @@ -406,3 +411,15 @@ def assert_garden_equal(obj1, obj2, do_raise=False): }, do_raise=do_raise, ) + + +def assert_topic_equal(obj1, obj2, do_raise=False): + return _assert_wrapper( + obj1, + obj2, + expected_type=Topic, + deep_fields={ + "subscribers": partial(assert_subscriber_equal, do_raise=True), + }, + do_raise=do_raise, + ) diff --git a/brewtils/test/fixtures.py b/brewtils/test/fixtures.py index 79c7a047..6b4b2a9a 100644 --- a/brewtils/test/fixtures.py +++ b/brewtils/test/fixtures.py @@ -32,6 +32,8 @@ User, UserToken, Role, + Subscriber, + Topic, ) @@ -947,3 +949,34 @@ def resolvable_chunk_dict(): @pytest.fixture def bg_resolvable_chunk(resolvable_chunk_dict): return Resolvable(**resolvable_chunk_dict) + + +@pytest.fixture +def subscriber_dict(): + """Subscribers as a dictionary.""" + return { + "garden": "garden", + "namespace": "ns", + "system": "system", + "version": "1.0.0", + "instance": "inst", + "command": "run", + } + + +@pytest.fixture +def bg_subscriber(subscriber_dict): + return Subscriber(**subscriber_dict) + + +@pytest.fixture +def topic_dict(subscriber_dict): + """Topic as dict""" + return {"id": "5d174df1", "name": "foo", "subscribers": [subscriber_dict]} + + +@pytest.fixture +def bg_topic(topic_dict, bg_subscriber): + dict_copy = copy.deepcopy(topic_dict) + dict_copy["subscribers"] = [bg_subscriber] + return Topic(**dict_copy) diff --git a/test/models_test.py b/test/models_test.py index 76744edf..2e2c1e47 100644 --- a/test/models_test.py +++ b/test/models_test.py @@ -19,6 +19,8 @@ RequestFile, RequestTemplate, Role, + Subscriber, + Topic, ) from pytest_lazyfixture import lazy_fixture @@ -680,3 +682,39 @@ def test_repr(self, bg_resolvable): bg_resolvable.storage, bg_resolvable.details, ) + + +@pytest.fixture +def subscriber1(): + return Subscriber( + garden="g", namespace="n", system="s", version="v", instance="i", command="c" + ) + + +@pytest.fixture +def topic1(subscriber1): + return Topic(name="foo.*", subscribers=[subscriber1]) + + +class TestSubscriber(object): + def test_str(self, subscriber1): + assert str(subscriber1) == "%s" % subscriber1.__dict__ + + def test_repr(self, subscriber1): + assert "g" in repr(subscriber1) + assert "n" in repr(subscriber1) + assert "s" in repr(subscriber1) + assert "v" in repr(subscriber1) + assert "i" in repr(subscriber1) + assert "c" in repr(subscriber1) + + +class TestTopic: + def test_str(self, topic1, subscriber1): + assert str(topic1) == "%s: %s" % (topic1.name, [str(subscriber1)]) + + def test_repr(self, topic1, subscriber1): + assert repr(topic1) == "" % ( + topic1.name, + [subscriber1], + ) diff --git a/test/request_handling_test.py b/test/request_handling_test.py index bb97724d..ec3fe315 100644 --- a/test/request_handling_test.py +++ b/test/request_handling_test.py @@ -8,6 +8,7 @@ from mock import ANY, MagicMock, Mock from requests import ConnectionError as RequestsConnectionError +from brewtils.decorators import parameter import brewtils.plugin from brewtils.errors import ( DiscardMessageException, @@ -22,7 +23,7 @@ SuppressStacktrace, TooLargeError, ) -from brewtils.models import Command, Request, System +from brewtils.models import Command, Request, System, Parameter from brewtils.request_handling import ( HTTPRequestUpdater, LocalRequestProcessor, @@ -525,13 +526,11 @@ def command_one(self): def command_two(self): return False - return ClientTest() + @parameter(key="key", default="value", is_kwarg=True) + def command_three(self, **kwargs): + return kwargs - @pytest.fixture - def system_client(self): - return System( - commands=[Command(name="command_one"), Command(name="command_two")] - ) + return ClientTest() @pytest.fixture def resolver_mock(self): @@ -544,7 +543,7 @@ def resolve(values, **_): return resolver @pytest.fixture - def local_request_processor(self, system_client, client, resolver_mock): + def local_request_processor(self, client, resolver_mock): brewtils.plugin.CLIENT = client def return_input_side_effect(*args, **kwargs): @@ -553,9 +552,7 @@ def return_input_side_effect(*args, **kwargs): _ez_client = Mock() _ez_client.put_request.side_effect = return_input_side_effect - return LocalRequestProcessor( - system=system_client, easy_client=_ez_client, resolver=resolver_mock - ) + return LocalRequestProcessor(easy_client=_ez_client, resolver=resolver_mock) def setup_request_context(self): brewtils.plugin.request_context = threading.local() @@ -578,3 +575,10 @@ def test_process_command(self, local_request_processor): ).output == "false" ) + + assert ( + local_request_processor.process_command( + Request(command="command_three", parameters={}) + ).output + == '{"key": "value"}' + ) diff --git a/test/rest/client_test.py b/test/rest/client_test.py index 51f560b5..57e267dc 100644 --- a/test/rest/client_test.py +++ b/test/rest/client_test.py @@ -112,6 +112,7 @@ def test_non_versioned_uris(self, client, url_prefix): ("logging_config_url", "http://host:80%sapi/v1/config/logging/"), ("job_url", "http://host:80%sapi/v1/jobs/"), ("token_url", "http://host:80%sapi/v1/token/"), + ("topic_url", "http://host:80%sapi/v1/topics/"), ("user_url", "http://host:80%sapi/v1/users/"), ("admin_url", "http://host:80%sapi/v1/admin/"), ], @@ -131,6 +132,7 @@ def test_version_1_uri(self, url_prefix, client, url, expected): ("logging_config_url", "https://host:80%sapi/v1/config/logging/"), ("job_url", "https://host:80%sapi/v1/jobs/"), ("token_url", "https://host:80%sapi/v1/token/"), + ("topic_url", "https://host:80%sapi/v1/topics/"), ("user_url", "https://host:80%sapi/v1/users/"), ("admin_url", "https://host:80%sapi/v1/admin/"), ], @@ -470,3 +472,29 @@ def test_client_cert_without_username_password(self, monkeypatch): client.get_garden("somegarden") assert get_tokens_mock.called is True + + def test_get_topic(self, client, session_mock): + client.get_topic("id") + session_mock.get.assert_called_with(client.topic_url + "id") + + def test_get_topics(self, client, session_mock): + client.get_topics() + session_mock.get_assert_called_with(client.topic_url, params={}) + + def test_post_topics(self, client, session_mock): + client.post_topics(payload="payload") + session_mock.post.assert_called_with( + client.topic_url, data="payload", headers=client.JSON_HEADERS + ) + + def test_patch_topic(self, client, session_mock): + client.patch_topic("id", "payload") + session_mock.patch.assert_called_with( + client.topic_url + "id", + data="payload", + headers=client.JSON_HEADERS, + ) + + def test_delete_topic(self, client, session_mock): + client.delete_topic("id") + session_mock.delete.assert_called_with(client.topic_url + "id") diff --git a/test/rest/easy_client_test.py b/test/rest/easy_client_test.py index 9ccf8533..b58e990a 100644 --- a/test/rest/easy_client_test.py +++ b/test/rest/easy_client_test.py @@ -602,3 +602,74 @@ def test_upload_file_fail(self, client, rest_client, server_error, target_file): rest_client.post_chunked_file.return_value = server_error with pytest.raises(SaveError): assert client.upload_chunked_file(target_file, "desired_name") + + +class TestTopics(object): + class TestGet(object): + def test_success(self, client, rest_client, bg_topic, success, parser): + rest_client.get_topic.return_value = success + parser.parse_topic.return_value = bg_topic + + assert client.get_topic(bg_topic.id) == bg_topic + + def test_404(self, client, rest_client, bg_topic, not_found): + rest_client.get_topic.return_value = not_found + + with pytest.raises(NotFoundError): + client.get_topic(bg_topic.id) + + def test_create(self, client, rest_client, success, bg_topic): + rest_client.create_topic.return_value = success + client.create_topic(bg_topic) + assert rest_client.post_topics.called is True + + def test_get_all(self, client, rest_client, bg_topic, success, parser): + second_topic = copy.deepcopy(bg_topic) + second_topic.name = "topic2" + both_topics = [bg_topic, second_topic] + rest_client.get_topics.return_value = success + parser.parse_topic.return_value = both_topics + + assert client.get_topics() == both_topics + + class TestRemove(object): + def test_not_found(self, monkeypatch, client, rest_client, not_found, bg_topic): + monkeypatch.setattr( + rest_client, "delete_topic", Mock(return_value=not_found) + ) + + with pytest.raises(NotFoundError): + client.remove_topic(bg_topic.id) + + def test_id(self, client, rest_client, success, bg_topic): + rest_client.delete_topic.return_value = success + + assert client.remove_topic(bg_topic.id) + + class TestPatch(object): + def test_add_subscriber( + self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber + ): + monkeypatch.setattr(rest_client, "patch_topic", Mock(return_value=success)) + + assert client.update_topic(bg_topic.id, add=bg_subscriber) + assert rest_client.patch_topic.called is True + + def test_remove_subscriber( + self, monkeypatch, client, rest_client, success, bg_topic, bg_subscriber + ): + monkeypatch.setattr(rest_client, "patch_topic", Mock(return_value=success)) + + assert client.update_topic(bg_topic.id, remove=bg_subscriber) + assert rest_client.patch_topic.called is True + + def test_remove_subscriber_not_found( + self, monkeypatch, client, rest_client, not_found, bg_topic, bg_subscriber + ): + monkeypatch.setattr( + rest_client, "patch_topic", Mock(return_value=not_found) + ) + + with pytest.raises(NotFoundError): + assert client.update_topic(bg_topic.id, remove=bg_subscriber) + assert rest_client.patch_topic.called is True diff --git a/test/schema_parser_test.py b/test/schema_parser_test.py index 2552b814..59b899f7 100644 --- a/test/schema_parser_test.py +++ b/test/schema_parser_test.py @@ -30,7 +30,9 @@ assert_resolvable_equal, assert_role_equal, assert_runner_equal, + assert_subscriber_equal, assert_system_equal, + assert_topic_equal, ) from marshmallow.exceptions import MarshmallowError from pytest_lazyfixture import lazy_fixture @@ -194,6 +196,18 @@ def test_no_modify(self, system_dict): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + brewtils.models.Subscriber, + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + brewtils.models.Topic, + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_single(self, model, data, assertion, expected): @@ -340,6 +354,18 @@ def test_single_from_string(self): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + "parse_subscriber", + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + "parse_topic", + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_single_specific(self, method, data, assertion, expected): @@ -479,6 +505,18 @@ def test_single_specific_from_string(self): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + brewtils.models.Subscriber, + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + brewtils.models.Topic, + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_many(self, model, data, assertion, expected): @@ -611,6 +649,18 @@ def test_many(self, model, data, assertion, expected): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + "parse_subscriber", + lazy_fixture("subscriber_dict"), + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + ( + "parse_topic", + lazy_fixture("topic_dict"), + assert_topic_equal, + lazy_fixture("bg_topic"), + ), ], ) def test_many_specific(self, method, data, assertion, expected): @@ -666,6 +716,8 @@ class TestSerialize(object): (lazy_fixture("bg_operation"), lazy_fixture("operation_dict")), (lazy_fixture("bg_runner"), lazy_fixture("runner_dict")), (lazy_fixture("bg_resolvable"), lazy_fixture("resolvable_dict")), + (lazy_fixture("bg_subscriber"), lazy_fixture("subscriber_dict")), + (lazy_fixture("bg_topic"), lazy_fixture("topic_dict")), ], ) def test_single(self, model, expected): @@ -777,6 +829,16 @@ def test_single(self, model, expected): lazy_fixture("bg_resolvable"), lazy_fixture("resolvable_dict"), ), + ( + "serialize_subscriber", + lazy_fixture("bg_subscriber"), + lazy_fixture("subscriber_dict"), + ), + ( + "serialize_topic", + lazy_fixture("bg_topic"), + lazy_fixture("topic_dict"), + ), ], ) def test_single_specific(self, method, data, expected): @@ -807,6 +869,8 @@ def test_single_specific(self, method, data, expected): (lazy_fixture("bg_operation"), lazy_fixture("operation_dict")), (lazy_fixture("bg_runner"), lazy_fixture("runner_dict")), (lazy_fixture("bg_resolvable"), lazy_fixture("resolvable_dict")), + (lazy_fixture("bg_subscriber"), lazy_fixture("subscriber_dict")), + (lazy_fixture("bg_topic"), lazy_fixture("topic_dict")), ], ) def test_many(self, model, expected): @@ -905,6 +969,12 @@ class TestRoundTrip(object): assert_resolvable_equal, lazy_fixture("bg_resolvable"), ), + ( + brewtils.models.Subscriber, + assert_subscriber_equal, + lazy_fixture("bg_subscriber"), + ), + (brewtils.models.Topic, assert_topic_equal, lazy_fixture("bg_topic")), ], ) def test_parsed_start(self, model, assertion, data):