From 10613091d3da1b178ed6dab8b006d1cb62756789 Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Thu, 20 Nov 2025 22:13:28 -0600 Subject: [PATCH 1/5] fix: black ci errors --- test/test_datasource.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_datasource.py b/test/test_datasource.py index 7f4cca75..56eb11ab 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -895,7 +895,8 @@ def test_publish_description(server: TSC.Server) -> None: ds_elem = body.find(".//datasource") assert ds_elem is not None assert ds_elem.attrib["description"] == "Sample description" - + + def test_get_datasource_no_owner(server: TSC.Server) -> None: with requests_mock.mock() as m: m.get(server.datasources.baseurl, text=GET_NO_OWNER.read_text()) From bde2790e96e4a5448f407b8e37b011151b6e316c Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Wed, 12 Nov 2025 21:57:36 -0600 Subject: [PATCH 2/5] chore: pytestify requests --- test/test_requests.py | 110 ++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 53 deletions(-) diff --git a/test/test_requests.py b/test/test_requests.py index 5c0d090b..3a22b516 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -1,6 +1,6 @@ -import re -import unittest +from urllib.parse import parse_qs +import pytest import requests import requests_mock @@ -8,54 +8,58 @@ from tableauserverclient.server.endpoint.exceptions import InternalServerError, NonXMLResponseError -class RequestTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - - # Fake sign in - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.workbooks.baseurl - - def test_make_get_request(self): - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks" - opts = TSC.RequestOptions(pagesize=13, pagenumber=15) - resp = self.server.workbooks.get_request(url, request_object=opts) - - self.assertTrue(re.search("pagesize=13", resp.request.query)) - self.assertTrue(re.search("pagenumber=15", resp.request.query)) - - def test_make_post_request(self): - with requests_mock.mock() as m: - m.post(requests_mock.ANY) - url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks" - resp = self.server.workbooks._make_request( - requests.post, - url, - content=b"1337", - auth_token="j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM", - content_type="multipart/mixed", - ) - self.assertEqual(resp.request.headers["x-tableau-auth"], "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM") - self.assertEqual(resp.request.headers["content-type"], "multipart/mixed") - self.assertTrue(re.search("Tableau Server Client", resp.request.headers["user-agent"])) - self.assertEqual(resp.request.body, b"1337") - - # Test that 500 server errors are handled properly - def test_internal_server_error(self): - self.server.version = "3.2" - server_response = "500: Internal Server Error" - with requests_mock.mock() as m: - m.register_uri("GET", self.server.server_info.baseurl, status_code=500, text=server_response) - self.assertRaisesRegex(InternalServerError, server_response, self.server.server_info.get) - - # Test that non-xml server errors are handled properly - def test_non_xml_error(self): - self.server.version = "3.2" - server_response = "this is not xml" - with requests_mock.mock() as m: - m.register_uri("GET", self.server.server_info.baseurl, status_code=499, text=server_response) - self.assertRaisesRegex(NonXMLResponseError, server_response, self.server.server_info.get) +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) + + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + + return server + +def test_make_get_request(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks" + opts = TSC.RequestOptions(pagesize=13, pagenumber=15) + resp = server.workbooks.get_request(url, request_object=opts) + + query = parse_qs(resp.request.query) + assert query.get("pagesize") == ["13"] + assert query.get("pagenumber") == ["15"] + +def test_make_post_request(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.post(requests_mock.ANY) + url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks" + resp = server.workbooks._make_request( + requests.post, + url, + content=b"1337", + auth_token="j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM", + content_type="multipart/mixed", + ) + assert resp.request.headers["x-tableau-auth"] == "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + assert resp.request.headers["content-type"] == "multipart/mixed" + assert "Tableau Server Client" in resp.request.headers["user-agent"] + assert resp.request.body == b"1337" + +# Test that 500 server errors are handled properly +def test_internal_server_error(server: TSC.Server) -> None: + server.version = "3.2" + server_response = "500: Internal Server Error" + with requests_mock.mock() as m: + m.register_uri("GET", server.server_info.baseurl, status_code=500, text=server_response) + with pytest.raises(InternalServerError, match=server_response): + server.server_info.get() + +# Test that non-xml server errors are handled properly +def test_non_xml_error(server: TSC.Server) -> None: + server.version = "3.2" + server_response = "this is not xml" + with requests_mock.mock() as m: + m.register_uri("GET", server.server_info.baseurl, status_code=499, text=server_response) + with pytest.raises(NonXMLResponseError, match=server_response): + server.server_info.get() From 4a939289b64b295af202fccceb1172224ee86b3d Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Wed, 12 Nov 2025 22:00:54 -0600 Subject: [PATCH 3/5] style: black --- test/test_requests.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_requests.py b/test/test_requests.py index 3a22b516..5ee68b02 100644 --- a/test/test_requests.py +++ b/test/test_requests.py @@ -19,6 +19,7 @@ def server(): return server + def test_make_get_request(server: TSC.Server) -> None: with requests_mock.mock() as m: m.get(requests_mock.ANY) @@ -30,6 +31,7 @@ def test_make_get_request(server: TSC.Server) -> None: assert query.get("pagesize") == ["13"] assert query.get("pagenumber") == ["15"] + def test_make_post_request(server: TSC.Server) -> None: with requests_mock.mock() as m: m.post(requests_mock.ANY) @@ -46,6 +48,7 @@ def test_make_post_request(server: TSC.Server) -> None: assert "Tableau Server Client" in resp.request.headers["user-agent"] assert resp.request.body == b"1337" + # Test that 500 server errors are handled properly def test_internal_server_error(server: TSC.Server) -> None: server.version = "3.2" @@ -55,6 +58,7 @@ def test_internal_server_error(server: TSC.Server) -> None: with pytest.raises(InternalServerError, match=server_response): server.server_info.get() + # Test that non-xml server errors are handled properly def test_non_xml_error(server: TSC.Server) -> None: server.version = "3.2" From f1af25c9937bbedd39abe046b281fb40ee92c55f Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Mon, 22 Dec 2025 08:12:51 -0600 Subject: [PATCH 4/5] chore: pytestify request_factory object requests --- .../test_datasource_requests.py | 18 ++-- .../request_factory/test_workbook_requests.py | 95 ++++++++++--------- 2 files changed, 57 insertions(+), 56 deletions(-) diff --git a/test/request_factory/test_datasource_requests.py b/test/request_factory/test_datasource_requests.py index 75bb535d..66b7a373 100644 --- a/test/request_factory/test_datasource_requests.py +++ b/test/request_factory/test_datasource_requests.py @@ -1,15 +1,13 @@ -import unittest import tableauserverclient as TSC import tableauserverclient.server.request_factory as TSC_RF from tableauserverclient import DatasourceItem -class DatasourceRequestTests(unittest.TestCase): - def test_generate_xml(self): - datasource_item: TSC.DatasourceItem = TSC.DatasourceItem("name") - datasource_item.name = "a ds" - datasource_item.description = "described" - datasource_item.use_remote_query_agent = False - datasource_item.ask_data_enablement = DatasourceItem.AskDataEnablement.Enabled - datasource_item.project_id = "testval" - TSC_RF.RequestFactory.Datasource._generate_xml(datasource_item) +def test_generate_xml(): + datasource_item: TSC.DatasourceItem = TSC.DatasourceItem("name") + datasource_item.name = "a ds" + datasource_item.description = "described" + datasource_item.use_remote_query_agent = False + datasource_item.ask_data_enablement = DatasourceItem.AskDataEnablement.Enabled + datasource_item.project_id = "testval" + TSC_RF.RequestFactory.Datasource._generate_xml(datasource_item) diff --git a/test/request_factory/test_workbook_requests.py b/test/request_factory/test_workbook_requests.py index 332b6def..b114e04a 100644 --- a/test/request_factory/test_workbook_requests.py +++ b/test/request_factory/test_workbook_requests.py @@ -1,4 +1,3 @@ -import unittest import tableauserverclient as TSC import tableauserverclient.server.request_factory as TSC_RF from tableauserverclient.helpers.strings import redact_xml @@ -6,50 +5,54 @@ import sys -class WorkbookRequestTests(unittest.TestCase): - def test_embedded_extract_req(self): - include_all = True - embedded_datasources = None - xml_result = TSC_RF.RequestFactory.Workbook.embedded_extract_req(include_all, embedded_datasources) - - 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 +def test_embedded_extract_req() -> None: + include_all = True + embedded_datasources = None + xml_result = TSC_RF.RequestFactory.Workbook.embedded_extract_req(include_all, embedded_datasources) + + +def test_generate_xml() -> None: + workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") + TSC_RF.RequestFactory.Workbook._generate_xml(workbook_item) + + +def test_generate_xml_invalid_connection() -> None: + workbook_item: TSC.WorkbookItem = TSC.WorkbookItem("name", "project_id") + conn = TSC.ConnectionItem() + with pytest.raises(ValueError): 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 + + +def test_generate_xml_invalid_connection_credentials() -> None: + 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 pytest.raises(ValueError): 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 + + +def test_generate_xml_valid_connection_credentials() -> None: + 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() -> None: + 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 From f923a799464b7e61749eb1ac7c5ee2fdd9343882 Mon Sep 17 00:00:00 2001 From: Jordan Woods <13803242+jorwoods@users.noreply.github.com> Date: Mon, 22 Dec 2025 09:17:14 -0600 Subject: [PATCH 5/5] chore: pytestify ssl_config --- test/test_ssl_config.py | 139 +++++++++++++++++++--------------------- 1 file changed, 65 insertions(+), 74 deletions(-) diff --git a/test/test_ssl_config.py b/test/test_ssl_config.py index 036a326c..28ef3fc5 100644 --- a/test/test_ssl_config.py +++ b/test/test_ssl_config.py @@ -1,77 +1,68 @@ -import unittest -import ssl -from unittest.mock import patch, MagicMock -from tableauserverclient import Server -from tableauserverclient.server.endpoint import Endpoint import logging +from unittest.mock import MagicMock +import pytest -class TestSSLConfig(unittest.TestCase): - @patch("requests.session") - @patch("tableauserverclient.server.endpoint.Endpoint.set_parameters") - def setUp(self, mock_set_parameters, mock_session): - """Set up test fixtures with mocked session and request validation""" - # Mock the session - self.mock_session = MagicMock() - mock_session.return_value = self.mock_session - - # Mock request preparation - self.mock_request = MagicMock() - self.mock_session.prepare_request.return_value = self.mock_request - - # Create server instance with mocked components - self.server = Server("http://test") - - def test_default_ssl_config(self): - """Test that by default, no custom SSL context is used""" - self.assertIsNone(self.server._ssl_context) - self.assertNotIn("verify", self.server.http_options) - - @patch("ssl.create_default_context") - def test_weak_dh_config(self, mock_create_context): - """Test that weak DH keys can be allowed when configured""" - # Setup mock SSL context - mock_context = MagicMock() - mock_create_context.return_value = mock_context - - # Configure SSL with weak DH - self.server.configure_ssl(allow_weak_dh=True) - - # Verify SSL context was created and configured correctly - mock_create_context.assert_called_once() - mock_context.set_dh_parameters.assert_called_once_with(min_key_bits=512) - - # Verify context was added to http options - self.assertEqual(self.server.http_options["verify"], mock_context) - - @patch("ssl.create_default_context") - def test_disable_weak_dh_config(self, mock_create_context): - """Test that SSL config can be reset to defaults""" - # Setup mock SSL context - mock_context = MagicMock() - mock_create_context.return_value = mock_context - - # First enable weak DH - self.server.configure_ssl(allow_weak_dh=True) - self.assertIsNotNone(self.server._ssl_context) - self.assertIn("verify", self.server.http_options) - - # Then disable it - self.server.configure_ssl(allow_weak_dh=False) - self.assertIsNone(self.server._ssl_context) - self.assertNotIn("verify", self.server.http_options) - - @patch("ssl.create_default_context") - def test_warning_on_weak_dh(self, mock_create_context): - """Test that a warning is logged when enabling weak DH keys""" - logging.getLogger().setLevel(logging.WARNING) - with self.assertLogs(level="WARNING") as log: - self.server.configure_ssl(allow_weak_dh=True) - self.assertTrue( - any("WARNING: Allowing weak Diffie-Hellman keys" in record for record in log.output), - "Expected warning about weak DH keys was not logged", - ) - - -if __name__ == "__main__": - unittest.main() +import tableauserverclient as TSC + + +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) + + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + + return server + + +def test_default_ssl_config(server): + """Test that by default, no custom SSL context is used""" + assert server._ssl_context is None + assert "verify" not in server.http_options + + +def test_weak_dh_config(server, monkeypatch): + """Test that weak DH keys can be allowed when configured""" + mock_context = MagicMock() + mock_create_context = MagicMock(return_value=mock_context) + monkeypatch.setattr("ssl.create_default_context", mock_create_context) + + server.configure_ssl(allow_weak_dh=True) + + mock_create_context.assert_called_once() + mock_context.set_dh_parameters.assert_called_once_with(min_key_bits=512) + assert server.http_options["verify"] == mock_context + + +def test_disable_weak_dh_config(server, monkeypatch): + """Test that SSL config can be reset to defaults""" + mock_context = MagicMock() + mock_create_context = MagicMock(return_value=mock_context) + monkeypatch.setattr("ssl.create_default_context", mock_create_context) + + # First enable weak DH + server.configure_ssl(allow_weak_dh=True) + assert server._ssl_context is not None + assert "verify" in server.http_options + + # Then disable it + server.configure_ssl(allow_weak_dh=False) + assert server._ssl_context is None + assert "verify" not in server.http_options + + +def test_warning_on_weak_dh(server, monkeypatch, caplog): + """Test that a warning is logged when enabling weak DH keys""" + mock_context = MagicMock() + mock_create_context = MagicMock(return_value=mock_context) + monkeypatch.setattr("ssl.create_default_context", mock_create_context) + + with caplog.at_level(logging.WARNING): + server.configure_ssl(allow_weak_dh=True) + + assert any( + "Allowing weak Diffie-Hellman keys" in record.getMessage() for record in caplog.records + ), "Expected warning about weak DH keys was not logged"