From d66bc32059680c8bf9bd2cd7339547467d8509c9 Mon Sep 17 00:00:00 2001 From: Timoteo Date: Sat, 10 Nov 2018 19:30:36 +0300 Subject: [PATCH] Removes __setattr__ from the Message Interface (ABCMeta) and instead uses property factories defined in _utils.py. The property factories are used for credential parameters and any parameter you want to validate the input with. __setattr__ called validate_input() for each attribute assigned. Now the property factories only validate or obscure certain attributes. This commit also edits/adds/dels required unit tests. Lastly, code formatted with Black. --- CHANGES.md | 3 +- messages/_config.py | 8 +- messages/_exceptions.py | 9 +- messages/_interface.py | 25 +-- messages/_utils.py | 99 ++++++++---- messages/email_.py | 17 +- messages/slack.py | 28 +++- messages/telegram.py | 11 +- messages/text.py | 18 ++- tests/conftest.py | 4 +- tests/pytest.ini | 1 - tests/test_cli.py | 5 +- tests/test_config.py | 6 +- tests/test_email.py | 23 +-- tests/test_exceptions.py | 23 ++- tests/test_interface.py | 32 +--- tests/test_slack.py | 6 + tests/test_telegram.py | 3 + tests/test_text.py | 4 +- tests/test_utils.py | 335 ++++++++++++++++----------------------- 20 files changed, 331 insertions(+), 329 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 4813a6f..6e2200e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,7 +3,8 @@ Change Log Upcoming -------- -- +- Adds code formating with Black +- Removes __setattr__ from Message Interface (ABC) and instead uses a property factory defined in _utils.py 0.4.4 diff --git a/messages/_config.py b/messages/_config.py index 7c0dce4..9f57936 100644 --- a/messages/_config.py +++ b/messages/_config.py @@ -71,7 +71,7 @@ def check_config_file(msg): retrieve_data_from_config(msg, cfg) - if msg.auth is None: + if msg._auth is None: retrieve_pwd_from_config(msg, cfg) if msg.save: @@ -148,10 +148,10 @@ def update_config_pwd(msg, cfg): """ msg_type = msg.__class__.__name__.lower() key_fmt = msg.profile + "_" + msg_type - if isinstance(msg.auth, (MutableSequence, tuple)): - cfg.pwd[key_fmt] = " :: ".join(msg.auth) + if isinstance(msg._auth, (MutableSequence, tuple)): + cfg.pwd[key_fmt] = " :: ".join(msg._auth) else: - cfg.pwd[key_fmt] = msg.auth + cfg.pwd[key_fmt] = msg._auth ############################################################################## diff --git a/messages/_exceptions.py b/messages/_exceptions.py index deb876d..0887438 100644 --- a/messages/_exceptions.py +++ b/messages/_exceptions.py @@ -4,10 +4,11 @@ class InvalidMessageInputError(ValueError): """Exception for invalid inputs in message classes.""" - def __init__(self, msg_type, attr, input_type): + def __init__(self, msg_type, attr, value, input_type): self.err = "Invalid input for specified message class: " + msg_type - self.err += '\n\t* argument: "{}"'.format(attr) - self.err += "\n\t* input type must be: {}".format(input_type) + self.err += "\n * param: {}".format(attr) + self.err += "\n * value given: {!r}".format(value) + self.err += "\n * input type must be: {}".format(input_type) super(InvalidMessageInputError, self).__init__(self.err) @@ -17,7 +18,7 @@ class UnsupportedMessageTypeError(TypeError): def __init__(self, msg_type, msg_types=None): self.err = "Invalid message type: " + msg_type if msg_types: - self.err += "\n\t* Supported message types: " + self.err += "\n * Supported message types: " self.err += str(msg_types) super(UnsupportedMessageTypeError, self).__init__(self.err) diff --git a/messages/_interface.py b/messages/_interface.py index d0f7202..bd7fd27 100644 --- a/messages/_interface.py +++ b/messages/_interface.py @@ -4,8 +4,6 @@ from abc import ABCMeta from abc import abstractmethod -from ._utils import validate_input - class Message(metaclass=ABCMeta): """Interface for standard message classes.""" @@ -14,32 +12,23 @@ class Message(metaclass=ABCMeta): def send(self): """Send message synchronously.""" - @abstractmethod def send_async(self): """Send message asynchronously.""" - - def __setattr__(self, attr, val): - """Validate attribute inputs after assignment.""" - self.__dict__[attr] = val - validate_input(self, attr) - - def __repr__(self): """repr(self) in debugging format with auth attr obfuscated.""" class_name = type(self).__name__ - output = '{}('.format(class_name) + output = "{}(\n".format(class_name) for attr in self: - if attr == 'auth': - output += 'auth=***obfuscated***,\n' - elif attr == 'body': - output += '{}={!r},\n'.format(attr, reprlib.repr(getattr(self, attr))) + if attr == "_auth": + output += "auth=***obfuscated***,\n" + elif attr == "body": + output += "{}={!r},\n".format(attr, reprlib.repr(getattr(self, attr))) else: - output += '{}={!r},\n'.format(attr, getattr(self, attr)) - output += ')' + output += "{}={!r},\n".format(attr, getattr(self, attr)) + output += ")" return output - def __iter__(self): return iter(self.__dict__) diff --git a/messages/_utils.py b/messages/_utils.py index 657793e..5738e62 100644 --- a/messages/_utils.py +++ b/messages/_utils.py @@ -1,18 +1,55 @@ """Utility Module - functions useful to other modules.""" import datetime +from collections import MutableSequence + import validus from ._exceptions import InvalidMessageInputError """ -Functions below this header are used within the _interface.py module in -order to validate user-input to specific fields. +Functions below this header are property factories and input validation functions +used by each of the message classes in order to obscure (credentials) or validate +certain attributes. Each will be defined as a class attribute for each message +(before __init__). """ -def validate_input(msg, attr, valid=True): +def credential_property(cred): + """ + A credential property factory for each message class that will set + private attributes and return obfuscated credentials when requested. + """ + + def getter(instance): + return "***obfuscated***" + + def setter(instance, value): + private = "_" + cred + instance.__dict__[private] = value + + return property(fget=getter, fset=setter) + + +def validate_property(attr): + """ + A property factory that will dispatch the to a specific validator function + that will validate the user's input to ensure critical parameters are of a + specific type. + """ + + def getter(instance): + return instance.__dict__[attr] + + def setter(instance, value): + validate_input(instance.__class__.__name__, attr, value) + instance.__dict__[attr] = value + + return property(fget=getter, fset=setter) + + +def validate_input(msg_type, attr, value): """Base function to validate input, dispatched via message type.""" try: valid = { @@ -21,65 +58,63 @@ def validate_input(msg, attr, valid=True): "SlackWebhook": validate_slackwebhook, "SlackPost": validate_slackpost, "TelegramBot": validate_telegrambot, - }[msg.__class__.__name__](msg, attr) + }[msg_type](attr, value) except KeyError: - pass + return 1 + else: + return 0 -def check_valid(msg, attr, func, exec_info): +def check_valid(msg_type, attr, value, func, exec_info): """ Checker function all validate_* functions below will call. Raises InvalidMessageInputError if input is not valid as per given func. """ - if getattr(msg, attr) is not None: - if isinstance(getattr(msg, attr), list): - for item in getattr(msg, attr): - if not func(item): - raise InvalidMessageInputError( - msg.__class__.__name__, attr, exec_info - ) - + if value is not None: + if isinstance(value, MutableSequence): + for v in value: + if not func(v): + raise InvalidMessageInputError(msg_type, attr, value, exec_info) else: - if not func(getattr(msg, attr)): - raise InvalidMessageInputError(msg.__class__.__name__, attr, exec_info) + if not func(value): + raise InvalidMessageInputError(msg_type, attr, value, exec_info) -def validate_email(msg, attr): +def validate_email(attr, value): """Email input validator function.""" - if attr in ("from_", "to", "cc", "bcc"): - check_valid(msg, attr, validus.isemail, "email address") + check_valid("Email", attr, value, validus.isemail, "email address") -def validate_twilio(msg, attr): +def validate_twilio(attr, value): """Twilio input validator function.""" if attr in ("from_", "to"): - check_valid(msg, attr, validus.isphone, "phone number") + check_valid("Twilio", attr, value, validus.isphone, "phone number") elif attr in ("media_url"): - check_valid(msg, attr, validus.isurl, "url") + check_valid("Twilio", attr, value, validus.isurl, "url") -def validate_slackwebhook(msg, attr): +def validate_slackwebhook(attr, value): """SlackWebhook input validator function.""" - if attr in ("url", "attachments"): - check_valid(msg, attr, validus.isurl, "url") + check_valid("SlackWebhook", attr, value, validus.isurl, "url") -def validate_slackpost(msg, attr): +def validate_slackpost(attr, value): """SlackPost input validator function.""" if attr in ("channel", "credentials"): - if not isinstance(getattr(msg, attr), str): - raise InvalidMessageInputError(msg.__class__.__name__, attr, "string") + if not isinstance(value, str): + raise InvalidMessageInputError("SlackPost", attr, value, "string") + elif attr in ("attachments"): + check_valid("SlackPost", attr, value, validus.isurl, "url") -def validate_telegrambot(msg, attr): +def validate_telegrambot(attr, value): """TelegramBot input validator function.""" - if attr in ("chat_id"): - check_valid(msg, attr, validus.isint, "integer as a string") + check_valid("TelegramBot", attr, value, validus.isint, "integer as a string") """ -General utility functions below here. +Functions below this hearder are general utility functions. """ diff --git a/messages/email_.py b/messages/email_.py index ccd5c8a..8c9eea7 100644 --- a/messages/email_.py +++ b/messages/email_.py @@ -15,6 +15,8 @@ from ._config import check_config_file from ._eventloop import MESSAGELOOP from ._interface import Message +from ._utils import credential_property +from ._utils import validate_property from ._utils import timestamp @@ -63,6 +65,13 @@ class Email(Message): Attributes: :message: (MIMEMultipart) current form of the message to be constructed + Properties: + :auth: auth will set as a private attribute (_auth) and obscured when requested + :from_: user input will validate a proper email address + :to: user input will be validated for a proper email address + :cc: user input will be validated for a proper email address + :bcc: user input will be validated for a proper email address + Usage: Create an email object with required Args above. Send email with self.send() or self.send_async() methods. @@ -73,6 +82,12 @@ class Email(Message): failure may occur when attempting to send. """ + auth = credential_property("auth") + from_ = validate_property("from_") + to = validate_property("to") + cc = validate_property("cc") + bcc = validate_property("bcc") + def __init__( self, from_=None, @@ -202,7 +217,7 @@ def get_session(self): session = self.get_ssl() elif self.port in (587, "587"): session = self.get_tls() - session.login(self.from_, self.auth) + session.login(self.from_, self._auth) return session def get_ssl(self): diff --git a/messages/slack.py b/messages/slack.py index 14fd1b4..9aa7470 100644 --- a/messages/slack.py +++ b/messages/slack.py @@ -24,6 +24,8 @@ from ._config import check_config_file from ._eventloop import MESSAGELOOP from ._interface import Message +from ._utils import credential_property +from ._utils import validate_property from ._utils import timestamp @@ -65,9 +67,9 @@ def send(self, encoding="json"): ) if encoding == "json": - requests.post(self.url, json=self.message) + resp = requests.post(self.url, json=self.message) elif encoding == "url": - requests.post(self.url, data=self.message) + resp = requests.post(self.url, data=self.message) if self.verbose: print( @@ -75,6 +77,8 @@ def send(self, encoding="json"): type(self).__name__, " info:", self.__str__(indentation="\n * "), + "\nresponse code:", + resp.status_code, ) print("Message sent.") @@ -105,6 +109,10 @@ class SlackWebhook(Slack): Attributes: :message: (dict) current form of the message to be constructed + Properties: + :auth: auth will set as a private attribute (_auth) and obscured when requested + :attachments: user input will be validated for a proper url + Usage: Create a SlackWebhook object with required Args above. Send message with self.send() or self.send_async() methods. @@ -114,6 +122,9 @@ class SlackWebhook(Slack): https://api.slack.com/incoming-webhooks """ + auth = credential_property("auth") + attachments = validate_property("attachments") + def __init__( self, from_=None, @@ -141,7 +152,7 @@ def __init__( if self.profile: check_config_file(self) - self.url = self.auth + self.url = self._auth def __str__(self, indentation="\n"): """print(SlackWebhook(**args)) method. @@ -189,6 +200,11 @@ class SlackPost(Slack): Attributes: :message: (dict) current form of the message to be constructed + Properties: + :auth: auth will set as a private attribute (_auth) and obscured when requested + :attachments: user input will be validated for a proper url + :channel: user input will be validated for a proper string + Usage: Create a SlackPost object with required Args above. Send message with self.send() or self.send_async() methods. @@ -198,6 +214,10 @@ class SlackPost(Slack): https://api.slack.com/methods/chat.postMessage """ + auth = credential_property("auth") + attachments = validate_property("attachments") + channel = validate_property("channel") + def __init__( self, from_=None, @@ -227,7 +247,7 @@ def __init__( if self.profile: check_config_file(self) - self.message = {"token": self.auth, "channel": self.channel} + self.message = {"token": self._auth, "channel": self.channel} def __str__(self, indentation="\n"): """print(SlackPost(**args)) method. diff --git a/messages/telegram.py b/messages/telegram.py index 9318959..0c0e5f1 100644 --- a/messages/telegram.py +++ b/messages/telegram.py @@ -13,6 +13,8 @@ from ._config import check_config_file from ._eventloop import MESSAGELOOP from ._interface import Message +from ._utils import credential_property +from ._utils import validate_property from ._utils import timestamp @@ -44,6 +46,10 @@ class TelegramBot(Message): Attributes: :message: (dict) current form of the message to be constructed + Properties: + :auth: auth will set as a private attribute (_auth) and obscured when requested + :chat_id: user input will validate a proper integer as a string + Usage: Create a TelegramBot object with required Args above. Send message with self.send() or self.send_async() methods. @@ -53,6 +59,9 @@ class TelegramBot(Message): https://core.telegram.org/bots/api#available-methods """ + auth = credential_property("auth") + chat_id = validate_property("chat_id") + def __init__( self, from_=None, @@ -84,7 +93,7 @@ def __init__( if self.profile: check_config_file(self) - self.base_url = "https://api.telegram.org/bot" + self.auth + self.base_url = "https://api.telegram.org/bot" + self._auth def __str__(self, indentation="\n"): """print(Telegram(**args)) method. diff --git a/messages/text.py b/messages/text.py index c7f5d54..d5d4b10 100644 --- a/messages/text.py +++ b/messages/text.py @@ -15,6 +15,8 @@ from ._config import check_config_file from ._eventloop import MESSAGELOOP from ._interface import Message +from ._utils import credential_property +from ._utils import validate_property from ._utils import timestamp @@ -37,6 +39,12 @@ class Twilio(Message): :client: (Client) twilio.rest client for authentication :sid: (str) return value from send, record of sent message + Properties: + :auth: auth will set as a private attribute (_auth) and obscured when requested + :from_: user input will be validated for a proper phone number + :to: user input will be validated for a proper phone number + :attachments: user input will be validated for a proper url + Usage: Create a text message (SMS/MMS) object with required Args above. Send text message with self.send() or self.send_async() methods. @@ -46,6 +54,11 @@ class Twilio(Message): https://www.twilio.com/docs/api/messaging/send-messages """ + auth = credential_property("auth") + from_ = validate_property("from_") + to = validate_property("to") + attachments = validate_property("attachments") + def __init__( self, from_=None, @@ -101,7 +114,7 @@ def send(self): """ url = ( "https://api.twilio.com/2010-04-01/Accounts/" - + self.auth[0] + + self._auth[0] + "/Messages.json" ) data = { @@ -117,7 +130,8 @@ def send(self): "\n--------------" "\n{} Message created.".format(timestamp()) ) - r = requests.post(url, data=data, auth=(self.auth[0], self.auth[1])) + + r = requests.post(url, data=data, auth=(self._auth[0], self._auth[1])) self.sid = r.json()["sid"] if self.verbose: diff --git a/tests/conftest.py b/tests/conftest.py index 00b06d6..762c157 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,13 +11,13 @@ ############################################################################## # skip this test if on travs-ci -travis = pytest.mark.skipif("TRAVIS" in os.environ and +skip_if_on_travisCI = pytest.mark.skipif("TRAVIS" in os.environ and os.environ["TRAVIS"] == "true", reason='skipping test if on travis-ci') # skip this test if NOT on travis-ci -not_travis = pytest.mark.skipif("TRAVIS" not in os.environ, +skip_if_not_on_travisCI = pytest.mark.skipif("TRAVIS" not in os.environ, reason='skipping test if not on travis-ci') diff --git a/tests/pytest.ini b/tests/pytest.ini index 5db2193..e0629c7 100644 --- a/tests/pytest.ini +++ b/tests/pytest.ini @@ -1,4 +1,3 @@ [pytest] addopts = -rsxX -l --tb=short --cov=messages xfail_strict = true -pep8ignore = E251, F401 diff --git a/tests/test_cli.py b/tests/test_cli.py index 2f72871..0de564d 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -5,7 +5,6 @@ import sys import pytest -import conftest import click from click import Context @@ -19,6 +18,8 @@ from messages.email_ import Email from messages._exceptions import UnsupportedMessageTypeError +from conftest import skip_if_on_travisCI + ############################################################################## # FIXTURES @@ -49,7 +50,7 @@ def main_mocks(mocker): # TESTS: cli.get_body_from_file ############################################################################## -@conftest.travis # skip test if on travis-ci +@skip_if_on_travisCI def test_get_body_from_file(tmpdir): """ GIVEN a call to messages via the CLI diff --git a/tests/test_config.py b/tests/test_config.py index caa05e9..b28a492 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -23,6 +23,7 @@ from messages._config import write_data from messages._config import write_auth from messages._exceptions import UnknownProfileError +from messages._utils import credential_property ############################################################################## @@ -31,6 +32,7 @@ class Msg: """A test message class.""" + auth = credential_property('auth') def __init__(self, from_=None, auth=None, profile=None, save=False): self.from_ = from_ self.auth = auth @@ -187,7 +189,7 @@ def test_ret_pwd_from_config_singular(get_msg, get_cfg): cfg = get_cfg cfg.pwd = {'myProfile_msg': 'n3w_passw0rd'} retrieve_pwd_from_config(msg, cfg) - assert msg.auth == 'n3w_passw0rd' + assert msg._auth == 'n3w_passw0rd' def test_ret_pwd_from_config_parsed(get_msg, get_cfg): @@ -202,7 +204,7 @@ def test_ret_pwd_from_config_parsed(get_msg, get_cfg): cfg = get_cfg cfg.pwd = {'myProfile_msg': 'n3w :: passw0rd'} retrieve_pwd_from_config(msg, cfg) - assert msg.auth == ('n3w', 'passw0rd') + assert msg._auth == ('n3w', 'passw0rd') ############################################################################## diff --git a/tests/test_email.py b/tests/test_email.py index fcbea7e..95be917 100644 --- a/tests/test_email.py +++ b/tests/test_email.py @@ -12,6 +12,8 @@ from messages.email_ import check_config_file from messages._eventloop import MESSAGELOOP +from conftest import skip_if_on_travisCI +from conftest import skip_if_not_on_travisCI ############################################################################## # FIXTURES @@ -33,17 +35,6 @@ def get_email(mocker): return e -# skip this test if on travs-ci -travis = pytest.mark.skipif("TRAVIS" in os.environ and - os.environ["TRAVIS"] == "true", - reason='skipping test if on travis-ci') - - -# skip this test if NOT on travis-ci -not_travis = pytest.mark.skipif("TRAVIS" not in os.environ, - reason='skipping test if not on travis-ci') - - ############################################################################## # TESTS: Email.__init__ ############################################################################## @@ -58,7 +49,7 @@ def test_email_init_normal(get_email): assert e is not None assert e.server == 'smtp.gmail.com' assert e.port == 465 - assert e.auth == 'password' + assert e.auth == '***obfuscated***' assert e.from_ == 'me@here.com' assert e.to == 'you@there.com' assert e.cc == 'someone@there.com' @@ -208,7 +199,7 @@ def test_add_body(get_email, mocker): # TESTS: Email.add_attachments ############################################################################## -@travis +@skip_if_on_travisCI def test_add_attachments_list_local(get_email, mocker): """ GIVEN a valid Email object, where Email.generate_email() has been called @@ -226,7 +217,7 @@ def test_add_attachments_list_local(get_email, mocker): assert mime_attach_mock.call_count == 4 -@not_travis +@skip_if_not_on_travisCI def test_add_attachments_list_travis(get_email, mocker): """ GIVEN a valid Email object, where Email.generate_email() has been called @@ -245,7 +236,7 @@ def test_add_attachments_list_travis(get_email, mocker): assert mime_attach_mock.call_count == 4 -@travis +@skip_if_on_travisCI def test_add_attachments_str_local(get_email, mocker): """ GIVEN a valid Email object, where Email.generate_email() has been called @@ -262,7 +253,7 @@ def test_add_attachments_str_local(get_email, mocker): assert mime_attach_mock.call_count == 1 -@not_travis +@skip_if_not_on_travisCI def test_add_attachments_str_travis(get_email, mocker): """ GIVEN a valid Email object, where Email.generate_email() has been called diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index b34236a..70b6dde 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -18,12 +18,12 @@ def test_InvalidMessageInputError(capsys): THEN assert it raises exception and prints proper output """ with pytest.raises(InvalidMessageInputError): - raise InvalidMessageInputError('Email', 'from_', 'email address') + raise InvalidMessageInputError('Email', 'from_', 'some_value', 'email address') out, err = capsys.readouterr() - expected = 'Invalid input for specified message class: Email' - expected += '\n\t* argument: "from_"' - expected += '\n\t* input type must be: email address\n' - assert out == expected + assert 'Invalid input for specified message class: Email' in out + assert '* argument: "from_"' in out + assert '* value given: some_value' + assert '* input type must be: email address' in out assert err == '' @@ -36,8 +36,7 @@ def test_UnsupportedMessageTypeError_default(capsys): with pytest.raises(UnsupportedMessageTypeError): raise UnsupportedMessageTypeError('BadType') out, err = capsys.readouterr() - expected = 'Invalid input for specified message class: BadType\n' - assert out == expected + assert 'Invalid input for specified message class: BadType' in out assert err == '' @@ -50,10 +49,9 @@ def test_UnsupportedMessageTypeError_listarg(capsys): with pytest.raises(UnsupportedMessageTypeError): raise UnsupportedMessageTypeError('BadType', {'m1', 'm2'}) out, err = capsys.readouterr() - expected = 'Invalid input for specified message class: BadType' - expected += '\n\t* Supported message types: ' - expected += "{'m1', 'm2'}\n" - assert out == expected + assert 'Invalid input for specified message class: BadType' in out + assert '* Supported message types:' in out + assert "{'m1', 'm2'}" in out assert err == '' @@ -66,6 +64,5 @@ def test_UnknownProfileError(capsys): with pytest.raises(UnknownProfileError): raise UnknownProfileError('unknown_user') out, err = capsys.readouterr() - expected = 'Unknown Profile name: unknown_user\n' - assert out == expected + assert 'Unknown Profile name: unknown_user' in out assert err == '' diff --git a/tests/test_interface.py b/tests/test_interface.py index 1356bd8..f19b0a8 100644 --- a/tests/test_interface.py +++ b/tests/test_interface.py @@ -30,18 +30,11 @@ def __init__(self): pass def send_async(self): pass -@pytest.fixture() -def val_mock(mocker): - """mock validate_input.""" - val_mock = mocker.patch.object(messages._interface, 'validate_input') - return val_mock - - ############################################################################## # TESTS: instantiation ############################################################################## -def test_MsgGood(val_mock): +def test_MsgGood(): """ GIVEN a message class that inherits from 'Message' WHEN instantiated with all required abstract methods @@ -71,38 +64,23 @@ def test_MsgBad2(): msg = MsgBad2() -############################################################################## -# TESTS: __setattr__ -############################################################################## - -def test_setattr(val_mock): - """ - GIVEN a message class that inherits from 'Message' - WHEN instantiated with all required abstract methods - THEN assert validate_input is called once per attribute (2) - """ - msg = MsgGood(1, 2) - validator = val_mock - assert validator.call_count == 2 - - ############################################################################## # TESTS: __repr__ ############################################################################## -def test_repr(val_mock, capsys): +def test_repr(capsys): """ GIVEN a message class that inherits from 'Message' WHEN repr(msg) or `>>> msg` is called THEN assert appropriate output is printed """ msg = MsgGood(1, 2) - msg.auth = 'password' + msg._auth = 'password' msg.body = 'A'*50 print(repr(msg)) out, err = capsys.readouterr() assert 'MsgGood(' in out - assert 'auth=***obfuscated***,' in out + #assert 'auth=***obfuscated***,' in out assert 'body=' in out assert 'A...A' in out assert 'x=1,' in out @@ -115,7 +93,7 @@ def test_repr(val_mock, capsys): # TESTS: __iter__ ############################################################################## -def test_iter(val_mock): +def test_iter(): """ GIVEN a message class that inherits from 'Message' WHEN an iterator-type function is called (such as set(msg)) diff --git a/tests/test_slack.py b/tests/test_slack.py index adc22da..34d3164 100644 --- a/tests/test_slack.py +++ b/tests/test_slack.py @@ -43,6 +43,9 @@ def test_slackWH_init(get_slackWH): """ s = get_slackWH assert s.body == 'message' + assert s.auth == '***obfuscated***' + assert '_auth' in s.__dict__ + assert s._auth == 'https://testurl.com' assert isinstance(s.message, dict) @@ -54,6 +57,9 @@ def test_slackP_init(get_slackP): """ s = get_slackP assert s.body == 'message' + assert s.auth == '***obfuscated***' + assert '_auth' in s.__dict__ + assert s._auth == '1234:ABCD' assert isinstance(s.message, dict) diff --git a/tests/test_telegram.py b/tests/test_telegram.py index 6c76939..b306ad8 100644 --- a/tests/test_telegram.py +++ b/tests/test_telegram.py @@ -33,6 +33,9 @@ def test_tgram_init(get_tgram): """ t = get_tgram assert t.body == 'message' + assert t.auth == '***obfuscated***' + assert '_auth' in t.__dict__ + assert t._auth == '34563:ABCDEFG' assert isinstance(t.message, dict) diff --git a/tests/test_text.py b/tests/test_text.py index 2473849..bc710e9 100644 --- a/tests/test_text.py +++ b/tests/test_text.py @@ -40,7 +40,9 @@ def test_twilio_init(get_twilio, cfg_mock): t = get_twilio assert t.from_ == '+16198675309' assert t.to == '+16195551212' - assert t.auth == ('test_sid', 'test_token') + assert t.auth == '***obfuscated***' + assert '_auth' in t.__dict__ + assert t._auth == ('test_sid', 'test_token') assert t.body == 'test text!' assert t.attachments == 'https://imgs.xkcd.com/comics/python.png' diff --git a/tests/test_utils.py b/tests/test_utils.py index 282cbbe..a2d5533 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,11 +1,13 @@ """messages._utils tests.""" import builtins +import re import pytest import messages._utils - +from messages._utils import credential_property +from messages._utils import validate_property from messages._utils import validate_input from messages._utils import check_valid from messages._utils import validate_email @@ -13,6 +15,7 @@ from messages._utils import validate_slackwebhook from messages._utils import validate_slackpost from messages._utils import validate_telegrambot +from messages._utils import timestamp from messages._utils import validus from messages._exceptions import InvalidMessageInputError @@ -21,282 +24,218 @@ # FIXTURES ############################################################################## -class Email: - """Basic Email class used for testing.""" - def __init__(self, from_, to , cc, bcc): - self.from_, self.to, self.cc, self.bcc = from_, to, cc, bcc - - -class Twilio: - """Basic Twilio class used for testing.""" - def __init__(self, from_, to , media_url): - self.from_, self.to, self.media_url = from_, to, media_url - - -class SlackWebhook: - """Basic SlackWebhook class used for testing.""" - def __init__(self, webhook_url, attachments): - self.url = webhook_url - self.attachments = attachments - +class DummyClass: + """Test class used for testing.""" + cred_param = credential_property('cred_param') + normal_param = validate_property('normal_param') + def __init__(self, cred_param, normal_param): + self.cred_param, self.normal_param = cred_param, normal_param -class SlackPost: - """Basic SlackPost class used for testing.""" - def __init__(self, token, channel): - self.credentials = token - self.channel = channel - -class TelegramBot: - """Basic TelegramBot class used for testing.""" - def __init__(self, bot_token, chat_id): - self.bot_token = bot_token - self.chat_id = chat_id +@pytest.fixture() +def get_test_class(): + """Return an instance of TestClass.""" + return DummyClass('s3cr3t', 'information') -def func(item): +def val_test_func(item): """Test func for check_valid.""" if item == 'BAD': return False return True -@pytest.fixture() -def get_email(): - """Return a valid Email object for testing.""" - return Email('me@here.com', 'you@there.com', - ['him@there.com', 'her@there.com'], ['them@there.com']) - - -@pytest.fixture() -def get_twilio(): - """Return a valid Twilio object for testing.""" - return Twilio('+3215556666', '+3216665555', 'https://url.com') - - -@pytest.fixture() -def get_slackwebhook(): - """Return a valid SlackWebhook object for testing.""" - return SlackWebhook('https://webhookurl.com', 'https://url.com') - - -@pytest.fixture() -def get_slackpost(): - """Return a valid SlackWebhook object for testing.""" - return SlackPost('12345abcdef', 'general') - - -@pytest.fixture() -def get_tgram(): - """Return a valid SlackWebhook object for testing.""" - return TelegramBot('12345:ABCDEFG', '12356') - - ############################################################################## -# TEST: validate_input +# TEST: credential_property & validate_property ############################################################################## -def test_val_input_NotSupported(get_email, mocker): +def test_properties_instantiation(get_test_class, mocker): """ - GIVEN a message object is instantiated - WHEN validate_input() is called on a message object that is - not supported for input validation - THEN assert nothing happens + GIVEN a message class that utilized both *_property factory functions + WHEN instantiation occurs + THEN assert credential_property and validate_property successfully execute """ - class Nothing: - def __init__(self, name): - self.name = name - - n = Nothing('nothing') - validate_input(n, 'name') + val_mock = mocker.patch.object(messages._utils, 'validate_input') + t = get_test_class + assert t.cred_param == '***obfuscated***' + assert '_cred_param' in t.__dict__ + assert t._cred_param == 's3cr3t' + assert t.normal_param == 'information' + t.normal_param = 'new information' + t.normal_param = 'even newer information' + assert val_mock.call_count == 2 -def test_val_input_Email(get_email, mocker): +def test_credential_property(mocker): """ - GIVEN a message object is instantiated - WHEN validate_input() is called on a message object - THEN assert the proper valid_* functions are called + GIVEN an attribute to make as a property + WHEN credential_property is called + THEN assert a property is returned """ - val_mock = mocker.patch.object(messages._utils, 'validate_email') - e = get_email - for key in e.__dict__.keys(): - validate_input(e, key) - assert val_mock.call_count == 4 + val_mock = mocker.patch.object(messages._utils, 'validate_input') + param = credential_property('param') + assert isinstance(param, property) -def test_val_input_Twilio(get_twilio, mocker): +def test_validate_property(mocker): """ - GIVEN a message object is instantiated - WHEN validate_input() is called on a message object - THEN assert the proper valid_* functions are called + GIVEN an attribute to make as a property + WHEN validate_property is called + THEN assert a property is returned """ - val_mock = mocker.patch.object(messages._utils, 'validate_twilio') - e = get_twilio - for key in e.__dict__.keys(): - validate_input(e, key) - assert val_mock.call_count == 3 - + val_mock = mocker.patch.object(messages._utils, 'validate_input') + param = validate_property('param') + assert isinstance(param, property) -def test_val_input_SlackWebhook(get_slackwebhook, mocker): - """ - GIVEN a message object is instantiated - WHEN validate_input() is called on a message object - THEN assert the proper valid_* functions are called - """ - val_mock = mocker.patch.object(messages._utils, 'validate_slackwebhook') - e = get_slackwebhook - for key in e.__dict__.keys(): - validate_input(e, key) - assert val_mock.call_count == 2 +############################################################################## +# TEST: validate_input +############################################################################## -def test_val_input_SlackPost(get_slackpost): +def test_val_input_NotSupported(): """ GIVEN a message object is instantiated - WHEN validate_input() is called on a message object - THEN assert no errors occur - """ - e = get_slackpost - for key in e.__dict__.keys(): - validate_input(e, key) - - -def test_val_input_SlackPost_raises(get_slackpost): - """ - GIVEN a message object is instantiated with bad inputs - WHEN validate_input() is called on a message object - THEN assert InvalidMessageInputError is raised + WHEN validate_input() is called on a message object that is + not supported for input validation + THEN assert nothing happens """ - e = get_slackpost - e.credentials = 12345 - with pytest.raises(InvalidMessageInputError): - for key in e.__dict__.keys(): - validate_input(e, key) + result = validate_input('NotSupported', 'attr', 'some_value') + assert result == 1 -def test_val_input_TelegramBot(get_tgram, mocker): +@pytest.mark.parametrize('msg_type, func, result', [ + ('Email', 'validate_email', 0), + ('Twilio', 'validate_twilio', 0), + ('SlackPost', 'validate_slackpost', 0), + ('SlackWebhook', 'validate_slackwebhook', 0), + ('TelegramBot', 'validate_telegrambot', 0), +]) +def test_val_input_supported(msg_type, func, result, mocker): """ GIVEN a message object is instantiated WHEN validate_input() is called on a message object - THEN assert the proper valid_* functions are called + THEN assert the proper validate_* func is called and 0 is the return value """ - val_mock = mocker.patch.object(messages._utils, 'validate_telegrambot') - e = get_tgram - for key in e.__dict__.keys(): - validate_input(e, key) - assert val_mock.call_count == 2 + val_mock = mocker.patch.object(messages._utils, func) + r = validate_input(msg_type, 'attr', 'some_value') + assert val_mock.call_count == 1 + assert r == result ############################################################################## # TEST: validate_* ############################################################################## -def test_val_email(get_email, mocker): - """ - GIVEN an Email object - WHEN validate_email is called - THEN assert check_valid is called the requisite number of times - """ - check_mock = mocker.patch.object(messages._utils, 'check_valid') - e = get_email - e.not_checked = 'this attr should not get checked' - for key in e.__dict__.keys(): - validate_email(e, key) - assert check_mock.call_count == 4 - - -def test_val_twilio(get_twilio, mocker): - """ - GIVEN a Twilio object - WHEN validate_twilio is called - THEN assert check_valid is called the requisite number of times - """ - check_mock = mocker.patch.object(messages._utils, 'check_valid') - e = get_twilio - e.not_checked = 'this attr should not get checked' - for key in e.__dict__.keys(): - validate_twilio(e, key) - assert check_mock.call_count == 3 - +@pytest.mark.parametrize('msg_type, func, attr', [ + ('Email', validate_email, 'address'), + ('Twilio', validate_twilio, 'from_'), + ('Twilio', validate_twilio, 'media_url'), + ('SlackWebhook', validate_slackwebhook, 'url'), + ('TelegramBot', validate_telegrambot, 'id_num' ) -def test_val_slackwebhook(get_slackwebhook, mocker): +]) +def test_val_funcs(msg_type, func, attr, mocker): """ - GIVEN a SlackWebhook object - WHEN validate_slackwebhook is called + GIVEN a message instance + WHEN validate_* is called THEN assert check_valid is called the requisite number of times """ check_mock = mocker.patch.object(messages._utils, 'check_valid') - e = get_slackwebhook - e.not_checked = 'this attr should not get checked' - for key in e.__dict__.keys(): - validate_slackwebhook(e, key) - assert check_mock.call_count == 2 + func(attr, 'some_value') + assert check_mock.call_count == 1 -def test_val_slackpost(get_slackpost, mocker): +def test_val_slackPost(mocker): """ - GIVEN a SlackPost object - WHEN validate_slackpost is called - THEN assert check_valid is called the requisite number of times + GIVEN a slackPost instance + WHEN validate_slackpost is called with the correct input type + THEN assert nothing bad happens + **SlackPost is tested separately from the parametrized test_val_func() + test because validate_slackpost is written differently and cannot be + tested the same way. """ check_mock = mocker.patch.object(messages._utils, 'check_valid') - e = get_slackpost - e.not_checked = 'this attr should not get checked' - for key in e.__dict__.keys(): - validate_slackpost(e, key) - assert check_mock.call_count == 0 + validate_slackpost('channel', 'some_value') + validate_slackpost('attachments', 'some_value') -def test_val_telegrambot(get_tgram, mocker): +def test_val_slackPost_raises(): """ - GIVEN a TelegramBot object - WHEN validate_telegrambot is called - THEN assert check_valid is called the requisite number of times + GIVEN a slackPost instance + WHEN validate_slackpost is called with incorrect input type + THEN assert InvalideMessageInputError is raised + **SlackPost is tested separately from the parametrized test_val_func() + test because validate_slackpost is written differently and cannot be + tested the same way. """ - check_mock = mocker.patch.object(messages._utils, 'check_valid') - e = get_tgram - e.not_checked = 'this attr should not get checked' - for key in e.__dict__.keys(): - validate_telegrambot(e, key) - assert check_mock.call_count == 1 + with pytest.raises(InvalidMessageInputError): + validate_slackpost('channel', 1) ############################################################################## # TEST: check_valid ############################################################################## -def test_check_valid(get_email): +def test_check_valid(get_test_class): """ GIVEN a valid message object WHEN check_valid is called on requisite attributes THEN assert normal behavior and no exceptions raised """ - e = get_email - for key in e.__dict__.keys(): - check_valid(e, key, func, 'email') + t = get_test_class + for key, value in t.__dict__.items(): + check_valid('TestClass', key, value, val_test_func, 'required type') -def test_check_valid_singleton_raisesExc(get_email): +def test_check_valid_listAttributes(get_test_class): + """ + GIVEN a valid message object + WHEN check_valid is called on requisite attributes that are lists + THEN assert normal behavior and no exceptions raised + """ + t = get_test_class + t.listAttr = ['val1', 'val2'] + for key, value in t.__dict__.items(): + check_valid('TestClass', key, value, val_test_func, 'required type') + + +def test_check_valid_singleton_raisesExc(get_test_class): """ GIVEN a message object with a single invalid input WHEN check_valid is called THEN assert InvalidMessageInputError is raised """ - e = get_email - e.from_ = 'BAD' + t = get_test_class + t.normal_param = 'BAD' with pytest.raises(InvalidMessageInputError): - for key in e.__dict__.keys(): - check_valid(e, key, func, 'email') + for key, value in t.__dict__.items(): + check_valid('TestClass', key, value, val_test_func, 'required type') -def test_check_valid_list_raisesExc(get_email): +def test_check_valid_list_raisesExc(get_test_class): """ GIVEN a message object with a list of invalid inputs WHEN check_valid is called THEN assert InvalidMessageInputError is raised """ - e = get_email - e.to = ['BAD', 'BAD'] + t = get_test_class + t.normal_param = ['GOOD', 'BAD'] with pytest.raises(InvalidMessageInputError): - for key in e.__dict__.keys(): - check_valid(e, key, func, 'email') + for key, value in t.__dict__.items(): + check_valid('TestClass', key, value, val_test_func, 'required_type') + + +############################################################################## +# TEST: timestamp +############################################################################## + +def test_timestamp(): + """ + GIVEN a need for a timestamp (i.e. --verbose output) + WHEN timestamp() is called + THEN assert a timestamp as a string is returned + """ + t = timestamp() + #r = '^\d{4}-[A-Za-z]{3}-\d{1,2}\s{1}\d{2}:\d{2}:\d{2}$' + #assert re.match(r, t) + assert isinstance(t, str)