diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 9fe99f953..60a209b61 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,7 +8,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ['3.7', '3.8', '3.9', '3.10'] runs-on: ${{ matrix.os }} diff --git a/README.md b/README.md index f14c23230..b21c2665d 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Use the Tableau Server Client (TSC) library to increase your productivity as you * Create users and groups. * Query projects, sites, and more. -This repository contains Python source code for the library and sample files showing how to use it. Python versions 3.6 and up are supported. +This repository contains Python source code for the library and sample files showing how to use it. As of May 2022, Python versions 3.7 and up are supported. To see sample code that works directly with the REST API (in Java, Python, or Postman), visit the [REST API Samples](https://github.com/tableau/rest-api-samples) repo. diff --git a/out.pdf b/out.pdf deleted file mode 100644 index 3fb9ba6de..000000000 Binary files a/out.pdf and /dev/null differ diff --git a/samples/add_default_permission.py b/samples/add_default_permission.py index 56d3afdf1..829190359 100644 --- a/samples/add_default_permission.py +++ b/samples/add_default_permission.py @@ -1,6 +1,6 @@ #### # This script demonstrates how to add default permissions using TSC -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. # # In order to demonstrate adding a new default permission, this sample will create # a new project and add a new capability to the new project, for the default "All users" group. diff --git a/samples/create_group.py b/samples/create_group.py index 16016398d..3875ffea5 100644 --- a/samples/create_group.py +++ b/samples/create_group.py @@ -2,7 +2,7 @@ # This script demonstrates how to create a group using the Tableau # Server Client. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### diff --git a/samples/create_project.py b/samples/create_project.py index 6271f3d93..1fa563fca 100644 --- a/samples/create_project.py +++ b/samples/create_project.py @@ -4,7 +4,7 @@ # parent_id. # # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/create_schedules.py b/samples/create_schedules.py index 4fe6db5a4..87b43dbca 100644 --- a/samples/create_schedules.py +++ b/samples/create_schedules.py @@ -2,7 +2,7 @@ # This script demonstrates how to create schedules using the Tableau # Server Client. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### diff --git a/samples/export.py b/samples/export.py index 5c12b712f..4c26770b9 100644 --- a/samples/export.py +++ b/samples/export.py @@ -2,7 +2,7 @@ # This script demonstrates how to export a view using the Tableau # Server Client. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/filter_sort_groups.py b/samples/filter_sort_groups.py index e4f2c2bee..c63764134 100644 --- a/samples/filter_sort_groups.py +++ b/samples/filter_sort_groups.py @@ -2,7 +2,7 @@ # This script demonstrates how to filter and sort groups using the Tableau # Server Client. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### diff --git a/samples/filter_sort_projects.py b/samples/filter_sort_projects.py index 628b1c972..bd43cd209 100644 --- a/samples/filter_sort_projects.py +++ b/samples/filter_sort_projects.py @@ -2,7 +2,7 @@ # This script demonstrates how to use the Tableau Server Client # to filter and sort on the name of the projects present on site. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/kill_all_jobs.py b/samples/kill_all_jobs.py index 02f19d976..1a833f938 100644 --- a/samples/kill_all_jobs.py +++ b/samples/kill_all_jobs.py @@ -1,7 +1,7 @@ #### # This script demonstrates how to kill all of the running jobs # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/list.py b/samples/list.py index db0b7c790..814c1b9ca 100644 --- a/samples/list.py +++ b/samples/list.py @@ -1,7 +1,7 @@ #### # This script demonstrates how to list all of the workbooks or datasources # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/login.py b/samples/login.py index c459b9370..f0ff9ad49 100644 --- a/samples/login.py +++ b/samples/login.py @@ -1,7 +1,7 @@ #### # This script demonstrates how to log in to Tableau Server Client. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/metadata_query.py b/samples/metadata_query.py index 65df9ddb0..26f8f94fa 100644 --- a/samples/metadata_query.py +++ b/samples/metadata_query.py @@ -1,7 +1,7 @@ #### # This script demonstrates how to use the metadata API to query information on a published data source # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/move_workbook_projects.py b/samples/move_workbook_projects.py index 22465925f..884c7eab1 100644 --- a/samples/move_workbook_projects.py +++ b/samples/move_workbook_projects.py @@ -4,7 +4,7 @@ # a workbook that matches a given name and update it to be in # the desired project. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/move_workbook_sites.py b/samples/move_workbook_sites.py index c473712e4..a2d11bdfe 100644 --- a/samples/move_workbook_sites.py +++ b/samples/move_workbook_sites.py @@ -4,7 +4,7 @@ # a workbook that matches a given name, download the workbook, # and then publish it to the destination site. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/publish_datasource.py b/samples/publish_datasource.py index ad929fd99..eecbe7088 100644 --- a/samples/publish_datasource.py +++ b/samples/publish_datasource.py @@ -15,7 +15,7 @@ # more information on personal access tokens, refer to the documentations: # (https://help.tableau.com/current/server/en-us/security_personal_access_tokens.htm) # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/publish_workbook.py b/samples/publish_workbook.py index c553eda0b..3cc27c582 100644 --- a/samples/publish_workbook.py +++ b/samples/publish_workbook.py @@ -11,7 +11,7 @@ # For more information, refer to the documentations on 'Publish Workbook' # (https://onlinehelp.tableau.com/current/api/rest_api/en-us/help.htm) # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/query_permissions.py b/samples/query_permissions.py index c0d1c3afa..0c285d4c3 100644 --- a/samples/query_permissions.py +++ b/samples/query_permissions.py @@ -1,6 +1,6 @@ #### # This script demonstrates how to query for permissions using TSC -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. # # Example usage: 'python query_permissions.py -s https://10ax.online.tableau.com --site # devSite123 -u tabby@tableau.com workbook b4065286-80f0-11ea-af1b-cb7191f48e45' diff --git a/samples/refresh.py b/samples/refresh.py index 18a7f36e2..f90441224 100644 --- a/samples/refresh.py +++ b/samples/refresh.py @@ -1,7 +1,7 @@ #### # This script demonstrates how to use trigger a refresh on a datasource or workbook # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/refresh_tasks.py b/samples/refresh_tasks.py index 6ef781544..2bfc85621 100644 --- a/samples/refresh_tasks.py +++ b/samples/refresh_tasks.py @@ -2,7 +2,7 @@ # This script demonstrates how to use the Tableau Server Client # to query extract refresh tasks and run them as needed. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/samples/set_refresh_schedule.py b/samples/set_refresh_schedule.py index decdc223f..9b3dbc236 100644 --- a/samples/set_refresh_schedule.py +++ b/samples/set_refresh_schedule.py @@ -2,7 +2,7 @@ # This script demonstrates how to set the refresh schedule for # a workbook or datasource. # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### diff --git a/samples/update_connection.py b/samples/update_connection.py index 44f8ec6c0..e27b4477f 100644 --- a/samples/update_connection.py +++ b/samples/update_connection.py @@ -1,7 +1,7 @@ #### # This script demonstrates how to update a connections credentials on a server to embed the credentials # -# To run the script, you must have installed Python 3.6 or later. +# To run the script, you must have installed Python 3.7 or later. #### import argparse diff --git a/setup.py b/setup.py index ae19dcd26..24d35250c 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,7 @@ # This makes work easier for offline installs or low bandwidth machines needs_pytest = {'pytest', 'test', 'ptr'}.intersection(sys.argv) pytest_runner = ['pytest-runner'] if needs_pytest else [] -test_requirements = ['black', 'mock', 'pytest', 'requests-mock>=1.0,<2.0', 'mypy==0.910'] +test_requirements = ['black', 'mock', 'pytest', 'requests-mock>=1.0,<2.0', 'mypy>=0.920'] setup( name='tableauserverclient', @@ -25,7 +25,10 @@ author_email='github@tableau.com', url='https://github.com/tableau/server-client-python', package_data={'tableauserverclient':['py.typed']}, - packages=['tableauserverclient', 'tableauserverclient.models', 'tableauserverclient.server', + packages=['tableauserverclient', + 'tableauserverclient.helpers', + 'tableauserverclient.models', + 'tableauserverclient.server', 'tableauserverclient.server.endpoint'], license='MIT', description='A Python module for working with the Tableau Server REST API.', @@ -37,6 +40,7 @@ 'defusedxml>=0.7.1', 'requests>=2.11,<3.0', ], + python_requires='>3.7.0', tests_require=test_requirements, extras_require={ 'test': test_requirements diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 897c69fb0..2ca56b6a5 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -52,6 +52,7 @@ NotSignedInError, Pager, ) +from .helpers import * __version__ = get_versions()["version"] __VERSION__ = __version__ diff --git a/tableauserverclient/helpers/__init__.py b/tableauserverclient/helpers/__init__.py new file mode 100644 index 000000000..7daf0d490 --- /dev/null +++ b/tableauserverclient/helpers/__init__.py @@ -0,0 +1 @@ +from .strings import * diff --git a/tableauserverclient/helpers/strings.py b/tableauserverclient/helpers/strings.py new file mode 100644 index 000000000..5d58c2ca5 --- /dev/null +++ b/tableauserverclient/helpers/strings.py @@ -0,0 +1,65 @@ +import requests + +from defusedxml.ElementTree import fromstring, tostring +from functools import singledispatch +from typing import TypeVar + + +# the redact method can handle either strings or bytes, but it can't mix them. +# Generic type so we can write the actual logic once, then use singledispatch to +# create the replacement text with the correct type +T = TypeVar("T", str, bytes) + + +# TODO: ideally this would be in the logging config +def safe_to_log(server_response: requests.Response) -> str: + """Checks if the server_response content is not xml (eg binary image or zip) + and replaces it with a constant""" + ALLOWED_CONTENT_TYPES = ("application/xml", "application/xml;charset=utf-8") + if server_response.headers.get("Content-Type", None) not in ALLOWED_CONTENT_TYPES: + return "[Truncated File Contents]" + + """ Check to determine if the response is a text response (xml or otherwise) + so that we do not attempt to log bytes and other binary data. """ + if not server_response.content or not server_response.encoding: + return "" + # max length 1000 + loggable_response: str = server_response.content.decode(server_response.encoding)[:1000] + redacted_response: str = redact_xml(loggable_response) + return redacted_response + + +# usage: _redact_any_type("") +# -> b" +def _redact_any_type(xml: T, sensitive_word: T, replacement: T, encoding=None) -> T: + try: + root = fromstring(xml) + matches = root.findall(".//*[@password]") + for item in matches: + item.attrib["password"] = "********" + matches = root.findall(".//password") + for item in matches: + item.text = "********" + # tostring returns bytes unless an encoding value is passed + return tostring(root, encoding=encoding) + except Exception: + # something about the xml handling failed. Just cut off the text at the first occurrence of "password" + location = xml.find(sensitive_word) + return xml[:location] + replacement + + +@singledispatch +def redact_xml(content): + # this will only be called if it didn't get directed to the str or bytes overloads + raise TypeError("Redaction only works on xml saved as str or bytes") + + +@redact_xml.register +def _(xml: str) -> str: + out = _redact_any_type(xml, "password", "...[redacted]", encoding="unicode") + return out + + +@redact_xml.register # type: ignore[no-redef] +def _(xml: bytes) -> bytes: + return _redact_any_type(bytearray(xml), b"password", b"..[redacted]") diff --git a/tableauserverclient/server/__init__.py b/tableauserverclient/server/__init__.py index 5d1fb961b..335306db8 100644 --- a/tableauserverclient/server/__init__.py +++ b/tableauserverclient/server/__init__.py @@ -59,3 +59,4 @@ from .server import Server from .pager import Pager from .exceptions import NotSignedInError +from ..helpers import * diff --git a/tableauserverclient/server/endpoint/endpoint.py b/tableauserverclient/server/endpoint/endpoint.py index 8fdb74751..5227ffe12 100644 --- a/tableauserverclient/server/endpoint/endpoint.py +++ b/tableauserverclient/server/endpoint/endpoint.py @@ -10,6 +10,7 @@ EndpointUnavailableError, ) from ..query import QuerySet +from ... import helpers logger = logging.getLogger("tableau.endpoint") @@ -33,17 +34,6 @@ def _make_common_headers(auth_token, content_type): return headers - @staticmethod - def _safe_to_log(server_response): - """Checks if the server_response content is not xml (eg binary image or zip) - and replaces it with a constant - """ - ALLOWED_CONTENT_TYPES = ("application/xml", "application/xml;charset=utf-8") - if server_response.headers.get("Content-Type", None) not in ALLOWED_CONTENT_TYPES: - return "[Truncated File Contents]" - else: - return server_response.content - def _make_request( self, method, @@ -55,7 +45,7 @@ def _make_request( ): parameters = parameters or {} parameters.update(self.parent_srv.http_options) - if not "headers" in parameters: + if "headers" not in parameters: parameters["headers"] = {} parameters["headers"].update(Endpoint._make_common_headers(auth_token, content_type)) @@ -64,18 +54,16 @@ def _make_request( logger.debug("request {}, url: {}".format(method.__name__, url)) if content: - logger.debug("request content: {}".format(content[:1000])) + logger.debug("request content: {}".format(helpers.strings.redact_xml(content[:1000]))) server_response = method(url, **parameters) - self.parent_srv._namespace.detect(server_response.content) self._check_status(server_response) - - # This check is to determine if the response is a text response (xml or otherwise) - # so that we do not attempt to log bytes and other binary data. - if len(server_response.content) > 0 and server_response.encoding: - logger.debug( - "Server response from {0}:\n\t{1}".format(url, server_response.content.decode(server_response.encoding)) - ) + if server_response.headers.get("Content-Type") == "application/octet-stream": + logger.debug("Server response from {0} was of type application/octet-stream".format(url)) + else: + loggable_response = helpers.strings.safe_to_log(server_response) + self.parent_srv._namespace.detect(server_response.content) + logger.debug("Server response from {0}:\n\t{1}".format(url, loggable_response)) return server_response def _check_status(self, server_response): @@ -86,7 +74,7 @@ def _check_status(self, server_response): raise ServerResponseError.from_response(server_response.content, self.parent_srv.namespace) except ParseError: # This will happen if we get a non-success HTTP code that - # doesn't return an xml error object (like metadata endpoints) + # doesn't return an xml error object (like metadata endpoints or 503 pages) # we convert this to a better exception and pass through the raw # response body raise NonXMLResponseError(server_response.content) @@ -118,7 +106,7 @@ def delete_request(self, url): # We don't return anything for a delete self._make_request(self.parent_srv.session.delete, url, auth_token=self.parent_srv.auth_token) - def put_request(self, url, xml_request=None, content_type="text/xml", parameters=None): + def put_request(self, url, xml_request=None, content_type=XML_CONTENT_TYPE, parameters=None): return self._make_request( self.parent_srv.session.put, url, @@ -128,7 +116,7 @@ def put_request(self, url, xml_request=None, content_type="text/xml", parameters parameters=parameters, ) - def post_request(self, url, xml_request, content_type="text/xml", parameters=None): + def post_request(self, url, xml_request, content_type=XML_CONTENT_TYPE, parameters=None): return self._make_request( self.parent_srv.session.post, url, @@ -138,7 +126,7 @@ def post_request(self, url, xml_request, content_type="text/xml", parameters=Non parameters=parameters, ) - def patch_request(self, url, xml_request, content_type="text/xml", parameters=None): + def patch_request(self, url, xml_request, content_type=XML_CONTENT_TYPE, parameters=None): return self._make_request( self.parent_srv.session.patch, url, diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index 901d0e62a..4d7a4a2b5 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -16,6 +16,7 @@ from .endpoint import QuerysetEndpoint, api, parameter_added_in from .exceptions import InternalServerError, MissingRequiredFieldError +from ...helpers import redact_xml from .permissions_endpoint import _PermissionsEndpoint from .resource_tagger import _ResourceTagger from .. import RequestFactory, WorkbookItem, ConnectionItem, ViewItem, PaginationItem @@ -441,7 +442,7 @@ def publish( connections=connections, hidden_views=hidden_views, ) - logger.debug("Request xml: {0} ".format(xml_request[:1000])) + logger.debug("Request xml: {0} ".format(redact_xml(xml_request[:1000]))) # Send the publishing request to server try: diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index 7313124af..fa6bdc9c6 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -39,6 +39,8 @@ def wrapper(self, *args, **kwargs): def _add_connections_element(connections_element, connection): connection_element = ET.SubElement(connections_element, "connection") + if not connection.server_address: + raise ValueError("Connection must have a server address") connection_element.attrib["serverAddress"] = connection.server_address if connection.server_port: connection_element.attrib["serverPort"] = connection.server_port @@ -55,6 +57,8 @@ def _add_hiddenview_element(views_element, view_name): def _add_credentials_element(parent_element, connection_credentials): credentials_element = ET.SubElement(parent_element, "connectionCredentials") + if not connection_credentials.password or not connection_credentials.name: + raise ValueError("Connection Credentials must have a name and password") credentials_element.attrib["name"] = connection_credentials.name credentials_element.attrib["password"] = connection_credentials.password credentials_element.attrib["embed"] = "true" if connection_credentials.embed else "false" @@ -232,7 +236,7 @@ def add_req(self, dqw_item): return ET.tostring(xml_request) - def update_req(self, database_item): + def update_req(self, dqw_item): xml_request = ET.Element("tsRequest") dqw_element = ET.SubElement(xml_request, "dataQualityWarning") diff --git a/test/request_factory/test_workbook_requests.py b/test/request_factory/test_workbook_requests.py index 797913614..332b6defa 100644 --- a/test/request_factory/test_workbook_requests.py +++ b/test/request_factory/test_workbook_requests.py @@ -1,6 +1,9 @@ import unittest import tableauserverclient as TSC import tableauserverclient.server.request_factory as TSC_RF +from tableauserverclient.helpers.strings import redact_xml +import pytest +import sys class WorkbookRequestTests(unittest.TestCase): @@ -12,3 +15,41 @@ def test_embedded_extract_req(self): def test_generate_xml(self): workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") TSC_RF.RequestFactory.Workbook._generate_xml(workbook_item) + + def test_generate_xml_invalid_connection(self): + workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") + conn = TSC.ConnectionItem() + with self.assertRaises(ValueError): + request = TSC_RF.RequestFactory.Workbook._generate_xml(workbook_item, connections=[conn]) + + def test_generate_xml_invalid_connection_credentials(self): + workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") + conn = TSC.ConnectionItem() + conn.server_address = "address" + creds = TSC.ConnectionCredentials("username", "password") + creds.name = None + conn.connection_credentials = creds + with self.assertRaises(ValueError): + request = TSC_RF.RequestFactory.Workbook._generate_xml(workbook_item, connections=[conn]) + + def test_generate_xml_valid_connection_credentials(self): + workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") + conn = TSC.ConnectionItem() + conn.server_address = "address" + creds = TSC.ConnectionCredentials("username", "DELETEME") + conn.connection_credentials = creds + request = TSC_RF.RequestFactory.Workbook._generate_xml(workbook_item, connections=[conn]) + assert request.find(b"DELETEME") > 0 + + def test_redact_passwords_in_xml(self): + if sys.version_info < (3, 7): + pytest.skip("Redaction is only implemented for 3.7+.") + workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") + conn = TSC.ConnectionItem() + conn.server_address = "address" + creds = TSC.ConnectionCredentials("username", "DELETEME") + conn.connection_credentials = creds + request = TSC_RF.RequestFactory.Workbook._generate_xml(workbook_item, connections=[conn]) + redacted = redact_xml(request) + assert request.find(b"DELETEME") > 0, request + assert redacted.find(b"DELETEME") == -1, redacted diff --git a/test/test_dqw.py b/test/test_dqw.py new file mode 100644 index 000000000..6d1219f66 --- /dev/null +++ b/test/test_dqw.py @@ -0,0 +1,11 @@ +import unittest +import tableauserverclient as TSC + + +class DQWTests(unittest.TestCase): + def test_existence(self): + dqw: TSC.DQWItem = TSC.DQWItem() + dqw.message = "message" + dqw.warning_type = TSC.DQWItem.WarningType.STALE + dqw.active = True + dqw.severe = True diff --git a/test/test_regression_tests.py b/test/test_regression_tests.py index 58d6329db..15f33b139 100644 --- a/test/test_regression_tests.py +++ b/test/test_regression_tests.py @@ -1,4 +1,6 @@ import unittest +import pytest +import sys try: from unittest import mock @@ -6,7 +8,8 @@ import mock # type: ignore[no-redef] import tableauserverclient.server.request_factory as factory -from tableauserverclient.server.endpoint import Endpoint +from tableauserverclient.models.workbook_item import WorkbookItem +from tableauserverclient.helpers.strings import redact_xml, safe_to_log from tableauserverclient.filesys_helpers import to_filename, make_download_path @@ -25,8 +28,7 @@ class FakeResponse(object): status_code = 200 server_response = FakeResponse() - - self.assertEqual(Endpoint._safe_to_log(server_response), "[Truncated File Contents]") + self.assertEqual(safe_to_log(server_response), "[Truncated File Contents]") class FileSysHelpers(unittest.TestCase): @@ -60,3 +62,41 @@ def test_make_download_path(self): with mock.patch("os.path.isdir") as mocked_isdir: mocked_isdir.return_value = True self.assertEqual("/root/folder/file.ext", make_download_path(*has_file_path_folder)) + + +class LoggingTest(unittest.TestCase): + def test_redact_password_string(self): + redacted = redact_xml( + "this is password: my_super_secret_passphrase_which_nobody_should_ever_see password: value" + ) + assert redacted.find("value") == -1 + assert redacted.find("secret") == -1 + assert redacted.find("ever_see") == -1 + assert redacted.find("my_super_secret_passphrase_which_nobody_should_ever_see") == -1 + + def test_redact_password_bytes(self): + redacted = redact_xml( + b"" + ) + assert redacted.find(b"value") == -1 + assert redacted.find(b"secret") == -1 + + def test_redact_password_with_special_char(self): + redacted = redact_xml( + " " + ) + assert redacted.find("my_s per_secre>_passphrase_which_nobody_should_ever_see with password: value") == -1 + + def test_redact_password_not_xml(self): + redacted = redact_xml( + " " + ) + assert redacted.find("my_s per_secre>_passphrase_which_nobody_should_ever_see") == -1 + + def test_redact_password_really_not_xml(self): + redacted = redact_xml( + "value='this is a nondescript text line which is public' password='my_s per_secre>_passphrase_which_nobody_should_ever_see with password: value and then a cookie " + ) + assert redacted.find("my_s per_secre>_passphrase_which_nobody_should_ever_see") == -1 + assert redacted.find("passphrase") == -1, redacted + assert redacted.find("cookie") == -1, redacted