diff --git a/.gitattributes b/.gitattributes index ade44ab7c..040321c04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ tableauserverclient/_version.py export-subst +tableauserverclient/bin/_version.py export-subst diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index cae0f409c..d6d36f7ba 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -1,9 +1,12 @@ name: Publish to PyPi -# This will publish a package to TestPyPi (and real Pypi if run on master) with a version -# number generated by versioneer from the most recent tag looking like v____ -# TODO: maybe move this into the package job so all release-based actions are together +# This will build a package with a version set by versioneer from the most recent tag matching v____ +# It will publish to TestPyPi, and to real Pypi *if* run on master where head has a release tag +# For a live run, this should only need to be triggered by a newly published repo release. +# This can also be run manually for testing on: + release: + types: [published] workflow_dispatch: push: tags: @@ -19,11 +22,11 @@ jobs: fetch-depth: 0 - uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: 3.13 - name: Build dist files run: | python -m pip install --upgrade pip - pip install -e .[test] build + python -m pip install -e .[test] build python -m build git describe --tag --dirty --always diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 2e197cf20..9ac7ebb6a 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -13,7 +13,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14', '3.14t'] runs-on: ${{ matrix.os }} @@ -38,6 +38,7 @@ jobs: uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} + allow-prereleases: ${{ matrix.allow-prereleases || false }} - name: Install dependencies run: | @@ -47,7 +48,7 @@ jobs: - name: Test with pytest if: always() run: | - pytest test + pytest test -n auto - name: Test build if: always() diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 000000000..10fb2b98c --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,2 @@ +#ECCN:Open Source +#GUSINFO:Open Source,Open Source Workflow diff --git a/MANIFEST.in b/MANIFEST.in index 9b7512fb9..7acbed103 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,8 +4,6 @@ include CONTRIBUTORS.md include LICENSE include LICENSE.versioneer include README.md -include tableauserverclient/_version.py -include versioneer.py recursive-include docs *.md recursive-include samples *.py recursive-include samples *.txt @@ -18,5 +16,4 @@ recursive-include test *.png recursive-include test *.py recursive-include test *.xml recursive-include test *.tde -global-include *.pyi global-include *.typed diff --git a/publish.sh b/publish.sh deleted file mode 100755 index 46d54a1ee..000000000 --- a/publish.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -# tag the release version and confirm a clean version number -git tag vxxxx -git describe --tag --dirty --always - -set -e - -rm -rf dist -python setup.py sdist bdist_wheel -twine upload dist/* diff --git a/pyproject.toml b/pyproject.toml index 68f7589ca..22760e803 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools>=75.0", "versioneer[toml]==0.29", "wheel"] +requires = ["setuptools>=77.0", "versioneer[toml]==0.29", "wheel"] build-backend = "setuptools.build_meta" [project] @@ -8,14 +8,14 @@ name="tableauserverclient" dynamic = ["version"] description='A Python module for working with the Tableau Server REST API.' authors = [{name="Tableau", email="github@tableau.com"}] -license = {file = "LICENSE"} +license-files = ["LICENSE"] readme = "README.md" dependencies = [ 'defusedxml>=0.7.1', # latest as at 7/31/23 'packaging>=23.1', # latest as at 7/31/23 'requests>=2.32', # latest as at 7/31/23 - 'urllib3>=2.2.2,<3', + 'urllib3>=2.6.0,<3', 'typing_extensions>=4.0', ] requires-python = ">=3.9" @@ -32,8 +32,14 @@ classifiers = [ repository = "https://github.com/tableau/server-client-python" [project.optional-dependencies] -test = ["black==24.8", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", - "requests-mock>=1.0,<2.0"] +test = ["black==24.10", "build", "mypy==1.4", "pytest>=7.0", "pytest-cov", "pytest-subtests", + "pytest-xdist", "requests-mock>=1.0,<2.0", "types-requests>=2.32.4.20250913"] + +[tool.setuptools.packages.find] +where = ["tableauserverclient", "tableauserverclient.helpers", "tableauserverclient.models", "tableauserverclient.server", "tableauserverclient.server.endpoint"] + +[tool.setuptools.dynamic] +version = {attr = "versioneer.get_version"} [tool.black] line-length = 120 @@ -61,5 +67,5 @@ addopts = "--junitxml=./test.junit.xml" VCS = "git" style = "pep440-pre" versionfile_source = "tableauserverclient/bin/_version.py" -versionfile_build = "tableauserverclient/bin/_version.py" +versionfile_build = "_version.py" tag_prefix = "v" diff --git a/samples/create_user.py b/samples/create_user.py new file mode 100644 index 000000000..8b20f069d --- /dev/null +++ b/samples/create_user.py @@ -0,0 +1,73 @@ +#### +# This script demonstrates how to create a user using the Tableau +# Server Client. +# +# To run the script, you must have installed Python 3.7 or later. +#### + + +import argparse +import logging +import os +import sys +from typing import Sequence + +import tableauserverclient as TSC + + +def parse_args(args: Sequence[str] | None) -> argparse.Namespace: + """ + Parse command line parameters + """ + if args is None: + args = sys.argv[1:] + parser = argparse.ArgumentParser(description="Creates a sample user group.") + # Common options; please keep those in sync across all samples + parser.add_argument("--server", "-s", help="server address") + parser.add_argument("--site", "-S", help="site name") + parser.add_argument("--token-name", "-p", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") + parser.add_argument( + "--logging-level", + "-l", + choices=["debug", "info", "error"], + default="error", + help="desired logging level (set to error by default)", + ) + # Options specific to this sample + # This sample has no additional options, yet. If you add some, please add them here + parser.add_argument("--role", "-r", help="Site Role for the new user", default="Unlicensed") + parser.add_argument( + "--user", + "-u", + help="Username for the new user. If using active directory, it should be in the format of SAMAccountName@FullyQualifiedDomainName", + ) + parser.add_argument( + "--email", "-e", help="Email address of the new user. If using active directory, this field is optional." + ) + + return parser.parse_args(args) + + +def main(): + args = parse_args(None) + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=True, http_options={"verify": False}) + with server.auth.sign_in(tableau_auth): + # this code shows 2 different error codes for common mistakes + # 400013: Invalid site role + # 409000: user already exists on site + + user = TSC.UserItem(args.user, args.role) + if args.email: + user.email = args.email + user = server.users.add(user) + + +if __name__ == "__main__": + main() diff --git a/samples/metadata_paginated_query.py b/samples/metadata_paginated_query.py new file mode 100644 index 000000000..c812c2e95 --- /dev/null +++ b/samples/metadata_paginated_query.py @@ -0,0 +1,87 @@ +#### +# 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.7 or later. +#### + +import argparse +import logging +from pprint import pprint + +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description="Use the metadata API to get information on a published data source.") + # Common options; please keep those in sync across all samples + parser.add_argument("--server", "-s", help="server address") + parser.add_argument("--site", "-S", help="site name") + parser.add_argument("--token-name", "-n", help="name of the personal access token used to sign into the server") + parser.add_argument("--token-value", "-v", help="value of the personal access token used to sign into the server") + parser.add_argument( + "--logging-level", + "-l", + choices=["debug", "info", "error"], + default="error", + help="desired logging level (set to error by default)", + ) + # Options specific to this sample + parser.add_argument( + "datasource_name", + nargs="?", + help="The name of the published datasource. If not present, we query all data sources.", + ) + + args = parser.parse_args() + + # Set logging level based on user input, or error by default + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + # Sign in to server + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=True) + with server.auth.sign_in(tableau_auth): + # Execute the query + result = server.metadata.query( + """ + # Query must declare that it accepts first and afterToken variables + query paged($first:Int, $afterToken:String) { + workbooksConnection(first: $first, after:$afterToken) { + nodes { + luid + name + projectName + description + } + totalCount + pageInfo { + endCursor + hasNextPage + } + } + } + """, + # "first" adjusts the page size. Here we set it to 5 to demonstrate pagination. + # Set it to a higher number to reduce the number of pages. Including + # first and afterToken is optional, and if not included, TSC will + # use its default page size of 100. + variables={"first": 5, "afterToken": None}, + ) + + # Multiple pages are captured in result["pages"]. Each page contains + # the result of one execution of the query above. + for page in result["pages"]: + # Display warnings/errors (if any) + if page.get("errors"): + print("### Errors/Warnings:") + pprint(result["errors"]) + + # Print the results + if result.get("data"): + print("### Results:") + pprint(result["data"]["workbooksConnection"]["nodes"]) + + +if __name__ == "__main__": + main() diff --git a/samples/update_connection_auth.py b/samples/update_connection_auth.py new file mode 100644 index 000000000..19134e60c --- /dev/null +++ b/samples/update_connection_auth.py @@ -0,0 +1,62 @@ +import argparse +import logging +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser( + description="Update a single connection on a datasource or workbook to embed credentials" + ) + + # Common options + parser.add_argument("--server", "-s", help="Server address", required=True) + parser.add_argument("--site", "-S", help="Site name", required=True) + parser.add_argument("--token-name", "-p", help="Personal access token name", required=True) + parser.add_argument("--token-value", "-v", help="Personal access token value", required=True) + parser.add_argument( + "--logging-level", + "-l", + choices=["debug", "info", "error"], + default="error", + help="Logging level (default: error)", + ) + + # Resource and connection details + parser.add_argument("resource_type", choices=["workbook", "datasource"]) + parser.add_argument("resource_id", help="Workbook or datasource ID") + parser.add_argument("connection_id", help="Connection ID to update") + parser.add_argument("datasource_username", help="Username to set for the connection") + parser.add_argument("datasource_password", help="Password to set for the connection") + parser.add_argument("authentication_type", help="Authentication type") + + args = parser.parse_args() + + # Logging setup + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=True) + + with server.auth.sign_in(tableau_auth): + endpoint = {"workbook": server.workbooks, "datasource": server.datasources}.get(args.resource_type) + + update_function = endpoint.update_connection + resource = endpoint.get_by_id(args.resource_id) + endpoint.populate_connections(resource) + + connections = [conn for conn in resource.connections if conn.id == args.connection_id] + assert len(connections) == 1, f"Connection ID '{args.connection_id}' not found." + + connection = connections[0] + connection.username = args.datasource_username + connection.password = args.datasource_password + connection.auth_type = args.authentication_type + connection.embed_password = True + + updated_connection = update_function(resource, connection) + print(f"Updated connection: {updated_connection.__dict__}") + + +if __name__ == "__main__": + main() diff --git a/samples/update_connections_auth.py b/samples/update_connections_auth.py new file mode 100644 index 000000000..f0c8dd852 --- /dev/null +++ b/samples/update_connections_auth.py @@ -0,0 +1,64 @@ +import argparse +import logging +import tableauserverclient as TSC + + +def main(): + parser = argparse.ArgumentParser(description="Bulk update all workbook or datasource connections") + + # Common options + parser.add_argument("--server", "-s", help="Server address", required=True) + parser.add_argument("--site", "-S", help="Site name", required=True) + parser.add_argument("--token-name", "-p", help="Personal access token name", required=True) + parser.add_argument("--token-value", "-v", help="Personal access token value", required=True) + parser.add_argument( + "--logging-level", + "-l", + choices=["debug", "info", "error"], + default="error", + help="Logging level (default: error)", + ) + + # Resource-specific + parser.add_argument("resource_type", choices=["workbook", "datasource"]) + parser.add_argument("resource_id") + parser.add_argument("datasource_username") + parser.add_argument("authentication_type") + parser.add_argument("--datasource_password", default=None, help="Datasource password (optional)") + parser.add_argument( + "--embed_password", default="true", choices=["true", "false"], help="Embed password (default: true)" + ) + + args = parser.parse_args() + + # Set logging level + logging_level = getattr(logging, args.logging_level.upper()) + logging.basicConfig(level=logging_level) + + tableau_auth = TSC.PersonalAccessTokenAuth(args.token_name, args.token_value, site_id=args.site) + server = TSC.Server(args.server, use_server_version=True) + + with server.auth.sign_in(tableau_auth): + endpoint = {"workbook": server.workbooks, "datasource": server.datasources}.get(args.resource_type) + + resource = endpoint.get_by_id(args.resource_id) + endpoint.populate_connections(resource) + + connection_luids = [conn.id for conn in resource.connections] + embed_password = args.embed_password.lower() == "true" + + # Call unified update_connections method + connection_items = endpoint.update_connections( + resource, + connection_luids=connection_luids, + authentication_type=args.authentication_type, + username=args.datasource_username, + password=args.datasource_password, + embed_password=embed_password, + ) + + print(f"Updated connections on {args.resource_type} {args.resource_id}: {connection_items}") + + +if __name__ == "__main__": + main() diff --git a/samples/update_datasource_data.py b/samples/update_datasource_data.py index f6bc92022..1b0160b87 100644 --- a/samples/update_datasource_data.py +++ b/samples/update_datasource_data.py @@ -76,7 +76,7 @@ def main(): print("Waiting for job...") # `wait_for_job` will throw if the job isn't executed successfully job = server.jobs.wait_for_job(job) - print("Job finished succesfully") + print("Job finished successfully") if __name__ == "__main__": diff --git a/setup.py b/setup.py index bdce51f2e..b52ba267e 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,7 @@ from setuptools import setup setup( - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), - # not yet sure how to move this to pyproject.toml - packages=[ - "tableauserverclient", - "tableauserverclient.helpers", - "tableauserverclient.models", - "tableauserverclient.server", - "tableauserverclient.server.endpoint", - ], + # This line is required to set the version number when building the wheel + # not yet sure how to move this to pyproject.toml - it may require work in versioneer + cmdclass=versioneer.get_cmdclass() ) diff --git a/tableauserverclient/__init__.py b/tableauserverclient/__init__.py index 21e2c4760..7241f23ca 100644 --- a/tableauserverclient/__init__.py +++ b/tableauserverclient/__init__.py @@ -2,6 +2,7 @@ from tableauserverclient.namespace import NEW_NAMESPACE as DEFAULT_NAMESPACE from tableauserverclient.models import ( BackgroundJobItem, + CollectionItem, ColumnItem, ConnectionCredentials, ConnectionItem, @@ -12,6 +13,8 @@ DatabaseItem, DataFreshnessPolicyItem, DatasourceItem, + ExtensionsServer, + ExtensionsSiteSettings, FavoriteItem, FlowItem, FlowRunItem, @@ -35,8 +38,10 @@ ProjectItem, Resource, RevisionItem, + SafeExtension, ScheduleItem, SiteAuthConfiguration, + SiteOIDCConfiguration, SiteItem, ServerInfoItem, SubscriptionItem, @@ -72,7 +77,7 @@ __all__ = [ "BackgroundJobItem", - "BackgroundJobItem", + "CollectionItem", "ColumnItem", "ConnectionCredentials", "ConnectionItem", @@ -86,6 +91,8 @@ "DEFAULT_NAMESPACE", "DQWItem", "ExcelRequestOptions", + "ExtensionsServer", + "ExtensionsSiteSettings", "FailedSignInError", "FavoriteItem", "FileuploadItem", @@ -119,12 +126,14 @@ "RequestOptions", "Resource", "RevisionItem", + "SafeExtension", "ScheduleItem", "Server", "ServerInfoItem", "ServerResponseError", "SiteItem", "SiteAuthConfiguration", + "SiteOIDCConfiguration", "Sort", "SubscriptionItem", "TableauAuth", @@ -139,7 +148,3 @@ "WeeklyInterval", "WorkbookItem", ] - -from .bin import _version - -__version__ = _version.get_versions()["version"] diff --git a/tableauserverclient/bin/__init__.py b/tableauserverclient/bin/__init__.py new file mode 100644 index 000000000..e4605a43b --- /dev/null +++ b/tableauserverclient/bin/__init__.py @@ -0,0 +1,3 @@ +# generated during initial setup of versioneer +from . import _version +__version__ = _version.get_versions()['version'] diff --git a/tableauserverclient/bin/_version.py b/tableauserverclient/bin/_version.py index f23819e86..680304c7d 100644 --- a/tableauserverclient/bin/_version.py +++ b/tableauserverclient/bin/_version.py @@ -46,14 +46,13 @@ class VersioneerConfig: def get_config() -> VersioneerConfig: """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py + # these strings are filled in from pyproject.toml at file generation time cfg = VersioneerConfig() cfg.VCS = "git" cfg.style = "pep440-pre" cfg.tag_prefix = "v" cfg.parentdir_prefix = "None" - cfg.versionfile_source = "tableauserverclient/_version.py" + cfg.versionfile_source = "tableauserverclient/bin/_version.py" cfg.verbose = False return cfg diff --git a/tableauserverclient/models/__init__.py b/tableauserverclient/models/__init__.py index 30cd88104..aa28e0dbf 100644 --- a/tableauserverclient/models/__init__.py +++ b/tableauserverclient/models/__init__.py @@ -1,3 +1,4 @@ +from tableauserverclient.models.collection_item import CollectionItem from tableauserverclient.models.column_item import ColumnItem from tableauserverclient.models.connection_credentials import ConnectionCredentials from tableauserverclient.models.connection_item import ConnectionItem @@ -9,6 +10,7 @@ from tableauserverclient.models.datasource_item import DatasourceItem from tableauserverclient.models.dqw_item import DQWItem from tableauserverclient.models.exceptions import UnpopulatedPropertyError +from tableauserverclient.models.extensions_item import ExtensionsServer, ExtensionsSiteSettings, SafeExtension from tableauserverclient.models.favorites_item import FavoriteItem from tableauserverclient.models.fileupload_item import FileuploadItem from tableauserverclient.models.flow_item import FlowItem @@ -30,6 +32,7 @@ ) from tableauserverclient.models.location_item import LocationItem from tableauserverclient.models.metric_item import MetricItem +from tableauserverclient.models.oidc_item import SiteOIDCConfiguration from tableauserverclient.models.pagination_item import PaginationItem from tableauserverclient.models.permissions_item import PermissionsRule, Permission from tableauserverclient.models.project_item import ProjectItem @@ -52,6 +55,7 @@ from tableauserverclient.models.extract_item import ExtractItem __all__ = [ + "CollectionItem", "ColumnItem", "ConnectionCredentials", "ConnectionItem", @@ -79,6 +83,7 @@ "BackgroundJobItem", "LocationItem", "MetricItem", + "SiteOIDCConfiguration", "PaginationItem", "Permission", "PermissionsRule", @@ -88,6 +93,7 @@ "ServerInfoItem", "SiteAuthConfiguration", "SiteItem", + "SiteOIDCConfiguration", "SubscriptionItem", "TableItem", "TableauAuth", @@ -108,4 +114,7 @@ "LinkedTaskStepItem", "LinkedTaskFlowRunItem", "ExtractItem", + "ExtensionsServer", + "ExtensionsSiteSettings", + "SafeExtension", ] diff --git a/tableauserverclient/models/collection_item.py b/tableauserverclient/models/collection_item.py new file mode 100644 index 000000000..4fdb61023 --- /dev/null +++ b/tableauserverclient/models/collection_item.py @@ -0,0 +1,52 @@ +from datetime import datetime +from typing import Optional +from xml.etree.ElementTree import Element + +from defusedxml.ElementTree import fromstring +from typing_extensions import Self + +from tableauserverclient.datetime_helpers import parse_datetime +from tableauserverclient.models.user_item import UserItem + + +class CollectionItem: + def __init__(self) -> None: + self.id: Optional[str] = None + self.name: Optional[str] = None + self.description: Optional[str] = None + self.created_at: Optional[datetime] = None + self.updated_at: Optional[datetime] = None + self.owner: Optional[UserItem] = None + self.total_item_count: Optional[int] = None + self.permissioned_item_count: Optional[int] = None + self.visibility: Optional[str] = None # Assuming visibility is a string, adjust as necessary + + @classmethod + def from_response(cls, response: bytes, ns) -> list[Self]: + parsed_response = fromstring(response) + + collection_elements = parsed_response.findall(".//t:collection", namespaces=ns) + if not collection_elements: + raise ValueError("No collection element found in the response") + + collections = [cls.from_xml(c, ns) for c in collection_elements] + return collections + + @classmethod + def from_xml(cls, xml: Element, ns) -> Self: + collection_item = cls() + collection_item.id = xml.get("id") + collection_item.name = xml.get("name") + collection_item.description = xml.get("description") + collection_item.created_at = parse_datetime(xml.get("createdAt")) + collection_item.updated_at = parse_datetime(xml.get("updatedAt")) + owner_element = xml.find(".//t:owner", namespaces=ns) + if owner_element is not None: + collection_item.owner = UserItem.from_xml(owner_element, ns) + else: + collection_item.owner = None + collection_item.total_item_count = int(xml.get("totalItemCount", 0)) + collection_item.permissioned_item_count = int(xml.get("permissionedItemCount", 0)) + collection_item.visibility = xml.get("visibility") + + return collection_item diff --git a/tableauserverclient/models/connection_item.py b/tableauserverclient/models/connection_item.py index 6a8244fb1..e155a3e3a 100644 --- a/tableauserverclient/models/connection_item.py +++ b/tableauserverclient/models/connection_item.py @@ -41,6 +41,9 @@ class ConnectionItem: server_port: str The port used for the connection. + auth_type: str + Specifies the type of authentication used by the connection. + connection_credentials: ConnectionCredentials The Connection Credentials object containing authentication details for the connection. Replaces username/password/embed_password when @@ -59,6 +62,7 @@ def __init__(self): self.username: Optional[str] = None self.connection_credentials: Optional[ConnectionCredentials] = None self._query_tagging: Optional[bool] = None + self._auth_type: Optional[str] = None @property def datasource_id(self) -> Optional[str]: @@ -91,8 +95,16 @@ def query_tagging(self, value: Optional[bool]): return self._query_tagging = value + @property + def auth_type(self) -> Optional[str]: + return self._auth_type + + @auth_type.setter + def auth_type(self, value: Optional[str]): + self._auth_type = value + def __repr__(self): - return "".format( + return "".format( **self.__dict__ ) @@ -108,10 +120,11 @@ def from_response(cls, resp, ns) -> list["ConnectionItem"]: connection_item.embed_password = string_to_bool(connection_xml.get("embedPassword", "")) connection_item.server_address = connection_xml.get("serverAddress", connection_xml.get("server", None)) connection_item.server_port = connection_xml.get("serverPort", connection_xml.get("port", None)) - connection_item.username = connection_xml.get("userName", None) + connection_item.username = connection_xml.get("userName", connection_xml.get("username", None)) connection_item._query_tagging = ( string_to_bool(s) if (s := connection_xml.get("queryTagging", None)) else None ) + connection_item._auth_type = connection_xml.get("authenticationType", None) datasource_elem = connection_xml.find(".//t:datasource", namespaces=ns) if datasource_elem is not None: connection_item._datasource_id = datasource_elem.get("id", None) @@ -139,6 +152,7 @@ def from_xml_element(cls, parsed_response, ns) -> list["ConnectionItem"]: connection_item.server_address = connection_xml.get("serverAddress", None) connection_item.server_port = connection_xml.get("serverPort", None) + connection_item._auth_type = connection_xml.get("authenticationType", None) connection_credentials = connection_xml.find(".//t:connectionCredentials", namespaces=ns) diff --git a/tableauserverclient/models/custom_view_item.py b/tableauserverclient/models/custom_view_item.py index 5cafe469c..92acb0e9b 100644 --- a/tableauserverclient/models/custom_view_item.py +++ b/tableauserverclient/models/custom_view_item.py @@ -158,10 +158,18 @@ def owner(self, value: UserItem): def workbook(self) -> Optional[WorkbookItem]: return self._workbook + @workbook.setter + def workbook(self, value: WorkbookItem) -> None: + self._workbook = value + @property def view(self) -> Optional[ViewItem]: return self._view + @view.setter + def view(self, value: ViewItem) -> None: + self._view = value + @classmethod def from_response(cls, resp, ns, workbook_id="") -> Optional["CustomViewItem"]: item = cls.list_from_response(resp, ns, workbook_id) diff --git a/tableauserverclient/models/data_freshness_policy_item.py b/tableauserverclient/models/data_freshness_policy_item.py index 6e0cb9001..209883e8c 100644 --- a/tableauserverclient/models/data_freshness_policy_item.py +++ b/tableauserverclient/models/data_freshness_policy_item.py @@ -66,7 +66,7 @@ def interval_item(self) -> Optional[list[str]]: return self._interval_item @interval_item.setter - def interval_item(self, value: list[str]): + def interval_item(self, value: Optional[list[str]]): self._interval_item = value @property @@ -127,7 +127,7 @@ def fresh_every_schedule(self) -> Optional[FreshEvery]: return self._fresh_every_schedule @fresh_every_schedule.setter - def fresh_every_schedule(self, value: FreshEvery): + def fresh_every_schedule(self, value: Optional[FreshEvery]): self._fresh_every_schedule = value @property @@ -135,7 +135,7 @@ def fresh_at_schedule(self) -> Optional[FreshAt]: return self._fresh_at_schedule @fresh_at_schedule.setter - def fresh_at_schedule(self, value: FreshAt): + def fresh_at_schedule(self, value: Optional[FreshAt]): self._fresh_at_schedule = value @classmethod diff --git a/tableauserverclient/models/datasource_item.py b/tableauserverclient/models/datasource_item.py index 5501ee332..f03a86355 100644 --- a/tableauserverclient/models/datasource_item.py +++ b/tableauserverclient/models/datasource_item.py @@ -490,7 +490,7 @@ def _set_values( self._owner = owner @classmethod - def from_response(cls, resp: str, ns: dict) -> list["DatasourceItem"]: + def from_response(cls, resp: bytes, ns: dict) -> list["DatasourceItem"]: all_datasource_items = list() parsed_response = fromstring(resp) all_datasource_xml = parsed_response.findall(".//t:datasource", namespaces=ns) @@ -535,6 +535,7 @@ def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple: project_id = None project_name = None + project = None project_elem = datasource_xml.find(".//t:project", namespaces=ns) if project_elem is not None: project = ProjectItem.from_xml(project_elem, ns) @@ -542,6 +543,7 @@ def _parse_element(datasource_xml: ET.Element, ns: dict) -> tuple: project_name = project_elem.get("name", None) owner_id = None + owner = None owner_elem = datasource_xml.find(".//t:owner", namespaces=ns) if owner_elem is not None: owner = UserItem.from_xml(owner_elem, ns) diff --git a/tableauserverclient/models/extensions_item.py b/tableauserverclient/models/extensions_item.py new file mode 100644 index 000000000..87466cdea --- /dev/null +++ b/tableauserverclient/models/extensions_item.py @@ -0,0 +1,186 @@ +from typing import overload +from typing_extensions import Self + +from defusedxml.ElementTree import fromstring + +from tableauserverclient.models.property_decorators import property_is_boolean + + +class ExtensionsServer: + def __init__(self) -> None: + self._enabled: bool | None = None + self._block_list: list[str] | None = None + + @property + def enabled(self) -> bool | None: + """Indicates whether the extensions server is enabled.""" + return self._enabled + + @enabled.setter + @property_is_boolean + def enabled(self, value: bool | None) -> None: + self._enabled = value + + @property + def block_list(self) -> list[str] | None: + """List of blocked extensions.""" + return self._block_list + + @block_list.setter + def block_list(self, value: list[str] | None) -> None: + self._block_list = value + + @classmethod + def from_response(cls: type[Self], response, ns) -> Self: + xml = fromstring(response) + obj = cls() + element = xml.find(".//t:extensionsServerSettings", namespaces=ns) + if element is None: + raise ValueError("Missing extensionsServerSettings element in response") + + if (enabled_element := element.find("./t:extensionsGloballyEnabled", namespaces=ns)) is not None: + obj.enabled = string_to_bool(enabled_element.text) + obj.block_list = [e.text for e in element.findall("./t:blockList", namespaces=ns) if e.text is not None] + + return obj + + +class SafeExtension: + def __init__( + self, url: str | None = None, full_data_allowed: bool | None = None, prompt_needed: bool | None = None + ) -> None: + self.url = url + self._full_data_allowed = full_data_allowed + self._prompt_needed = prompt_needed + + @property + def full_data_allowed(self) -> bool | None: + return self._full_data_allowed + + @full_data_allowed.setter + @property_is_boolean + def full_data_allowed(self, value: bool | None) -> None: + self._full_data_allowed = value + + @property + def prompt_needed(self) -> bool | None: + return self._prompt_needed + + @prompt_needed.setter + @property_is_boolean + def prompt_needed(self, value: bool | None) -> None: + self._prompt_needed = value + + +class ExtensionsSiteSettings: + def __init__(self) -> None: + self._enabled: bool | None = None + self._use_default_setting: bool | None = None + self.safe_list: list[SafeExtension] | None = None + self._allow_trusted: bool | None = None + self._include_tableau_built: bool | None = None + self._include_partner_built: bool | None = None + self._include_sandboxed: bool | None = None + + @property + def enabled(self) -> bool | None: + return self._enabled + + @enabled.setter + @property_is_boolean + def enabled(self, value: bool | None) -> None: + self._enabled = value + + @property + def use_default_setting(self) -> bool | None: + return self._use_default_setting + + @use_default_setting.setter + @property_is_boolean + def use_default_setting(self, value: bool | None) -> None: + self._use_default_setting = value + + @property + def allow_trusted(self) -> bool | None: + return self._allow_trusted + + @allow_trusted.setter + @property_is_boolean + def allow_trusted(self, value: bool | None) -> None: + self._allow_trusted = value + + @property + def include_tableau_built(self) -> bool | None: + return self._include_tableau_built + + @include_tableau_built.setter + @property_is_boolean + def include_tableau_built(self, value: bool | None) -> None: + self._include_tableau_built = value + + @property + def include_partner_built(self) -> bool | None: + return self._include_partner_built + + @include_partner_built.setter + @property_is_boolean + def include_partner_built(self, value: bool | None) -> None: + self._include_partner_built = value + + @property + def include_sandboxed(self) -> bool | None: + return self._include_sandboxed + + @include_sandboxed.setter + @property_is_boolean + def include_sandboxed(self, value: bool | None) -> None: + self._include_sandboxed = value + + @classmethod + def from_response(cls: type[Self], response, ns) -> Self: + xml = fromstring(response) + element = xml.find(".//t:extensionsSiteSettings", namespaces=ns) + obj = cls() + if element is None: + raise ValueError("Missing extensionsSiteSettings element in response") + + if (enabled_element := element.find("./t:extensionsEnabled", namespaces=ns)) is not None: + obj.enabled = string_to_bool(enabled_element.text) + if (default_settings_element := element.find("./t:useDefaultSetting", namespaces=ns)) is not None: + obj.use_default_setting = string_to_bool(default_settings_element.text) + if (allow_trusted_element := element.find("./t:allowTrusted", namespaces=ns)) is not None: + obj.allow_trusted = string_to_bool(allow_trusted_element.text) + if (include_tableau_built_element := element.find("./t:includeTableauBuilt", namespaces=ns)) is not None: + obj.include_tableau_built = string_to_bool(include_tableau_built_element.text) + if (include_partner_built_element := element.find("./t:includePartnerBuilt", namespaces=ns)) is not None: + obj.include_partner_built = string_to_bool(include_partner_built_element.text) + if (include_sandboxed_element := element.find("./t:includeSandboxed", namespaces=ns)) is not None: + obj.include_sandboxed = string_to_bool(include_sandboxed_element.text) + + safe_list = [] + for safe_extension_element in element.findall("./t:safeList", namespaces=ns): + url = safe_extension_element.find("./t:url", namespaces=ns) + full_data_allowed = safe_extension_element.find("./t:fullDataAllowed", namespaces=ns) + prompt_needed = safe_extension_element.find("./t:promptNeeded", namespaces=ns) + + safe_extension = SafeExtension( + url=url.text if url is not None else None, + full_data_allowed=string_to_bool(full_data_allowed.text) if full_data_allowed is not None else None, + prompt_needed=string_to_bool(prompt_needed.text) if prompt_needed is not None else None, + ) + safe_list.append(safe_extension) + + obj.safe_list = safe_list + return obj + + +@overload +def string_to_bool(s: str) -> bool: ... + + +@overload +def string_to_bool(s: None) -> None: ... + + +def string_to_bool(s): + return s.lower() == "true" if s is not None else None diff --git a/tableauserverclient/models/favorites_item.py b/tableauserverclient/models/favorites_item.py index 4fea280f7..1189efc31 100644 --- a/tableauserverclient/models/favorites_item.py +++ b/tableauserverclient/models/favorites_item.py @@ -1,9 +1,8 @@ import logging -from typing import Union +from typing import TypedDict, Union from defusedxml.ElementTree import fromstring - -from tableauserverclient.models.tableau_types import TableauItem +from tableauserverclient.models.collection_item import CollectionItem from tableauserverclient.models.datasource_item import DatasourceItem from tableauserverclient.models.flow_item import FlowItem from tableauserverclient.models.project_item import ProjectItem @@ -13,16 +12,22 @@ from tableauserverclient.helpers.logging import logger -FavoriteType = dict[ - str, - list[TableauItem], -] + +class FavoriteType(TypedDict): + collections: list[CollectionItem] + datasources: list[DatasourceItem] + flows: list[FlowItem] + projects: list[ProjectItem] + metrics: list[MetricItem] + views: list[ViewItem] + workbooks: list[WorkbookItem] class FavoriteItem: @classmethod def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType: favorites: FavoriteType = { + "collections": [], "datasources": [], "flows": [], "projects": [], @@ -32,6 +37,7 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType: } parsed_response = fromstring(xml) + collections_xml = parsed_response.findall(".//t:favorite/t:collection", namespace) datasources_xml = parsed_response.findall(".//t:favorite/t:datasource", namespace) flows_xml = parsed_response.findall(".//t:favorite/t:flow", namespace) metrics_xml = parsed_response.findall(".//t:favorite/t:metric", namespace) @@ -40,13 +46,14 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType: workbooks_xml = parsed_response.findall(".//t:favorite/t:workbook", namespace) logger.debug( - "ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}".format( + "ds: {}, flows: {}, metrics: {}, projects: {}, views: {}, wbs: {}, collections: {}".format( len(datasources_xml), len(flows_xml), len(metrics_xml), len(projects_xml), len(views_xml), len(workbooks_xml), + len(collections_xml), ) ) for datasource in datasources_xml: @@ -85,5 +92,11 @@ def from_response(cls, xml: Union[str, bytes], namespace: dict) -> FavoriteType: logger.debug(fav_workbook) favorites["workbooks"].append(fav_workbook) + for collection in collections_xml: + fav_collection = CollectionItem.from_xml(collection, namespace) + if fav_collection: + logger.debug(fav_collection) + favorites["collections"].append(fav_collection) + logger.debug(favorites) return favorites diff --git a/tableauserverclient/models/flow_item.py b/tableauserverclient/models/flow_item.py index 063897e41..0aed3d257 100644 --- a/tableauserverclient/models/flow_item.py +++ b/tableauserverclient/models/flow_item.py @@ -129,7 +129,7 @@ def description(self) -> Optional[str]: return self._description @description.setter - def description(self, value: str) -> None: + def description(self, value: Optional[str]) -> None: self._description = value @property diff --git a/tableauserverclient/models/group_item.py b/tableauserverclient/models/group_item.py index 00f35e518..c368b51f7 100644 --- a/tableauserverclient/models/group_item.py +++ b/tableauserverclient/models/group_item.py @@ -1,6 +1,7 @@ from typing import Callable, Optional, TYPE_CHECKING from defusedxml.ElementTree import fromstring +from typing_extensions import Self from .exceptions import UnpopulatedPropertyError from .property_decorators import property_not_empty, property_is_enum @@ -92,7 +93,7 @@ def name(self) -> Optional[str]: return self._name @name.setter - def name(self, value: str) -> None: + def name(self, value: Optional[str]) -> None: self._name = value @property @@ -157,3 +158,8 @@ def from_response(cls, resp, ns) -> list["GroupItem"]: @staticmethod def as_reference(id_: str) -> ResourceReference: return ResourceReference(id_, GroupItem.tag_name) + + def to_reference(self: Self) -> ResourceReference: + if self.id is None: + raise ValueError(f"{self.__class__.__qualname__} must have id to be converted to reference") + return ResourceReference(self.id, self.tag_name) diff --git a/tableauserverclient/models/groupset_item.py b/tableauserverclient/models/groupset_item.py index aa653a79e..ad00504cd 100644 --- a/tableauserverclient/models/groupset_item.py +++ b/tableauserverclient/models/groupset_item.py @@ -2,6 +2,7 @@ import xml.etree.ElementTree as ET from defusedxml.ElementTree import fromstring +from typing_extensions import Self from tableauserverclient.models.group_item import GroupItem from tableauserverclient.models.reference_item import ResourceReference @@ -24,6 +25,14 @@ def __str__(self) -> str: def __repr__(self) -> str: return self.__str__() + @property + def name(self) -> Optional[str]: + return self._name + + @name.setter + def name(self, value: Optional[str]) -> None: + self._name = value + @classmethod def from_response(cls, response: bytes, ns: dict[str, str]) -> list["GroupSetItem"]: parsed_response = fromstring(response) @@ -51,3 +60,8 @@ def get_group(group_xml: ET.Element) -> GroupItem: @staticmethod def as_reference(id_: str) -> ResourceReference: return ResourceReference(id_, GroupSetItem.tag_name) + + def to_reference(self: Self) -> ResourceReference: + if self.id is None: + raise ValueError(f"{self.__class__.__qualname__} must have id to be converted to reference") + return ResourceReference(self.id, self.tag_name) diff --git a/tableauserverclient/models/interval_item.py b/tableauserverclient/models/interval_item.py index 52fd658c5..d365ad51c 100644 --- a/tableauserverclient/models/interval_item.py +++ b/tableauserverclient/models/interval_item.py @@ -305,6 +305,7 @@ def interval(self, interval_values): "Fourth", "Fifth", "Last", + "Customized Monthly", ] for value in range(1, 32): VALID_INTERVALS.append(str(value)) diff --git a/tableauserverclient/models/oidc_item.py b/tableauserverclient/models/oidc_item.py new file mode 100644 index 000000000..6e9626ed1 --- /dev/null +++ b/tableauserverclient/models/oidc_item.py @@ -0,0 +1,82 @@ +from typing import Optional +from defusedxml.ElementTree import fromstring + + +class SiteOIDCConfiguration: + def __init__(self) -> None: + self.enabled: bool = False + self.test_login_url: Optional[str] = None + self.known_provider_alias: Optional[str] = None + self.allow_embedded_authentication: bool = False + self.use_full_name: bool = False + self.idp_configuration_name: Optional[str] = None + self.idp_configuration_id: Optional[str] = None + self.client_id: Optional[str] = None + self.client_secret: Optional[str] = None + self.authorization_endpoint: Optional[str] = None + self.token_endpoint: Optional[str] = None + self.userinfo_endpoint: Optional[str] = None + self.jwks_uri: Optional[str] = None + self.end_session_endpoint: Optional[str] = None + self.custom_scope: Optional[str] = None + self.essential_acr_values: Optional[str] = None + self.email_mapping: Optional[str] = None + self.first_name_mapping: Optional[str] = None + self.last_name_mapping: Optional[str] = None + self.full_name_mapping: Optional[str] = None + self.prompt: Optional[str] = None + self.client_authentication: Optional[str] = None + self.voluntary_acr_values: Optional[str] = None + + def __str__(self) -> str: + return ( + f"{self.__class__.__qualname__}(enabled={self.enabled}, " + f"test_login_url={self.test_login_url}, " + f"idp_configuration_name={self.idp_configuration_name}, " + f"idp_configuration_id={self.idp_configuration_id}, " + f"client_id={self.client_id})" + ) + + def __repr__(self) -> str: + return f"<{str(self)}>" + + @classmethod + def from_response(cls, raw_xml: bytes, ns) -> "SiteOIDCConfiguration": + """ + Parses the raw XML bytes and returns a SiteOIDCConfiguration object. + """ + root = fromstring(raw_xml) + elem = root.find("t:siteOIDCConfiguration", namespaces=ns) + if elem is None: + raise ValueError("No siteOIDCConfiguration element found in the XML.") + config = cls() + + config.enabled = str_to_bool(elem.get("enabled", "false")) + config.test_login_url = elem.get("testLoginUrl") + config.known_provider_alias = elem.get("knownProviderAlias") + config.allow_embedded_authentication = str_to_bool(elem.get("allowEmbeddedAuthentication", "false").lower()) + config.use_full_name = str_to_bool(elem.get("useFullName", "false").lower()) + config.idp_configuration_name = elem.get("idpConfigurationName") + config.idp_configuration_id = elem.get("idpConfigurationId") + config.client_id = elem.get("clientId") + config.client_secret = elem.get("clientSecret") + config.authorization_endpoint = elem.get("authorizationEndpoint") + config.token_endpoint = elem.get("tokenEndpoint") + config.userinfo_endpoint = elem.get("userinfoEndpoint") + config.jwks_uri = elem.get("jwksUri") + config.end_session_endpoint = elem.get("endSessionEndpoint") + config.custom_scope = elem.get("customScope") + config.essential_acr_values = elem.get("essentialAcrValues") + config.email_mapping = elem.get("emailMapping") + config.first_name_mapping = elem.get("firstNameMapping") + config.last_name_mapping = elem.get("lastNameMapping") + config.full_name_mapping = elem.get("fullNameMapping") + config.prompt = elem.get("prompt") + config.client_authentication = elem.get("clientAuthentication") + config.voluntary_acr_values = elem.get("voluntaryAcrValues") + + return config + + +def str_to_bool(s: str) -> bool: + return s == "true" diff --git a/tableauserverclient/models/permissions_item.py b/tableauserverclient/models/permissions_item.py index b91cf89ca..bc29234c4 100644 --- a/tableauserverclient/models/permissions_item.py +++ b/tableauserverclient/models/permissions_item.py @@ -44,6 +44,7 @@ class Capability: SaveAs = "SaveAs" PulseMetricDefine = "PulseMetricDefine" ExtractRefresh = "ExtractRefresh" + WebAuthoringForFlows = "WebAuthoringForFlows" def __repr__(self): return "" diff --git a/tableauserverclient/models/project_item.py b/tableauserverclient/models/project_item.py index 1ab369ba7..dd90f8bfb 100644 --- a/tableauserverclient/models/project_item.py +++ b/tableauserverclient/models/project_item.py @@ -86,9 +86,10 @@ def __init__( content_permissions: Optional[str] = None, parent_id: Optional[str] = None, samples: Optional[bool] = None, + id: Optional[str] = None, ) -> None: self._content_permissions = None - self._id: Optional[str] = None + self._id: Optional[str] = id self.description: Optional[str] = description self.name: str = name self.content_permissions: Optional[str] = content_permissions @@ -194,7 +195,7 @@ def name(self) -> Optional[str]: return self._name @name.setter - def name(self, value: str) -> None: + def name(self, value: Optional[str]) -> None: self._name = value @property diff --git a/tableauserverclient/models/property_decorators.py b/tableauserverclient/models/property_decorators.py index 5048b3498..050346594 100644 --- a/tableauserverclient/models/property_decorators.py +++ b/tableauserverclient/models/property_decorators.py @@ -1,7 +1,7 @@ import datetime import re from functools import wraps -from typing import Any, Optional +from typing import Any from collections.abc import Container from tableauserverclient.datetime_helpers import parse_datetime @@ -67,7 +67,7 @@ def wrapper(self, value): return wrapper -def property_is_int(range: tuple[int, int], allowed: Optional[Container[Any]] = None): +def property_is_int(range: tuple[int, int], allowed: Container[Any] | None = None): """Takes a range of ints and a list of exemptions to check against when setting a property on a model. The range is a tuple of (min, max) and the allowed list (empty by default) allows values outside that range. diff --git a/tableauserverclient/models/reference_item.py b/tableauserverclient/models/reference_item.py index 4c1fff564..30536b4d0 100644 --- a/tableauserverclient/models/reference_item.py +++ b/tableauserverclient/models/reference_item.py @@ -1,9 +1,12 @@ +from typing_extensions import Self + + class ResourceReference: - def __init__(self, id_, tag_name): + def __init__(self, id_: str | None, tag_name: str) -> None: self.id = id_ self.tag_name = tag_name - def __str__(self): + def __str__(self) -> str: return f"" __repr__ = __str__ @@ -13,18 +16,21 @@ def __eq__(self, other: object) -> bool: return False return (self.id == other.id) and (self.tag_name == other.tag_name) + def __hash__(self: Self) -> int: + return hash((self.id, self.tag_name)) + @property - def id(self): + def id(self) -> str | None: return self._id @id.setter - def id(self, value): + def id(self, value: str | None) -> None: self._id = value @property - def tag_name(self): + def tag_name(self) -> str: return self._tag_name @tag_name.setter - def tag_name(self, value): + def tag_name(self, value: str) -> None: self._tag_name = value diff --git a/tableauserverclient/models/schedule_item.py b/tableauserverclient/models/schedule_item.py index a2118e3d6..794d9883b 100644 --- a/tableauserverclient/models/schedule_item.py +++ b/tableauserverclient/models/schedule_item.py @@ -1,6 +1,6 @@ import xml.etree.ElementTree as ET from datetime import datetime -from typing import Optional, Union +from typing import Optional, Union, TYPE_CHECKING from defusedxml.ElementTree import fromstring @@ -16,6 +16,10 @@ property_is_enum, ) +if TYPE_CHECKING: + from requests import Response + + Interval = Union[HourlyInterval, DailyInterval, WeeklyInterval, MonthlyInterval] @@ -407,3 +411,8 @@ def _read_warnings(parsed_response, ns): for warning_xml in all_warning_xml: warnings.append(warning_xml.get("message", None)) return warnings + + +def parse_batch_schedule_state(response: "Response", ns) -> list[str]: + xml = fromstring(response.content) + return [text for tag in xml.findall(".//t:scheduleLuid", namespaces=ns) if (text := tag.text)] diff --git a/tableauserverclient/models/site_item.py b/tableauserverclient/models/site_item.py index ab65b97b5..9cda5c898 100644 --- a/tableauserverclient/models/site_item.py +++ b/tableauserverclient/models/site_item.py @@ -85,6 +85,9 @@ class SiteItem: state: str Shows the current state of the site (Active or Suspended). + attribute_capture_enabled: Optional[str] + Enables user attributes for all Tableau Server embedding workflows. + """ _user_quota: Optional[int] = None @@ -164,6 +167,7 @@ def __init__( time_zone=None, auto_suspend_refresh_enabled: bool = True, auto_suspend_refresh_inactivity_window: int = 30, + attribute_capture_enabled: Optional[bool] = None, ): self._admin_mode = None self._id: Optional[str] = None @@ -217,6 +221,7 @@ def __init__( self.time_zone = time_zone self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window + self.attribute_capture_enabled = attribute_capture_enabled @property def admin_mode(self) -> Optional[str]: @@ -720,6 +725,7 @@ def _parse_common_tags(self, site_xml, ns): time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) = self._parse_element(site_xml, ns) self._set_values( @@ -774,6 +780,7 @@ def _parse_common_tags(self, site_xml, ns): time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) return self @@ -830,6 +837,7 @@ def _set_values( time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ): if id is not None: self._id = id @@ -937,6 +945,7 @@ def _set_values( self.auto_suspend_refresh_enabled = auto_suspend_refresh_enabled if auto_suspend_refresh_inactivity_window is not None: self.auto_suspend_refresh_inactivity_window = auto_suspend_refresh_inactivity_window + self.attribute_capture_enabled = attribute_capture_enabled @classmethod def from_response(cls, resp, ns) -> list["SiteItem"]: @@ -996,6 +1005,7 @@ def from_response(cls, resp, ns) -> list["SiteItem"]: time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) = cls._parse_element(site_xml, ns) site_item = cls(name, content_url) @@ -1051,6 +1061,7 @@ def from_response(cls, resp, ns) -> list["SiteItem"]: time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) all_site_items.append(site_item) return all_site_items @@ -1132,6 +1143,9 @@ def _parse_element(site_xml, ns): flows_enabled = string_to_bool(site_xml.get("flowsEnabled", "")) cataloging_enabled = string_to_bool(site_xml.get("catalogingEnabled", "")) + attribute_capture_enabled = ( + string_to_bool(ace) if (ace := site_xml.get("attributeCaptureEnabled")) is not None else None + ) return ( id, @@ -1185,6 +1199,7 @@ def _parse_element(site_xml, ns): time_zone, auto_suspend_refresh_enabled, auto_suspend_refresh_inactivity_window, + attribute_capture_enabled, ) @@ -1215,6 +1230,17 @@ def from_response(cls, resp: bytes, ns: dict) -> list["SiteAuthConfiguration"]: all_auth_configs.append(auth_config) return all_auth_configs + def __str__(self): + return ( + f"{self.__class__.__qualname__}(auth_setting={self.auth_setting}, " + f"enabled={self.enabled}, " + f"idp_configuration_id={self.idp_configuration_id}, " + f"idp_configuration_name={self.idp_configuration_name})" + ) + + def __repr__(self): + return f"<{str(self)}>" + # Used to convert string represented boolean to a boolean type def string_to_bool(s: str) -> bool: diff --git a/tableauserverclient/models/tableau_auth.py b/tableauserverclient/models/tableau_auth.py index 7d7981433..7922ff562 100644 --- a/tableauserverclient/models/tableau_auth.py +++ b/tableauserverclient/models/tableau_auth.py @@ -87,7 +87,7 @@ def __repr__(self): uid = f", user_id_to_impersonate=f{self.user_id_to_impersonate}" else: uid = "" - return f"" + return f"<{self.__class__.__qualname__} username={self.username} password=redacted (site={self.site_id}{uid})>" # A Tableau-generated Personal Access Token @@ -155,8 +155,8 @@ def __repr__(self): else: uid = "" return ( - f"" + f"<{self.__class__.__qualname__}(name={self.token_name} token={self.personal_access_token[:2]}..." + f"site={self.site_id}{uid}) >" ) @@ -198,19 +198,26 @@ class JWTAuth(Credentials): """ - def __init__(self, jwt: str, site_id: Optional[str] = None, user_id_to_impersonate: Optional[str] = None) -> None: + def __init__( + self, + jwt: str, + isUat: bool = False, + site_id: Optional[str] = None, + user_id_to_impersonate: Optional[str] = None, + ) -> None: if jwt is None: raise TabError("Must provide a JWT token when using JWT authentication") super().__init__(site_id, user_id_to_impersonate) self.jwt = jwt + self.isUat = isUat @property def credentials(self) -> dict[str, str]: - return {"jwt": self.jwt} + return {"jwt": self.jwt, "isUat": str(self.isUat).lower()} def __repr__(self): if self.user_id_to_impersonate: uid = f", user_id_to_impersonate=f{self.user_id_to_impersonate}" else: uid = "" - return f"<{self.__class__.__qualname__} jwt={self.jwt[:5]}... (site={self.site_id}{uid})>" + return f"<{self.__class__.__qualname__} jwt={self.jwt[:5]}... isUat={self.isUat} (site={self.site_id}{uid})>" diff --git a/tableauserverclient/models/user_item.py b/tableauserverclient/models/user_item.py index c995b4e07..9add6aec0 100644 --- a/tableauserverclient/models/user_item.py +++ b/tableauserverclient/models/user_item.py @@ -5,6 +5,7 @@ from typing import Optional, TYPE_CHECKING from defusedxml.ElementTree import fromstring +from typing_extensions import Self from tableauserverclient.datetime_helpers import parse_datetime from tableauserverclient.models.site_item import SiteAuthConfiguration @@ -17,6 +18,7 @@ if TYPE_CHECKING: from tableauserverclient.server import Pager + from tableauserverclient.models.favorites_item import FavoriteType class UserItem: @@ -131,7 +133,7 @@ def __init__( self._id: Optional[str] = None self._last_login: Optional[datetime] = None self._workbooks = None - self._favorites: Optional[dict[str, list]] = None + self._favorites: Optional["FavoriteType"] = None self._groups = None self.email: Optional[str] = None self.fullname: Optional[str] = None @@ -185,7 +187,7 @@ def name(self) -> Optional[str]: return self._name @name.setter - def name(self, value: str): + def name(self, value: Optional[str]): self._name = value # valid: username, domain/username, username@domain, domain/username@email @@ -218,7 +220,7 @@ def workbooks(self) -> "Pager": return self._workbooks() @property - def favorites(self) -> dict[str, list]: + def favorites(self) -> "FavoriteType": if self._favorites is None: error = "User item must be populated with favorites first." raise UnpopulatedPropertyError(error) @@ -376,6 +378,11 @@ def _parse_xml(cls, element_name, resp, ns): def as_reference(id_) -> ResourceReference: return ResourceReference(id_, UserItem.tag_name) + def to_reference(self: Self) -> ResourceReference: + if self.id is None: + raise ValueError(f"{self.__class__.__qualname__} must have id to be converted to reference") + return ResourceReference(self.id, self.tag_name) + @staticmethod def _parse_element(user_xml, ns): id = user_xml.get("id", None) diff --git a/tableauserverclient/models/workbook_item.py b/tableauserverclient/models/workbook_item.py index a3ede65d6..df70df390 100644 --- a/tableauserverclient/models/workbook_item.py +++ b/tableauserverclient/models/workbook_item.py @@ -330,7 +330,7 @@ def thumbnails_user_id(self) -> Optional[str]: return self._thumbnails_user_id @thumbnails_user_id.setter - def thumbnails_user_id(self, value: str): + def thumbnails_user_id(self, value: Optional[str]): self._thumbnails_user_id = value @property @@ -338,7 +338,7 @@ def thumbnails_group_id(self) -> Optional[str]: return self._thumbnails_group_id @thumbnails_group_id.setter - def thumbnails_group_id(self, value: str): + def thumbnails_group_id(self, value: Optional[str]): self._thumbnails_group_id = value @property diff --git a/tableauserverclient/server/endpoint/__init__.py b/tableauserverclient/server/endpoint/__init__.py index b05b9addd..d944bc429 100644 --- a/tableauserverclient/server/endpoint/__init__.py +++ b/tableauserverclient/server/endpoint/__init__.py @@ -6,6 +6,7 @@ from tableauserverclient.server.endpoint.datasources_endpoint import Datasources from tableauserverclient.server.endpoint.endpoint import Endpoint, QuerysetEndpoint from tableauserverclient.server.endpoint.exceptions import ServerResponseError, MissingRequiredFieldError +from tableauserverclient.server.endpoint.extensions_endpoint import Extensions from tableauserverclient.server.endpoint.favorites_endpoint import Favorites from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads from tableauserverclient.server.endpoint.flow_runs_endpoint import FlowRuns @@ -17,6 +18,7 @@ from tableauserverclient.server.endpoint.linked_tasks_endpoint import LinkedTasks from tableauserverclient.server.endpoint.metadata_endpoint import Metadata from tableauserverclient.server.endpoint.metrics_endpoint import Metrics +from tableauserverclient.server.endpoint.oidc_endpoint import OIDC from tableauserverclient.server.endpoint.projects_endpoint import Projects from tableauserverclient.server.endpoint.schedules_endpoint import Schedules from tableauserverclient.server.endpoint.server_info_endpoint import ServerInfo @@ -41,6 +43,7 @@ "QuerysetEndpoint", "MissingRequiredFieldError", "Endpoint", + "Extensions", "Favorites", "Fileuploads", "FlowRuns", @@ -52,6 +55,7 @@ "LinkedTasks", "Metadata", "Metrics", + "OIDC", "Projects", "Schedules", "ServerInfo", diff --git a/tableauserverclient/server/endpoint/datasources_endpoint.py b/tableauserverclient/server/endpoint/datasources_endpoint.py index 168446974..6a734f7b3 100644 --- a/tableauserverclient/server/endpoint/datasources_endpoint.py +++ b/tableauserverclient/server/endpoint/datasources_endpoint.py @@ -6,8 +6,8 @@ from contextlib import closing from pathlib import Path -from typing import Literal, Optional, TYPE_CHECKING, Union, overload -from collections.abc import Iterable, Mapping, Sequence +from typing import Literal, Optional, TYPE_CHECKING, TypedDict, TypeVar, Union, overload +from collections.abc import Iterable, Sequence from tableauserverclient.helpers.headers import fix_filename from tableauserverclient.models.dqw_item import DQWItem @@ -50,13 +50,50 @@ FileObject = Union[io.BufferedReader, io.BytesIO] PathOrFile = Union[FilePath, FileObject] -FilePath = Union[str, os.PathLike] FileObjectR = Union[io.BufferedReader, io.BytesIO] FileObjectW = Union[io.BufferedWriter, io.BytesIO] PathOrFileR = Union[FilePath, FileObjectR] PathOrFileW = Union[FilePath, FileObjectW] +HyperActionCondition = TypedDict( + "HyperActionCondition", + { + "op": str, + "target-col": str, + "source-col": str, + }, +) + +HyperActionRow = TypedDict( + "HyperActionRow", + { + "action": Literal[ + "update", + "upsert", + "delete", + ], + "source-table": str, + "target-table": str, + "condition": HyperActionCondition, + }, +) + +HyperActionTable = TypedDict( + "HyperActionTable", + { + "action": Literal[ + "insert", + "replace", + ], + "source-table": str, + "target-table": str, + }, +) + +HyperAction = Union[HyperActionTable, HyperActionRow] + + class Datasources(QuerysetEndpoint[DatasourceItem], TaggingMixin[DatasourceItem]): def __init__(self, parent_srv: "Server") -> None: super().__init__(parent_srv) @@ -191,16 +228,34 @@ def delete(self, datasource_id: str) -> None: self.delete_request(url) logger.info(f"Deleted single datasource (ID: {datasource_id})") + T = TypeVar("T", bound=FileObjectW) + + @overload + def download( + self, + datasource_id: str, + filepath: T, + include_extract: bool = True, + ) -> T: ... + + @overload + def download( + self, + datasource_id: str, + filepath: Optional[FilePath] = None, + include_extract: bool = True, + ) -> str: ... + # Download 1 datasource by id @api(version="2.0") @parameter_added_in(no_extract="2.5") @parameter_added_in(include_extract="2.5") def download( self, - datasource_id: str, - filepath: Optional[PathOrFileW] = None, - include_extract: bool = True, - ) -> PathOrFileW: + datasource_id, + filepath=None, + include_extract=True, + ): """ Downloads the specified data source from a site. The data source is downloaded as a .tdsx file. @@ -319,8 +374,63 @@ def update_connection( logger.info(f"Updated datasource item (ID: {datasource_item.id} & connection item {connection_item.id}") return connection + @api(version="3.26") + def update_connections( + self, + datasource_item: DatasourceItem, + connection_luids: Iterable[str], + authentication_type: str, + username: Optional[str] = None, + password: Optional[str] = None, + embed_password: Optional[bool] = None, + ) -> list[ConnectionItem]: + """ + Bulk updates one or more datasource connections by LUID. + + Parameters + ---------- + datasource_item : DatasourceItem + The datasource item containing the connections. + + connection_luids : Iterable of str + The connection LUIDs to update. + + authentication_type : str + The authentication type to use (e.g., 'auth-keypair'). + + username : str, optional + The username to set. + + password : str, optional + The password or secret to set. + + embed_password : bool, optional + Whether to embed the password. + + Returns + ------- + Iterable of str + The connection LUIDs that were updated. + """ + + url = f"{self.baseurl}/{datasource_item.id}/connections" + + request_body = RequestFactory.Datasource.update_connections_req( + connection_luids=connection_luids, + authentication_type=authentication_type, + username=username, + password=password, + embed_password=embed_password, + ) + server_response = self.put_request(url, request_body) + connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) + updated_ids: list[str] = [conn.id for conn in connection_items] + + logger.info(f"Updated connections for datasource {datasource_item.id}: {', '.join(updated_ids)}") + return connection_items + @api(version="2.8") - def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> JobItem: + def refresh(self, datasource_item: Union[DatasourceItem, str], incremental: bool = False) -> JobItem: """ Refreshes the extract of an existing workbook. @@ -328,8 +438,8 @@ def refresh(self, datasource_item: DatasourceItem, incremental: bool = False) -> Parameters ---------- - workbook_item : WorkbookItem | str - The workbook item or workbook ID. + workbook_item : DatasourceItem | str + The datasource item or datasource ID. incremental: bool Whether to do a full refresh or incremental refresh of the extract data @@ -424,13 +534,13 @@ def publish( @parameter_added_in(as_job="3.0") def publish( self, - datasource_item: DatasourceItem, - file: PathOrFileR, - mode: str, - connection_credentials: Optional[ConnectionCredentials] = None, - connections: Optional[Sequence[ConnectionItem]] = None, - as_job: bool = False, - ) -> Union[DatasourceItem, JobItem]: + datasource_item, + file, + mode, + connection_credentials=None, + connections=None, + as_job=False, + ): """ Publishes a data source to a server, or appends data to an existing data source. @@ -576,7 +686,7 @@ def update_hyper_data( datasource_or_connection_item: Union[DatasourceItem, ConnectionItem, str], *, request_id: str, - actions: Sequence[Mapping], + actions: Sequence[HyperAction], payload: Optional[FilePath] = None, ) -> JobItem: """ @@ -843,15 +953,35 @@ def _get_datasource_revisions( revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, datasource_item) return revisions - # Download 1 datasource revision by revision number - @api(version="2.3") + T = TypeVar("T", bound=FileObjectW) + + @overload def download_revision( self, datasource_id: str, revision_number: Optional[str], - filepath: Optional[PathOrFileW] = None, + filepath: T, include_extract: bool = True, - ) -> PathOrFileW: + ) -> T: ... + + @overload + def download_revision( + self, + datasource_id: str, + revision_number: Optional[str], + filepath: Optional[FilePath] = None, + include_extract: bool = True, + ) -> str: ... + + # Download 1 datasource revision by revision number + @api(version="2.3") + def download_revision( + self, + datasource_id, + revision_number, + filepath=None, + include_extract=True, + ): """ Downloads a specific version of a data source prior to the current one in .tdsx format. To download the current version of a data source set diff --git a/tableauserverclient/server/endpoint/extensions_endpoint.py b/tableauserverclient/server/endpoint/extensions_endpoint.py new file mode 100644 index 000000000..d14855931 --- /dev/null +++ b/tableauserverclient/server/endpoint/extensions_endpoint.py @@ -0,0 +1,79 @@ +from tableauserverclient.models.extensions_item import ExtensionsServer, ExtensionsSiteSettings +from tableauserverclient.server.endpoint.endpoint import Endpoint +from tableauserverclient.server.endpoint.endpoint import api +from tableauserverclient.server.request_factory import RequestFactory + + +class Extensions(Endpoint): + def __init__(self, parent_srv): + super().__init__(parent_srv) + + @property + def _server_baseurl(self) -> str: + return f"{self.parent_srv.baseurl}/settings/extensions" + + @property + def baseurl(self) -> str: + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/settings/extensions" + + @api(version="3.21") + def get_server_settings(self) -> ExtensionsServer: + """Lists the settings for extensions of a server + + Returns + ------- + ExtensionsServer + The server extensions settings + """ + response = self.get_request(self._server_baseurl) + return ExtensionsServer.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.21") + def update_server_settings(self, extensions_server: ExtensionsServer) -> ExtensionsServer: + """Updates the settings for extensions of a server. Overwrites all existing settings. Any + sites omitted from the block list will be unblocked. + + Parameters + ---------- + extensions_server : ExtensionsServer + The server extensions settings to update + + Returns + ------- + ExtensionsServer + The updated server extensions settings + """ + req = RequestFactory.Extensions.update_server_extensions(extensions_server) + response = self.put_request(self._server_baseurl, req) + return ExtensionsServer.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.21") + def get(self) -> ExtensionsSiteSettings: + """Lists the extensions settings for the site + + Returns + ------- + ExtensionsSiteSettings + The site extensions settings + """ + response = self.get_request(self.baseurl) + return ExtensionsSiteSettings.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.21") + def update(self, extensions_site_settings: ExtensionsSiteSettings) -> ExtensionsSiteSettings: + """Updates the extensions settings for the site. Any extensions omitted + from the safe extensions list will be removed. + + Parameters + ---------- + extensions_site_settings : ExtensionsSiteSettings + The site extensions settings to update + + Returns + ------- + ExtensionsSiteSettings + The updated site extensions settings + """ + req = RequestFactory.Extensions.update_site_extensions(extensions_site_settings) + response = self.put_request(self.baseurl, req) + return ExtensionsSiteSettings.from_response(response.content, self.parent_srv.namespace) diff --git a/tableauserverclient/server/endpoint/flow_task_endpoint.py b/tableauserverclient/server/endpoint/flow_task_endpoint.py index 9e21661e6..cb210a9ef 100644 --- a/tableauserverclient/server/endpoint/flow_task_endpoint.py +++ b/tableauserverclient/server/endpoint/flow_task_endpoint.py @@ -18,7 +18,7 @@ def baseurl(self) -> str: return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/tasks/flows" @api(version="3.22") - def create(self, flow_item: TaskItem) -> TaskItem: + def create(self, flow_item: TaskItem) -> bytes: if not flow_item: error = "No flow provided" raise ValueError(error) diff --git a/tableauserverclient/server/endpoint/flows_endpoint.py b/tableauserverclient/server/endpoint/flows_endpoint.py index 42c9d4c1e..ea29f2963 100644 --- a/tableauserverclient/server/endpoint/flows_endpoint.py +++ b/tableauserverclient/server/endpoint/flows_endpoint.py @@ -308,7 +308,7 @@ def update_connection(self, flow_item: FlowItem, connection_item: ConnectionItem return connection @api(version="3.3") - def refresh(self, flow_item: FlowItem) -> JobItem: + def refresh(self, flow_item: Union[FlowItem, str]) -> JobItem: """ Runs the flow to refresh the data. @@ -316,15 +316,16 @@ def refresh(self, flow_item: FlowItem) -> JobItem: Parameters ---------- - flow_item: FlowItem - The flow item to refresh. + flow_item: FlowItem | str + The FlowItem or str of the flow id to refresh. Returns ------- JobItem The job item that was created to refresh the flow. """ - url = f"{self.baseurl}/{flow_item.id}/run" + flow_id = getattr(flow_item, "id", flow_item) + url = f"{self.baseurl}/{flow_id}/run" empty_req = RequestFactory.Empty.empty_req() server_response = self.post_request(url, empty_req) new_job = JobItem.from_response(server_response.content, self.parent_srv.namespace)[0] diff --git a/tableauserverclient/server/endpoint/oidc_endpoint.py b/tableauserverclient/server/endpoint/oidc_endpoint.py new file mode 100644 index 000000000..d16008095 --- /dev/null +++ b/tableauserverclient/server/endpoint/oidc_endpoint.py @@ -0,0 +1,157 @@ +from typing import Protocol, Union, TYPE_CHECKING +from tableauserverclient.models.oidc_item import SiteOIDCConfiguration +from tableauserverclient.server.endpoint import Endpoint +from tableauserverclient.server.request_factory import RequestFactory +from tableauserverclient.server.endpoint.endpoint import api + +if TYPE_CHECKING: + from tableauserverclient.models.site_item import SiteAuthConfiguration + from tableauserverclient.server.server import Server + + +class IDPAttributes(Protocol): + idp_configuration_id: str + + +class IDPProperty(Protocol): + @property + def idp_configuration_id(self) -> str: ... + + +HasIdpConfigurationID = Union[str, IDPAttributes] + + +class OIDC(Endpoint): + def __init__(self, server: "Server") -> None: + self.parent_srv = server + + @property + def baseurl(self) -> str: + return f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/site-oidc-configuration" + + @api(version="3.24") + def get(self) -> list["SiteAuthConfiguration"]: + """ + Get all OpenID Connect (OIDC) configurations for the currently + authenticated Tableau Cloud site. To get all of the configuration + details, use the get_by_id method. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_identity_pools.htm#AuthnService_ListAuthConfigurations + + Returns + ------- + list[SiteAuthConfiguration] + """ + return self.parent_srv.sites.list_auth_configurations() + + @api(version="3.24") + def get_by_id(self, id: Union[str, HasIdpConfigurationID]) -> SiteOIDCConfiguration: + """ + Get details about a specific OpenID Connect (OIDC) configuration on the + current Tableau Cloud site. Only retrieves configurations for the + currently authenticated site. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#get_openid_connect_configuration + + Parameters + ---------- + id : Union[str, HasID] + The ID of the OIDC configuration to retrieve. Can be either the + ID string or an object with an id attribute. + + Returns + ------- + SiteOIDCConfiguration + The OIDC configuration for the specified site. + """ + target = getattr(id, "idp_configuration_id", id) + url = f"{self.baseurl}/{target}" + response = self.get_request(url) + return SiteOIDCConfiguration.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.22") + def create(self, config_item: SiteOIDCConfiguration) -> SiteOIDCConfiguration: + """ + Create the OpenID Connect (OIDC) configuration for the currently + authenticated Tableau Cloud site. The config_item must have the + following attributes set, others are optional: + + idp_configuration_name + client_id + client_secret + authorization_endpoint + token_endpoint + userinfo_endpoint + enabled + jwks_uri + + The secret in the returned config will be masked. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#create_openid_connect_configuration + + Parameters + ---------- + config : SiteOIDCConfiguration + The OIDC configuration to create. + + Returns + ------- + SiteOIDCConfiguration + The created OIDC configuration. + """ + url = self.baseurl + create_req = RequestFactory.OIDC.create_req(config_item) + response = self.put_request(url, create_req) + return SiteOIDCConfiguration.from_response(response.content, self.parent_srv.namespace) + + @api(version="3.24") + def delete_configuration(self, config: Union[str, HasIdpConfigurationID]) -> None: + """ + Delete the OpenID Connect (OIDC) configuration for the currently + authenticated Tableau Cloud site. The config parameter can be either + the ID of the configuration or the configuration object itself. + + **Important**: Before removing the OIDC configuration, make sure that + users who are set to authenticate with OIDC are set to use a different + authentication type. Users who are not set with a different + authentication type before removing the OIDC configuration will not be + able to sign in to Tableau Cloud. + + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#remove_openid_connect_configuration + + Parameters + ---------- + config : Union[str, HasID] + The OIDC configuration to delete. Can be either the ID of the + configuration or the configuration object itself. + """ + + target = getattr(config, "idp_configuration_id", config) + + url = f"{self.parent_srv.baseurl}/sites/{self.parent_srv.site_id}/disable-site-oidc-configuration?idpConfigurationId={target}" + _ = self.put_request(url) + return None + + @api(version="3.22") + def update(self, config: SiteOIDCConfiguration) -> SiteOIDCConfiguration: + """ + Update the Tableau Cloud site's OpenID Connect (OIDC) configuration. The + secret in the returned config will be masked. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_openid_connect.htm#update_openid_connect_configuration + + Parameters + ---------- + config : SiteOIDCConfiguration + The OIDC configuration to update. Must have the id attribute set. + + Returns + ------- + SiteOIDCConfiguration + The updated OIDC configuration. + """ + url = f"{self.baseurl}/{config.idp_configuration_id}" + update_req = RequestFactory.OIDC.update_req(config) + response = self.put_request(url, update_req) + return SiteOIDCConfiguration.from_response(response.content, self.parent_srv.namespace) diff --git a/tableauserverclient/server/endpoint/projects_endpoint.py b/tableauserverclient/server/endpoint/projects_endpoint.py index 68eb573cc..c7a834802 100644 --- a/tableauserverclient/server/endpoint/projects_endpoint.py +++ b/tableauserverclient/server/endpoint/projects_endpoint.py @@ -89,6 +89,18 @@ def delete(self, project_id: str) -> None: self.delete_request(url) logger.info(f"Deleted single project (ID: {project_id})") + @api(version="2.0") + def get_by_id(self, project_id: str) -> ProjectItem: + """ + Fetch a project by ID. This is a convenience method making up for a gap in the server API. + It uses the same endpoint as the update method, but without the ability to update the project. + """ + if not project_id: + error = "Project ID undefined." + raise ValueError(error) + project = ProjectItem(id=project_id) + return self.update(project, samples=False) + @api(version="2.0") def update(self, project_item: ProjectItem, samples: bool = False) -> ProjectItem: """ diff --git a/tableauserverclient/server/endpoint/schedules_endpoint.py b/tableauserverclient/server/endpoint/schedules_endpoint.py index 090d400b6..8984af407 100644 --- a/tableauserverclient/server/endpoint/schedules_endpoint.py +++ b/tableauserverclient/server/endpoint/schedules_endpoint.py @@ -1,13 +1,15 @@ +from collections.abc import Iterable import copy import logging import warnings from collections import namedtuple -from typing import TYPE_CHECKING, Callable, Optional, Union +from typing import TYPE_CHECKING, Any, Callable, Literal, Optional, Union, overload from .endpoint import Endpoint, api, parameter_added_in from .exceptions import MissingRequiredFieldError from tableauserverclient.server import RequestFactory from tableauserverclient.models import PaginationItem, ScheduleItem, TaskItem, ExtractItem +from tableauserverclient.models.schedule_item import parse_batch_schedule_state from tableauserverclient.helpers.logging import logger @@ -279,3 +281,48 @@ def get_extract_refresh_tasks( extract_items = ExtractItem.from_response(server_response.content, self.parent_srv.namespace) return extract_items, pagination_item + + @overload + def batch_update_state( + self, + schedules: Iterable[ScheduleItem | str], + state: Literal["active", "suspended"], + update_all: Literal[False] = False, + ) -> list[str]: ... + + @overload + def batch_update_state( + self, schedules: Any, state: Literal["active", "suspended"], update_all: Literal[True] + ) -> list[str]: ... + + @api(version="3.27") + def batch_update_state(self, schedules, state, update_all=False) -> list[str]: + """ + Batch update the status of one or more scheudles. If update_all is set, + all schedules on the Tableau Server are affected. + + Parameters + ---------- + schedules: Iterable[ScheudleItem | str] | Any + The schedules to be updated. If update_all=True, this is ignored. + + state: Literal["active", "suspended"] + The state of the schedules, whether active or suspended. + + update_all: bool + Whether or not to apply the status to all schedules. + + Returns + ------- + List[str] + The IDs of the affected schedules. + """ + params = {"state": state} + if update_all: + params["updateAll"] = "true" + payload = RequestFactory.Empty.empty_req() + else: + payload = RequestFactory.Schedule.batch_update_state(schedules) + + response = self.put_request(self.baseurl, payload, parameters={"params": params}) + return parse_batch_schedule_state(response, self.parent_srv.namespace) diff --git a/tableauserverclient/server/endpoint/users_endpoint.py b/tableauserverclient/server/endpoint/users_endpoint.py index 17af21a03..6deb76716 100644 --- a/tableauserverclient/server/endpoint/users_endpoint.py +++ b/tableauserverclient/server/endpoint/users_endpoint.py @@ -1,14 +1,19 @@ +from collections.abc import Iterable import copy +import csv +import io +import itertools import logging from typing import Optional +import warnings from tableauserverclient.server.query import QuerySet -from .endpoint import QuerysetEndpoint, api -from .exceptions import MissingRequiredFieldError, ServerResponseError +from tableauserverclient.server.endpoint.endpoint import QuerysetEndpoint, api +from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError, ServerResponseError from tableauserverclient.server import RequestFactory, RequestOptions -from tableauserverclient.models import UserItem, WorkbookItem, PaginationItem, GroupItem -from ..pager import Pager +from tableauserverclient.models import UserItem, WorkbookItem, PaginationItem, GroupItem, JobItem +from tableauserverclient.server.pager import Pager from tableauserverclient.helpers.logging import logger @@ -344,7 +349,34 @@ def add(self, user_item: UserItem) -> UserItem: # Add new users to site. This does not actually perform a bulk action, it's syntactic sugar @api(version="2.0") - def add_all(self, users: list[UserItem]): + def add_all(self, users: list[UserItem]) -> tuple[list[UserItem], list[UserItem]]: + """ + Syntactic sugar for calling users.add multiple times. This method has + been deprecated in favor of using the bulk_add which accomplishes the + same task in one API call. + + .. deprecated:: v0.41.0 + `add_all` will be removed as its functionality is replicated via + the `bulk_add` method. + + Parameters + ---------- + users: list[UserItem] + A list of UserItem objects to add to the site. Each UserItem object + will be passed to the `add` method individually. + + Returns + ------- + tuple[list[UserItem], list[UserItem]] + The first element of the tuple is a list of UserItem objects that + were successfully added to the site. The second element is a list + of UserItem objects that failed to be added to the site. + + Warnings + -------- + This method is deprecated. Use the `bulk_add` method instead. + """ + warnings.warn("This method is deprecated, use bulk_add method instead.", DeprecationWarning) created = [] failed = [] for user in users: @@ -357,8 +389,143 @@ def add_all(self, users: list[UserItem]): # helping the user by parsing a file they could have used to add users through the UI # line format: Username [required], password, display name, license, admin, publish + @api(version="3.15") + def bulk_add(self, users: Iterable[UserItem]) -> JobItem: + """ + When adding users in bulk, the server will return a job item that can be used to track the progress of the + operation. This method will return the job item that was created when the users were added. + + For each user, name is required, and other fields are optional. If connected to activte directory and + the user name is not unique across domains, then the domain attribute must be populated on + the UserItem. + + The user's display name is read from the fullname attribute. + + Email is optional, but if provided, it must be a valid email address. + + If auth_setting is not provided, and idp_configuration_id is None, then + default is ServerDefault. + + If site_role is not provided, the default is Unlicensed. + + Password is optional, and only used if the server is using local + authentication. If using any other authentication method, the password + should not be provided. + + Details about administrator level and publishing capability are + inferred from the site_role. + + If the user belongs to a different IDP configuration, the UserItem's + idp_configuration_id attribute must be set to the IDP configuration ID + that the user belongs to. + + Parameters + ---------- + users: Iterable[UserItem] + An iterable of UserItem objects to add to the site. See above for + what fields are required and optional. + + Returns + ------- + JobItem + The job that is started for adding the users in bulk. + + Examples + -------- + >>> import tableauserverclient as TSC + >>> server = TSC.Server('http://localhost') + >>> # Login to the server + + >>> # Create a list of UserItem objects to add to the site + >>> users = [ + >>> TSC.UserItem(name="user1", site_role="Unlicensed"), + >>> TSC.UserItem(name="user2", site_role="Explorer"), + >>> TSC.UserItem(name="user3", site_role="Creator"), + >>> ] + + >>> # Set the domain name for the users + >>> for user in users: + >>> user.domain_name = "example.com" + + >>> # Add the users to the site + >>> job = server.users.bulk_add(users) + + """ + url = f"{self.baseurl}/import" + # Allow for iterators to be passed into the function + csv_users, xml_users = itertools.tee(users, 2) + csv_content = create_users_csv(csv_users) + + xml_request, content_type = RequestFactory.User.import_from_csv_req(csv_content, xml_users) + server_response = self.post_request(url, xml_request, content_type) + return JobItem.from_response(server_response.content, self.parent_srv.namespace).pop() + + @api(version="3.15") + def bulk_remove(self, users: Iterable[UserItem]) -> None: + """ + Remove multiple users from the site. The users are identified by their + domain and name. The users are removed in bulk, so the server will not + return a job item to track the progress of the operation nor a response + for each user that was removed. + + Parameters + ---------- + users: Iterable[UserItem] + An iterable of UserItem objects to remove from the site. Each + UserItem object should have the domain and name attributes set. + + Returns + ------- + None + + Examples + -------- + >>> import tableauserverclient as TSC + >>> server = TSC.Server('http://localhost') + >>> # Login to the server + + >>> # Find the users to remove + >>> example_users = server.users.filter(domain_name="example.com") + >>> server.users.bulk_remove(example_users) + """ + url = f"{self.baseurl}/delete" + csv_content = remove_users_csv(users) + request, content_type = RequestFactory.User.delete_csv_req(csv_content) + server_response = self.post_request(url, request, content_type) + return None + @api(version="2.0") def create_from_file(self, filepath: str) -> tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]]: + """ + Syntactic sugar for calling users.add multiple times. This method has + been deprecated in favor of using the bulk_add which accomplishes the + same task in one API call. + + .. deprecated:: v0.41.0 + `add_all` will be removed as its functionality is replicated via + the `bulk_add` method. + + Parameters + ---------- + filepath: str + The path to the CSV file containing the users to add to the site. + The file is read in line by line and each line is passed to the + `add` method. + + Returns + ------- + tuple[list[UserItem], list[tuple[UserItem, ServerResponseError]]] + The first element of the tuple is a list of UserItem objects that + were successfully added to the site. The second element is a list + of tuples where the first element is the UserItem object that failed + to be added to the site and the second element is the ServerResponseError + that was raised when attempting to add the user. + + Warnings + -------- + This method is deprecated. Use the `bulk_add` method instead. + """ + warnings.warn("This method is deprecated, use bulk_add instead", DeprecationWarning) created = [] failed = [] if not filepath.find("csv"): @@ -569,3 +736,105 @@ def filter(self, *invalid, page_size: Optional[int] = None, **kwargs) -> QuerySe """ return super().filter(*invalid, page_size=page_size, **kwargs) + + +def create_users_csv(users: Iterable[UserItem]) -> bytes: + """ + Create a CSV byte string from an Iterable of UserItem objects. The CSV will + have the following columns, and no header row: + + - Username + - Password + - Display Name + - License + - Admin Level + - Publish capability + - Email + + Parameters + ---------- + users: Iterable[UserItem] + An iterable of UserItem objects to create the CSV from. + + Returns + ------- + bytes + A byte string containing the CSV data. + """ + with io.StringIO() as output: + writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL) + for user in users: + site_role = user.site_role or "Unlicensed" + if site_role == "ServerAdministrator": + license = "Creator" + admin_level = "System" + elif site_role.startswith("SiteAdministrator"): + admin_level = "Site" + license = site_role.replace("SiteAdministrator", "") + else: + license = site_role + admin_level = "" + + if any(x in site_role for x in ("Creator", "Admin", "Publish")): + publish = 1 + else: + publish = 0 + + writer.writerow( + ( + f"{user.domain_name}\\{user.name}" if user.domain_name else user.name, + getattr(user, "password", ""), + user.fullname, + license, + admin_level, + publish, + user.email, + ) + ) + output.seek(0) + result = output.read().encode("utf-8") + return result + + +def remove_users_csv(users: Iterable[UserItem]) -> bytes: + """ + Create a CSV byte string from an Iterable of UserItem objects. This function + only consumes the domain and name attributes of the UserItem objects. The + CSV will have space for the following columns, though only the first column + will be populated, and no header row: + + - Username + - Password + - Display Name + - License + - Admin Level + - Publish capability + - Email + + Parameters + ---------- + users: Iterable[UserItem] + An iterable of UserItem objects to create the CSV from. + + Returns + ------- + bytes + A byte string containing the CSV data. + """ + with io.StringIO() as output: + writer = csv.writer(output, quoting=csv.QUOTE_MINIMAL) + for user in users: + writer.writerow( + ( + f"{user.domain_name}\\{user.name}" if user.domain_name else user.name, + None, + None, + None, + None, + None, + None, + ) + ) + output.seek(0) + result = output.read().encode("utf-8") + return result diff --git a/tableauserverclient/server/endpoint/views_endpoint.py b/tableauserverclient/server/endpoint/views_endpoint.py index 9d1c8b00f..162c04105 100644 --- a/tableauserverclient/server/endpoint/views_endpoint.py +++ b/tableauserverclient/server/endpoint/views_endpoint.py @@ -371,6 +371,29 @@ def update(self, view_item: ViewItem) -> ViewItem: # Returning view item to stay consistent with datasource/view update functions return view_item + @api(version="3.27") + def delete(self, view: ViewItem | str) -> None: + """ + Deletes a view in a workbook. If you delete the only view in a workbook, + the workbook is deleted. Can be used to remove hidden views when + republishing or migrating to a different environment. + + REST API: https://help.tableau.com/current/api/rest_api/en-us/REST/rest_api_ref_workbooks_and_views.htm#delete_view + + Parameters + ---------- + view: ViewItem | str + The ViewItem or the luid for the view to be deleted. + + Returns + ------- + None + """ + id_ = getattr(view, "id", view) + self.delete_request(f"{self.baseurl}/{id_}") + logger.info(f"View({id_}) deleted.") + return None + @api(version="1.0") def add_tags(self, item: Union[ViewItem, str], tags: Union[Iterable[str], str]) -> set[str]: """ diff --git a/tableauserverclient/server/endpoint/workbooks_endpoint.py b/tableauserverclient/server/endpoint/workbooks_endpoint.py index bf4088b9f..5f9695829 100644 --- a/tableauserverclient/server/endpoint/workbooks_endpoint.py +++ b/tableauserverclient/server/endpoint/workbooks_endpoint.py @@ -30,9 +30,12 @@ from tableauserverclient.server import RequestFactory from typing import ( + Literal, Optional, TYPE_CHECKING, + TypeVar, Union, + overload, ) from collections.abc import Iterable, Sequence @@ -325,16 +328,92 @@ def update_connection(self, workbook_item: WorkbookItem, connection_item: Connec logger.info(f"Updated workbook item (ID: {workbook_item.id} & connection item {connection_item.id})") return connection + # Update workbook_connections + @api(version="3.26") + def update_connections( + self, + workbook_item: WorkbookItem, + connection_luids: Iterable[str], + authentication_type: str, + username: Optional[str] = None, + password: Optional[str] = None, + embed_password: Optional[bool] = None, + ) -> list[ConnectionItem]: + """ + Bulk updates one or more workbook connections by LUID, including authenticationType, username, password, and embedPassword. + + Parameters + ---------- + workbook_item : WorkbookItem + The workbook item containing the connections. + + connection_luids : Iterable of str + The connection LUIDs to update. + + authentication_type : str + The authentication type to use (e.g., 'AD Service Principal'). + + username : str, optional + The username to set (e.g., client ID for keypair auth). + + password : str, optional + The password or secret to set. + + embed_password : bool, optional + Whether to embed the password. + + Returns + ------- + Iterable of str + The connection LUIDs that were updated. + """ + + url = f"{self.baseurl}/{workbook_item.id}/connections" + + request_body = RequestFactory.Workbook.update_connections_req( + connection_luids, + authentication_type, + username=username, + password=password, + embed_password=embed_password, + ) + + # Send request + server_response = self.put_request(url, request_body) + connection_items = ConnectionItem.from_response(server_response.content, self.parent_srv.namespace) + updated_ids: list[str] = [conn.id for conn in connection_items] + + logger.info(f"Updated connections for workbook {workbook_item.id}: {', '.join(updated_ids)}") + return connection_items + + T = TypeVar("T", bound=FileObjectW) + + @overload + def download( + self, + workbook_id: str, + filepath: T, + include_extract: bool = True, + ) -> T: ... + + @overload + def download( + self, + workbook_id: str, + filepath: Optional[FilePath] = None, + include_extract: bool = True, + ) -> str: ... + # Download workbook contents with option of passing in filepath @api(version="2.0") @parameter_added_in(no_extract="2.5") @parameter_added_in(include_extract="2.5") def download( self, - workbook_id: str, - filepath: Optional[PathOrFileW] = None, - include_extract: bool = True, - ) -> PathOrFileW: + workbook_id, + filepath=None, + include_extract=True, + ): """ Downloads a workbook to the specified directory (optional). @@ -683,6 +762,30 @@ def delete_permission(self, item: WorkbookItem, capability_item: PermissionsRule """ return self._permissions.delete(item, capability_item) + @overload + def publish( + self, + workbook_item: WorkbookItem, + file: PathOrFileR, + mode: str, + connections: Optional[Sequence[ConnectionItem]], + as_job: Literal[False], + skip_connection_check: bool, + parameters=None, + ) -> WorkbookItem: ... + + @overload + def publish( + self, + workbook_item: WorkbookItem, + file: PathOrFileR, + mode: str, + connections: Optional[Sequence[ConnectionItem]], + as_job: Literal[True], + skip_connection_check: bool, + parameters=None, + ) -> JobItem: ... + @api(version="2.0") @parameter_added_in(as_job="3.0") @parameter_added_in(connections="2.8") @@ -919,15 +1022,27 @@ def _get_workbook_revisions( revisions = RevisionItem.from_response(server_response.content, self.parent_srv.namespace, workbook_item) return revisions + T = TypeVar("T", bound=FileObjectW) + + @overload + def download_revision( + self, workbook_id: str, revision_number: Optional[str], filepath: T, include_extract: bool + ) -> T: ... + + @overload + def download_revision( + self, workbook_id: str, revision_number: Optional[str], filepath: Optional[FilePath], include_extract: bool + ) -> str: ... + # Download 1 workbook revision by revision number @api(version="2.3") def download_revision( self, - workbook_id: str, - revision_number: Optional[str], - filepath: Optional[PathOrFileW] = None, - include_extract: bool = True, - ) -> PathOrFileW: + workbook_id, + revision_number, + filepath, + include_extract=True, + ): """ Downloads a workbook revision to the specified directory (optional). diff --git a/tableauserverclient/server/request_factory.py b/tableauserverclient/server/request_factory.py index c898004f7..57deb6e26 100644 --- a/tableauserverclient/server/request_factory.py +++ b/tableauserverclient/server/request_factory.py @@ -184,6 +184,9 @@ def _generate_xml(self, datasource_item: DatasourceItem, connection_credentials= project_element = ET.SubElement(datasource_element, "project") project_element.attrib["id"] = datasource_item.project_id + if datasource_item.description is not None: + datasource_element.attrib["description"] = datasource_item.description + if connection_credentials is not None and connections is not None: raise RuntimeError("You cannot set both `connections` and `connection_credentials`") @@ -196,7 +199,7 @@ def _generate_xml(self, datasource_item: DatasourceItem, connection_credentials= _add_connections_element(connections_element, connection) return ET.tostring(xml_request) - def update_req(self, datasource_item): + def update_req(self, datasource_item: DatasourceItem) -> bytes: xml_request = ET.Element("tsRequest") datasource_element = ET.SubElement(xml_request, "datasource") if datasource_item.name: @@ -219,6 +222,8 @@ def update_req(self, datasource_item): datasource_element.attrib["certificationNote"] = str(datasource_item.certification_note) if datasource_item.encrypt_extracts is not None: datasource_element.attrib["encryptExtracts"] = str(datasource_item.encrypt_extracts).lower() + if datasource_item.description is not None: + datasource_element.attrib["description"] = datasource_item.description return ET.tostring(xml_request) @@ -244,6 +249,32 @@ def publish_req_chunked(self, datasource_item, connection_credentials=None, conn parts = {"request_payload": ("", xml_request, "text/xml")} return _add_multipart(parts) + @_tsrequest_wrapped + def update_connections_req( + self, + element: ET.Element, + connection_luids: Iterable[str], + authentication_type: str, + username: Optional[str] = None, + password: Optional[str] = None, + embed_password: Optional[bool] = None, + ): + conn_luids_elem = ET.SubElement(element, "connectionLUIDs") + for luid in connection_luids: + ET.SubElement(conn_luids_elem, "connectionLUID").text = luid + + connection_elem = ET.SubElement(element, "connection") + connection_elem.set("authenticationType", authentication_type) + + if username is not None: + connection_elem.set("userName", username) + + if password is not None: + connection_elem.set("password", password) + + if embed_password is not None: + connection_elem.set("embedPassword", str(embed_password).lower()) + class DQWRequest: def add_req(self, dqw_item): @@ -486,7 +517,10 @@ def add_req(self, rules: Iterable[PermissionsRule]) -> bytes: for rule in rules: grantee_capabilities_element = ET.SubElement(permissions_element, "granteeCapabilities") grantee_element = ET.SubElement(grantee_capabilities_element, rule.grantee.tag_name) - grantee_element.attrib["id"] = rule.grantee.id + if rule.grantee.id is not None: + grantee_element.attrib["id"] = rule.grantee.id + else: + raise ValueError("Grantee must have an ID") capabilities_element = ET.SubElement(grantee_capabilities_element, "capabilities") self._add_all_capabilities(capabilities_element, rule.capabilities) @@ -609,6 +643,16 @@ def add_datasource_req(self, id_: Optional[str], task_type: str = TaskItem.Type. def add_flow_req(self, id_: Optional[str], task_type: str = TaskItem.Type.RunFlow) -> bytes: return self._add_to_req(id_, "flow", task_type) + @_tsrequest_wrapped + def batch_update_state(self, xml: ET.Element, schedules: Iterable[ScheduleItem | str]) -> None: + luids = ET.SubElement(xml, "scheduleLuids") + for schedule in schedules: + luid = getattr(schedule, "id", schedule) + if not isinstance(luid, str): + continue + luid_tag = ET.SubElement(luids, "scheduleLuid") + luid_tag.text = luid + class SiteRequest: def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = None): @@ -715,6 +759,8 @@ def update_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = Non site_element.attrib["autoSuspendRefreshInactivityWindow"] = str( site_item.auto_suspend_refresh_inactivity_window ) + if site_item.attribute_capture_enabled is not None: + site_element.attrib["attributeCaptureEnabled"] = str(site_item.attribute_capture_enabled).lower() return ET.tostring(xml_request) @@ -819,6 +865,8 @@ def create_req(self, site_item: "SiteItem", parent_srv: Optional["Server"] = Non site_element.attrib["autoSuspendRefreshInactivityWindow"] = str( site_item.auto_suspend_refresh_inactivity_window ) + if site_item.attribute_capture_enabled is not None: + site_element.attrib["attributeCaptureEnabled"] = str(site_item.attribute_capture_enabled).lower() return ET.tostring(xml_request) @@ -894,6 +942,7 @@ def batch_create(self, element: ET.Element, tags: set[str], content: content_typ if item.id is None: raise ValueError(f"Item {item} must have an ID to be tagged.") content_element.attrib["id"] = item.id + content_element.attrib["contentType"] = item.__class__.__name__.replace("Item", "") return ET.tostring(element) @@ -936,6 +985,32 @@ def add_req(self, user_item: UserItem) -> bytes: user_element.attrib["idpConfigurationId"] = user_item.idp_configuration_id return ET.tostring(xml_request) + def import_from_csv_req(self, csv_content: bytes, users: Iterable[UserItem]): + xml_request = ET.Element("tsRequest") + for user in users: + if user.name is None: + raise ValueError("User name must be populated.") + user_element = ET.SubElement(xml_request, "user") + user_element.attrib["name"] = user.name + if user.auth_setting is not None and user.idp_configuration_id is not None: + raise ValueError("User cannot have both authSetting and idpConfigurationId.") + elif user.idp_configuration_id is not None: + user_element.attrib["idpConfigurationId"] = user.idp_configuration_id + else: + user_element.attrib["authSetting"] = user.auth_setting or "ServerDefault" + + parts = { + "tableau_user_import": ("tsc_users_file.csv", csv_content, "file"), + "request_payload": ("", ET.tostring(xml_request), "text/xml"), + } + return _add_multipart(parts) + + def delete_csv_req(self, csv_content: bytes): + parts = { + "tableau_user_delete": ("tsc_users_file.csv", csv_content, "file"), + } + return _add_multipart(parts) + class WorkbookRequest: def _generate_xml( @@ -1092,6 +1167,32 @@ def embedded_extract_req( if (id_ := datasource_item.id) is not None: datasource_element.attrib["id"] = id_ + @_tsrequest_wrapped + def update_connections_req( + self, + element: ET.Element, + connection_luids: Iterable[str], + authentication_type: str, + username: Optional[str] = None, + password: Optional[str] = None, + embed_password: Optional[bool] = None, + ): + conn_luids_elem = ET.SubElement(element, "connectionLUIDs") + for luid in connection_luids: + ET.SubElement(conn_luids_elem, "connectionLUID").text = luid + + connection_elem = ET.SubElement(element, "connection") + connection_elem.set("authenticationType", authentication_type) + + if username is not None: + connection_elem.set("userName", username) + + if password is not None: + connection_elem.set("password", password) + + if embed_password is not None: + connection_elem.set("embedPassword", str(embed_password).lower()) + class Connection: @_tsrequest_wrapped @@ -1110,6 +1211,8 @@ def update_req(self, xml_request: ET.Element, connection_item: "ConnectionItem") connection_element.attrib["userName"] = connection_item.username if connection_item.password is not None: connection_element.attrib["password"] = connection_item.password + if connection_item.auth_type is not None: + connection_element.attrib["authenticationType"] = connection_item.auth_type if connection_item.embed_password is not None: connection_element.attrib["embedPassword"] = str(connection_item.embed_password).lower() if connection_item.query_tagging is not None: @@ -1446,6 +1549,180 @@ def publish(self, xml_request: ET.Element, virtual_connection: VirtualConnection return ET.tostring(xml_request) +class OIDCRequest: + @_tsrequest_wrapped + def create_req(self, xml_request: ET.Element, oidc_item: SiteOIDCConfiguration) -> bytes: + oidc_element = ET.SubElement(xml_request, "siteOIDCConfiguration") + + # Check required attributes first + + if oidc_item.idp_configuration_name is None: + raise ValueError(f"OIDC Item missing idp_configuration_name: {oidc_item}") + if oidc_item.client_id is None: + raise ValueError(f"OIDC Item missing client_id: {oidc_item}") + if oidc_item.client_secret is None: + raise ValueError(f"OIDC Item missing client_secret: {oidc_item}") + if oidc_item.authorization_endpoint is None: + raise ValueError(f"OIDC Item missing authorization_endpoint: {oidc_item}") + if oidc_item.token_endpoint is None: + raise ValueError(f"OIDC Item missing token_endpoint: {oidc_item}") + if oidc_item.userinfo_endpoint is None: + raise ValueError(f"OIDC Item missing userinfo_endpoint: {oidc_item}") + if not isinstance(oidc_item.enabled, bool): + raise ValueError(f"OIDC Item missing enabled: {oidc_item}") + if oidc_item.jwks_uri is None: + raise ValueError(f"OIDC Item missing jwks_uri: {oidc_item}") + + oidc_element.attrib["name"] = oidc_item.idp_configuration_name + oidc_element.attrib["clientId"] = oidc_item.client_id + oidc_element.attrib["clientSecret"] = oidc_item.client_secret + oidc_element.attrib["authorizationEndpoint"] = oidc_item.authorization_endpoint + oidc_element.attrib["tokenEndpoint"] = oidc_item.token_endpoint + oidc_element.attrib["userInfoEndpoint"] = oidc_item.userinfo_endpoint + oidc_element.attrib["enabled"] = str(oidc_item.enabled).lower() + oidc_element.attrib["jwksUri"] = oidc_item.jwks_uri + + if oidc_item.allow_embedded_authentication is not None: + oidc_element.attrib["allowEmbeddedAuthentication"] = str(oidc_item.allow_embedded_authentication).lower() + if oidc_item.custom_scope is not None: + oidc_element.attrib["customScope"] = oidc_item.custom_scope + if oidc_item.prompt is not None: + oidc_element.attrib["prompt"] = oidc_item.prompt + if oidc_item.client_authentication is not None: + oidc_element.attrib["clientAuthentication"] = oidc_item.client_authentication + if oidc_item.essential_acr_values is not None: + oidc_element.attrib["essentialAcrValues"] = oidc_item.essential_acr_values + if oidc_item.voluntary_acr_values is not None: + oidc_element.attrib["voluntaryAcrValues"] = oidc_item.voluntary_acr_values + if oidc_item.email_mapping is not None: + oidc_element.attrib["emailMapping"] = oidc_item.email_mapping + if oidc_item.first_name_mapping is not None: + oidc_element.attrib["firstNameMapping"] = oidc_item.first_name_mapping + if oidc_item.last_name_mapping is not None: + oidc_element.attrib["lastNameMapping"] = oidc_item.last_name_mapping + if oidc_item.full_name_mapping is not None: + oidc_element.attrib["fullNameMapping"] = oidc_item.full_name_mapping + if oidc_item.use_full_name is not None: + oidc_element.attrib["useFullName"] = str(oidc_item.use_full_name).lower() + + return ET.tostring(xml_request) + + @_tsrequest_wrapped + def update_req(self, xml_request: ET.Element, oidc_item: SiteOIDCConfiguration) -> bytes: + oidc_element = ET.SubElement(xml_request, "siteOIDCConfiguration") + + # Check required attributes first + + if oidc_item.idp_configuration_name is None: + raise ValueError(f"OIDC Item missing idp_configuration_name: {oidc_item}") + if oidc_item.client_id is None: + raise ValueError(f"OIDC Item missing client_id: {oidc_item}") + if oidc_item.client_secret is None: + raise ValueError(f"OIDC Item missing client_secret: {oidc_item}") + if oidc_item.authorization_endpoint is None: + raise ValueError(f"OIDC Item missing authorization_endpoint: {oidc_item}") + if oidc_item.token_endpoint is None: + raise ValueError(f"OIDC Item missing token_endpoint: {oidc_item}") + if oidc_item.userinfo_endpoint is None: + raise ValueError(f"OIDC Item missing userinfo_endpoint: {oidc_item}") + if not isinstance(oidc_item.enabled, bool): + raise ValueError(f"OIDC Item missing enabled: {oidc_item}") + if oidc_item.jwks_uri is None: + raise ValueError(f"OIDC Item missing jwks_uri: {oidc_item}") + + oidc_element.attrib["name"] = oidc_item.idp_configuration_name + oidc_element.attrib["clientId"] = oidc_item.client_id + oidc_element.attrib["clientSecret"] = oidc_item.client_secret + oidc_element.attrib["authorizationEndpoint"] = oidc_item.authorization_endpoint + oidc_element.attrib["tokenEndpoint"] = oidc_item.token_endpoint + oidc_element.attrib["userInfoEndpoint"] = oidc_item.userinfo_endpoint + oidc_element.attrib["enabled"] = str(oidc_item.enabled).lower() + oidc_element.attrib["jwksUri"] = oidc_item.jwks_uri + + if oidc_item.allow_embedded_authentication is not None: + oidc_element.attrib["allowEmbeddedAuthentication"] = str(oidc_item.allow_embedded_authentication).lower() + if oidc_item.custom_scope is not None: + oidc_element.attrib["customScope"] = oidc_item.custom_scope + if oidc_item.prompt is not None: + oidc_element.attrib["prompt"] = oidc_item.prompt + if oidc_item.client_authentication is not None: + oidc_element.attrib["clientAuthentication"] = oidc_item.client_authentication + if oidc_item.essential_acr_values is not None: + oidc_element.attrib["essentialAcrValues"] = oidc_item.essential_acr_values + if oidc_item.voluntary_acr_values is not None: + oidc_element.attrib["voluntaryAcrValues"] = oidc_item.voluntary_acr_values + if oidc_item.email_mapping is not None: + oidc_element.attrib["emailMapping"] = oidc_item.email_mapping + if oidc_item.first_name_mapping is not None: + oidc_element.attrib["firstNameMapping"] = oidc_item.first_name_mapping + if oidc_item.last_name_mapping is not None: + oidc_element.attrib["lastNameMapping"] = oidc_item.last_name_mapping + if oidc_item.full_name_mapping is not None: + oidc_element.attrib["fullNameMapping"] = oidc_item.full_name_mapping + if oidc_item.use_full_name is not None: + oidc_element.attrib["useFullName"] = str(oidc_item.use_full_name).lower() + + return ET.tostring(xml_request) + + +class ExtensionsRequest: + @_tsrequest_wrapped + def update_server_extensions(self, xml_request: ET.Element, extensions_server: "ExtensionsServer") -> None: + extensions_element = ET.SubElement(xml_request, "extensionsServerSettings") + if not isinstance(extensions_server.enabled, bool): + raise ValueError(f"Extensions Server missing enabled: {extensions_server}") + enabled_element = ET.SubElement(extensions_element, "extensionsGloballyEnabled") + enabled_element.text = str(extensions_server.enabled).lower() + + if extensions_server.block_list is None: + return + for blocked in extensions_server.block_list: + blocked_element = ET.SubElement(extensions_element, "blockList") + blocked_element.text = blocked + return + + @_tsrequest_wrapped + def update_site_extensions(self, xml_request: ET.Element, extensions_site_settings: ExtensionsSiteSettings) -> None: + ext_element = ET.SubElement(xml_request, "extensionsSiteSettings") + if not isinstance(extensions_site_settings.enabled, bool): + raise ValueError(f"Extensions Site Settings missing enabled: {extensions_site_settings}") + enabled_element = ET.SubElement(ext_element, "extensionsEnabled") + enabled_element.text = str(extensions_site_settings.enabled).lower() + if not isinstance(extensions_site_settings.use_default_setting, bool): + raise ValueError( + f"Extensions Site Settings missing use_default_setting: {extensions_site_settings.use_default_setting}" + ) + default_element = ET.SubElement(ext_element, "useDefaultSetting") + default_element.text = str(extensions_site_settings.use_default_setting).lower() + if extensions_site_settings.allow_trusted is not None: + allow_trusted_element = ET.SubElement(ext_element, "allowTrusted") + allow_trusted_element.text = str(extensions_site_settings.allow_trusted).lower() + if extensions_site_settings.include_sandboxed is not None: + include_sandboxed_element = ET.SubElement(ext_element, "includeSandboxed") + include_sandboxed_element.text = str(extensions_site_settings.include_sandboxed).lower() + if extensions_site_settings.include_tableau_built is not None: + include_tableau_built_element = ET.SubElement(ext_element, "includeTableauBuilt") + include_tableau_built_element.text = str(extensions_site_settings.include_tableau_built).lower() + if extensions_site_settings.include_partner_built is not None: + include_partner_built_element = ET.SubElement(ext_element, "includePartnerBuilt") + include_partner_built_element.text = str(extensions_site_settings.include_partner_built).lower() + + if extensions_site_settings.safe_list is None: + return + + safe_element = ET.SubElement(ext_element, "safeList") + for safe in extensions_site_settings.safe_list: + if safe.url is not None: + url_element = ET.SubElement(safe_element, "url") + url_element.text = safe.url + if safe.full_data_allowed is not None: + full_data_element = ET.SubElement(safe_element, "fullDataAllowed") + full_data_element.text = str(safe.full_data_allowed).lower() + if safe.prompt_needed is not None: + prompt_element = ET.SubElement(safe_element, "promptNeeded") + prompt_element.text = str(safe.prompt_needed).lower() + + class RequestFactory: Auth = AuthRequest() Connection = Connection() @@ -1456,6 +1733,7 @@ class RequestFactory: Database = DatabaseRequest() DQW = DQWRequest() Empty = EmptyRequest() + Extensions = ExtensionsRequest() Favorite = FavoriteRequest() Fileupload = FileuploadRequest() Flow = FlowRequest() @@ -1463,6 +1741,7 @@ class RequestFactory: Group = GroupRequest() GroupSet = GroupSetRequest() Metric = MetricRequest() + OIDC = OIDCRequest() Permission = PermissionRequest() Project = ProjectRequest() Schedule = ScheduleRequest() diff --git a/tableauserverclient/server/request_options.py b/tableauserverclient/server/request_options.py index 4a104255f..70c85d140 100644 --- a/tableauserverclient/server/request_options.py +++ b/tableauserverclient/server/request_options.py @@ -96,7 +96,10 @@ def get_query_params(self) -> dict: if self.pagesize: params["pageSize"] = self.pagesize if self.fields: - params["fields"] = ",".join(self.fields) + if "_all_" in self.fields: + params["fields"] = "_all_" + else: + params["fields"] = ",".join(sorted(self.fields)) return params def page_size(self, page_size): @@ -384,7 +387,14 @@ def parameter(self, name: str, value: str) -> Self: Self The current object """ - self.view_parameters.append((name, value)) + prefix = "vf_Parameters." + if name.startswith(prefix): + proper_name = name + elif name.startswith("Parameters."): + proper_name = f"vf_{name}" + else: + proper_name = f"{prefix}{name}" + self.view_parameters.append((proper_name, value)) return self def _append_view_filters(self, params) -> None: diff --git a/tableauserverclient/server/server.py b/tableauserverclient/server/server.py index d5d163db3..b497e9086 100644 --- a/tableauserverclient/server/server.py +++ b/tableauserverclient/server/server.py @@ -38,6 +38,8 @@ GroupSets, Tags, VirtualConnections, + OIDC, + Extensions, ) from tableauserverclient.server.exceptions import ( ServerInfoEndpointNotFoundError, @@ -183,6 +185,8 @@ def __init__(self, server_address, use_server_version=False, http_options=None, self.group_sets = GroupSets(self) self.tags = Tags(self) self.virtual_connections = VirtualConnections(self) + self.oidc = OIDC(self) + self.extensions = Extensions(self) self._session = self._session_factory() self._http_options = dict() # must set this before making a server call diff --git a/test/_utils.py b/test/_utils.py index b4ee93bc3..a23f37b57 100644 --- a/test/_utils.py +++ b/test/_utils.py @@ -1,5 +1,6 @@ import os.path import unittest +from typing import Optional from xml.etree import ElementTree as ET from contextlib import contextmanager @@ -32,6 +33,19 @@ def server_response_error_factory(code: str, summary: str, detail: str) -> str: return ET.tostring(root, encoding="utf-8").decode("utf-8") +def server_response_factory(tag: str, **attributes: str) -> bytes: + ns = "http://tableau.com/api" + ET.register_namespace("", ns) + root = ET.Element( + f"{{{ns}}}tsResponse", + ) + if attributes is None: + attributes = {} + + elem = ET.SubElement(root, f"{{{ns}}}{tag}", attrib=attributes) + return ET.tostring(root, encoding="utf-8") + + @contextmanager def mocked_time(): mock_time = 0 diff --git a/test/assets/datasource_connections_update.xml b/test/assets/datasource_connections_update.xml new file mode 100644 index 000000000..d726aad25 --- /dev/null +++ b/test/assets/datasource_connections_update.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/test/assets/datasource_get_no_owner.xml b/test/assets/datasource_get_no_owner.xml new file mode 100644 index 000000000..a0149d5ee --- /dev/null +++ b/test/assets/datasource_get_no_owner.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/test/assets/extensions_server_settings_false.xml b/test/assets/extensions_server_settings_false.xml new file mode 100644 index 000000000..16fd3e85d --- /dev/null +++ b/test/assets/extensions_server_settings_false.xml @@ -0,0 +1,6 @@ + + + + false + + diff --git a/test/assets/extensions_server_settings_true.xml b/test/assets/extensions_server_settings_true.xml new file mode 100644 index 000000000..c562d4719 --- /dev/null +++ b/test/assets/extensions_server_settings_true.xml @@ -0,0 +1,8 @@ + + + + true + https://test.com + https://example.com + + diff --git a/test/assets/extensions_site_settings.xml b/test/assets/extensions_site_settings.xml new file mode 100644 index 000000000..e5f963ca9 --- /dev/null +++ b/test/assets/extensions_site_settings.xml @@ -0,0 +1,16 @@ + + + + true + false + true + false + false + false + + http://localhost:9123/Dynamic.html + true + true + + + diff --git a/test/assets/favorites_add_view.xml b/test/assets/favorites_add_view.xml index f6fc15c9a..0f5c6d166 100644 --- a/test/assets/favorites_add_view.xml +++ b/test/assets/favorites_add_view.xml @@ -11,4 +11,6 @@ - \ No newline at end of file + + + diff --git a/test/assets/favorites_get.xml b/test/assets/favorites_get.xml index 3d2e2ee6a..8fd780b1d 100644 --- a/test/assets/favorites_get.xml +++ b/test/assets/favorites_get.xml @@ -43,5 +43,17 @@ + + + + + - \ No newline at end of file + diff --git a/test/assets/oidc_create.xml b/test/assets/oidc_create.xml new file mode 100644 index 000000000..cbe632f3b --- /dev/null +++ b/test/assets/oidc_create.xml @@ -0,0 +1,30 @@ + + + + diff --git a/test/assets/oidc_get.xml b/test/assets/oidc_get.xml new file mode 100644 index 000000000..cbe632f3b --- /dev/null +++ b/test/assets/oidc_get.xml @@ -0,0 +1,30 @@ + + + + diff --git a/test/assets/oidc_update.xml b/test/assets/oidc_update.xml new file mode 100644 index 000000000..cbe632f3b --- /dev/null +++ b/test/assets/oidc_update.xml @@ -0,0 +1,30 @@ + + + + diff --git a/test/assets/schedule_batch_update_state.xml b/test/assets/schedule_batch_update_state.xml new file mode 100644 index 000000000..7749a3eeb --- /dev/null +++ b/test/assets/schedule_batch_update_state.xml @@ -0,0 +1,7 @@ + + + 593d2ebf-0d18-4deb-9d21-b113a4902583 + cecbb71e-def0-4030-8068-5ae50f51db1c + f39a6e7d-405e-4c07-8c18-95845f9da80e + + diff --git a/test/assets/schedule_get_customized_monthly_id.xml b/test/assets/schedule_get_customized_monthly_id.xml new file mode 100644 index 000000000..cc2bf5606 --- /dev/null +++ b/test/assets/schedule_get_customized_monthly_id.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/test/assets/users_bulk_add_job.xml b/test/assets/users_bulk_add_job.xml new file mode 100644 index 000000000..7301ac7d3 --- /dev/null +++ b/test/assets/users_bulk_add_job.xml @@ -0,0 +1,4 @@ + + + + diff --git a/test/assets/virtual_connection_populate_connections.xml b/test/assets/virtual_connection_populate_connections.xml index 77d899520..0835e478f 100644 --- a/test/assets/virtual_connection_populate_connections.xml +++ b/test/assets/virtual_connection_populate_connections.xml @@ -1,6 +1,6 @@ - + diff --git a/test/assets/virtual_connection_populate_connections2.xml b/test/assets/virtual_connection_populate_connections2.xml index f0ad2646d..78ff90f65 100644 --- a/test/assets/virtual_connection_populate_connections2.xml +++ b/test/assets/virtual_connection_populate_connections2.xml @@ -1,6 +1,6 @@ - + diff --git a/test/assets/workbook_update_connections.xml b/test/assets/workbook_update_connections.xml new file mode 100644 index 000000000..ce6ca227f --- /dev/null +++ b/test/assets/workbook_update_connections.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/test/http/__init__.py b/test/http/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/http/test_http_requests.py b/test/http/test_http_requests.py index ce845502d..a595e7d36 100644 --- a/test/http/test_http_requests.py +++ b/test/http/test_http_requests.py @@ -1,5 +1,5 @@ +import pytest import tableauserverclient as TSC -import unittest import requests import requests_mock @@ -27,91 +27,103 @@ def __init__(self, status_code): return MockResponse(200) -class ServerTests(unittest.TestCase): - def test_init_server_model_empty_throws(self): - with self.assertRaises(TypeError): - server = TSC.Server() - - def test_init_server_model_no_protocol_defaults_htt(self): - server = TSC.Server("fake-url") - - def test_init_server_model_valid_server_name_works(self): - server = TSC.Server("http://fake-url") - - def test_init_server_model_valid_https_server_name_works(self): - # by default, it will just set the version to 2.3 - server = TSC.Server("https://fake-url") - - def test_init_server_model_bad_server_name_not_version_check(self): - server = TSC.Server("fake-url", use_server_version=False) - - @mock.patch("requests.sessions.Session.get", side_effect=mocked_requests_get) - def test_init_server_model_bad_server_name_do_version_check(self, mock_get): - server = TSC.Server("fake-url", use_server_version=True) - - def test_init_server_model_bad_server_name_not_version_check_random_options(self): - # with self.assertRaises(MissingSchema): - server = TSC.Server("fake-url", use_server_version=False, http_options={"foo": 1}) - - def test_init_server_model_bad_server_name_not_version_check_real_options(self): - # with self.assertRaises(ValueError): - server = TSC.Server("fake-url", use_server_version=False, http_options={"verify": False}) - - def test_http_options_skip_ssl_works(self): - http_options = {"verify": False} - server = TSC.Server("http://fake-url") - server.add_http_options(http_options) - - def test_http_options_multiple_options_works(self): - http_options = {"verify": False, "birdname": "Parrot"} - server = TSC.Server("http://fake-url") - server.add_http_options(http_options) - - # ValueError: dictionary update sequence element #0 has length 1; 2 is required - def test_http_options_multiple_dicts_fails(self): - http_options_1 = {"verify": False} - http_options_2 = {"birdname": "Parrot"} - server = TSC.Server("http://fake-url") - with self.assertRaises(ValueError): - server.add_http_options([http_options_1, http_options_2]) - - # TypeError: cannot convert dictionary update sequence element #0 to a sequence - def test_http_options_not_sequence_fails(self): - server = TSC.Server("http://fake-url") - with self.assertRaises(ValueError): - server.add_http_options({1, 2, 3}) - - def test_validate_connection_http(self): - url = "http://cookies.com" - server = TSC.Server(url) - server.validate_connection_settings() - self.assertEqual(url, server.server_address) - - def test_validate_connection_https(self): - url = "https://cookies.com" - server = TSC.Server(url) - server.validate_connection_settings() - self.assertEqual(url, server.server_address) - - def test_validate_connection_no_protocol(self): - url = "cookies.com" - fixed_url = "http://cookies.com" - server = TSC.Server(url) - server.validate_connection_settings() - self.assertEqual(fixed_url, server.server_address) - - -class SessionTests(unittest.TestCase): - test_header = {"x-test": "true"} - - @staticmethod - def session_factory(): - session = requests.session() - session.headers.update(SessionTests.test_header) - return session - - def test_session_factory_adds_headers(self): - test_request_bin = "http://capture-this-with-mock.com" - with requests_mock.mock() as m: - m.get(url="http://capture-this-with-mock.com/api/2.4/serverInfo", request_headers=SessionTests.test_header) - server = TSC.Server(test_request_bin, use_server_version=True, session_factory=SessionTests.session_factory) +def test_init_server_model_empty_throws(): + with pytest.raises(TypeError): + server = TSC.Server() + + +def test_init_server_model_no_protocol_defaults_htt(): + server = TSC.Server("fake-url") + + +def test_init_server_model_valid_server_name_works(): + server = TSC.Server("http://fake-url") + + +def test_init_server_model_valid_https_server_name_works(): + # by default, it will just set the version to 2.3 + server = TSC.Server("https://fake-url") + + +def test_init_server_model_bad_server_name_not_version_check(): + server = TSC.Server("fake-url", use_server_version=False) + + +@mock.patch("requests.sessions.Session.get", side_effect=mocked_requests_get) +def test_init_server_model_bad_server_name_do_version_check(mock_get): + server = TSC.Server("fake-url", use_server_version=True) + + +def test_init_server_model_bad_server_name_not_version_check_random_options(): + server = TSC.Server("fake-url", use_server_version=False, http_options={"foo": 1}) + + +def test_init_server_model_bad_server_name_not_version_check_real_options(): + server = TSC.Server("fake-url", use_server_version=False, http_options={"verify": False}) + + +def test_http_options_skip_ssl_works(): + http_options = {"verify": False} + server = TSC.Server("http://fake-url") + server.add_http_options(http_options) + + +def test_http_options_multiple_options_works(): + http_options = {"verify": False, "birdname": "Parrot"} + server = TSC.Server("http://fake-url") + server.add_http_options(http_options) + + +# ValueError: dictionary update sequence element #0 has length 1; 2 is required +def test_http_options_multiple_dicts_fails(): + http_options_1 = {"verify": False} + http_options_2 = {"birdname": "Parrot"} + server = TSC.Server("http://fake-url") + with pytest.raises(ValueError): + server.add_http_options([http_options_1, http_options_2]) + + +# TypeError: cannot convert dictionary update sequence element #0 to a sequence +def test_http_options_not_sequence_fails(): + server = TSC.Server("http://fake-url") + with pytest.raises(ValueError): + server.add_http_options({1, 2, 3}) + + +def test_validate_connection_http(): + url = "http://cookies.com" + server = TSC.Server(url) + server.validate_connection_settings() + assert url == server.server_address + + +def test_validate_connection_https(): + url = "https://cookies.com" + server = TSC.Server(url) + server.validate_connection_settings() + assert url == server.server_address + + +def test_validate_connection_no_protocol(): + url = "cookies.com" + fixed_url = "http://cookies.com" + server = TSC.Server(url) + server.validate_connection_settings() + assert fixed_url == server.server_address + + +test_header = {"x-test": "true"} + + +@pytest.fixture +def session_factory() -> requests.Session: + session = requests.session() + session.headers.update(test_header) + return session + + +def test_session_factory_adds_headers(session_factory): + test_request_bin = "http://capture-this-with-mock.com" + with requests_mock.mock() as m: + m.get(url="http://capture-this-with-mock.com/api/2.4/serverInfo", request_headers=test_header) + server = TSC.Server(test_request_bin, use_server_version=True, session_factory=lambda: session_factory) diff --git a/test/models/__init__.py b/test/models/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/models/_models.py b/test/models/_models.py index 59011c6c3..9be97a87b 100644 --- a/test/models/_models.py +++ b/test/models/_models.py @@ -10,48 +10,27 @@ ) -def get_defined_models(): - # nothing clever here: list was manually copied from tsc/models/__init__.py - return [ - BackgroundJobItem, - ConnectionItem, - DataAccelerationReportItem, - DataAlertItem, - DatasourceItem, - FlowItem, - GroupItem, - JobItem, - MetricItem, - PermissionsRule, - ProjectItem, - RevisionItem, - ScheduleItem, - SubscriptionItem, - Credentials, - JWTAuth, - TableauAuth, - PersonalAccessTokenAuth, - ServerInfoItem, - SiteItem, - TaskItem, - UserItem, - ViewItem, - WebhookItem, - WorkbookItem, - PaginationItem, - Permission.Mode, - Permission.Capability, - DailyInterval, - WeeklyInterval, - MonthlyInterval, - HourlyInterval, - TableItem, - Target, - ] - - def get_unimplemented_models(): return [ + # these items should have repr , please fix + CollectionItem, + DQWItem, + ExtensionsServer, + ExtensionsSiteSettings, + FileuploadItem, + FlowRunItem, + LinkedTaskFlowRunItem, + LinkedTaskItem, + LinkedTaskStepItem, + SafeExtension, + # these should be implemented together for consistency + CSVRequestOptions, + ExcelRequestOptions, + ImageRequestOptions, + PDFRequestOptions, + PPTXRequestOptions, + RequestOptions, + # these don't need it FavoriteItem, # no repr because there is no state Resource, # list of type names TableauItem, # should be an interface diff --git a/test/models/test_repr.py b/test/models/test_repr.py index 92d11978f..34f8509a7 100644 --- a/test/models/test_repr.py +++ b/test/models/test_repr.py @@ -1,15 +1,30 @@ import inspect - -from unittest import TestCase -import _models # type: ignore # did not set types for this +from typing import Any +from test.models._models import get_unimplemented_models import tableauserverclient as TSC -from typing import Any +import pytest -# ensure that all models that don't need parameters can be instantiated -# todo.... -def instantiate_class(name: str, obj: Any): +def is_concrete(obj: Any): + return inspect.isclass(obj) and not inspect.isabstract(obj) + + +@pytest.mark.parametrize("class_name, obj", inspect.getmembers(TSC, is_concrete)) +def test_by_reflection(class_name, obj): + instance = try_instantiate_class(class_name, obj) + if instance: + class_type = type(instance) + if class_type in get_unimplemented_models(): + print(f"Class '{class_name}' has no repr defined, skipping test") + return + else: + assert type(instance.__repr__).__name__ == "method" + print(instance.__repr__.__name__) + + +# Instantiate a class if it doesn't require any parameters +def try_instantiate_class(name: str, obj: Any) -> Any | None: # Get the constructor (init) of the class constructor = getattr(obj, "__init__", None) if constructor: @@ -22,30 +37,12 @@ def instantiate_class(name: str, obj: Any): print(f"Class '{name}' requires the following parameters for instantiation:") for param in required_parameters: print(f"- {param.name}") + return None else: print(f"Class '{name}' does not require any parameters for instantiation.") # Instantiate the class instance = obj() - print(f"Instantiated: {name} -> {instance}") + return instance else: print(f"Class '{name}' does not have a constructor (__init__ method).") - - -class TestAllModels(TestCase): - # not all models have __repr__ yet: see above list - def test_repr_is_implemented(self): - m = _models.get_defined_models() - for model in m: - with self.subTest(model.__name__, model=model): - print(model.__name__, type(model.__repr__).__name__) - self.assertEqual(type(model.__repr__).__name__, "function") - - # 2 - Iterate through the objects in the module - def test_by_reflection(self): - for class_name, obj in inspect.getmembers(TSC, is_concrete): - with self.subTest(class_name, obj=obj): - instantiate_class(class_name, obj) - - -def is_concrete(obj: Any): - return inspect.isclass(obj) and not inspect.isabstract(obj) + return None diff --git a/test/request_factory/test_datasource_requests.py b/test/request_factory/test_datasource_requests.py index 75bb535d5..66b7a373e 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_task_requests.py b/test/request_factory/test_task_requests.py index 6287fa6ea..bf2ccd5fe 100644 --- a/test/request_factory/test_task_requests.py +++ b/test/request_factory/test_task_requests.py @@ -1,47 +1,61 @@ -import unittest import xml.etree.ElementTree as ET from unittest.mock import Mock + +import pytest + from tableauserverclient.server.request_factory import TaskRequest -class TestTaskRequest(unittest.TestCase): - def setUp(self): - self.task_request = TaskRequest() - self.xml_request = ET.Element("tsRequest") - - def test_refresh_req_default(self): - result = self.task_request.refresh_req() - self.assertEqual(result, ET.tostring(self.xml_request)) - - def test_refresh_req_incremental(self): - with self.assertRaises(ValueError): - self.task_request.refresh_req(incremental=True) - - def test_refresh_req_with_parent_srv_version_3_25(self): - parent_srv = Mock() - parent_srv.check_at_least_version.return_value = True - result = self.task_request.refresh_req(incremental=True, parent_srv=parent_srv) - expected_xml = ET.Element("tsRequest") - task_element = ET.SubElement(expected_xml, "extractRefresh") - task_element.attrib["incremental"] = "true" - self.assertEqual(result, ET.tostring(expected_xml)) - - def test_refresh_req_with_parent_srv_version_3_25_non_incremental(self): - parent_srv = Mock() - parent_srv.check_at_least_version.return_value = True - result = self.task_request.refresh_req(incremental=False, parent_srv=parent_srv) - expected_xml = ET.Element("tsRequest") - ET.SubElement(expected_xml, "extractRefresh") - self.assertEqual(result, ET.tostring(expected_xml)) - - def test_refresh_req_with_parent_srv_version_below_3_25(self): - parent_srv = Mock() - parent_srv.check_at_least_version.return_value = False - with self.assertRaises(ValueError): - self.task_request.refresh_req(incremental=True, parent_srv=parent_srv) - - def test_refresh_req_with_parent_srv_version_below_3_25_non_incremental(self): - parent_srv = Mock() - parent_srv.check_at_least_version.return_value = False - result = self.task_request.refresh_req(incremental=False, parent_srv=parent_srv) - self.assertEqual(result, ET.tostring(self.xml_request)) +@pytest.fixture +def task_request() -> TaskRequest: + return TaskRequest() + + +@pytest.fixture +def xml_request() -> ET.Element: + return ET.Element("tsRequest") + + +def test_refresh_req_default(task_request: TaskRequest, xml_request: ET.Element) -> None: + result = task_request.refresh_req() + assert result == ET.tostring(xml_request) + + +def test_refresh_req_incremental(task_request: TaskRequest) -> None: + with pytest.raises(ValueError): + task_request.refresh_req(incremental=True) + + +def test_refresh_req_with_parent_srv_version_3_25(task_request: TaskRequest) -> None: + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = True + result = task_request.refresh_req(incremental=True, parent_srv=parent_srv) + expected_xml = ET.Element("tsRequest") + task_element = ET.SubElement(expected_xml, "extractRefresh") + task_element.attrib["incremental"] = "true" + assert result == ET.tostring(expected_xml) + + +def test_refresh_req_with_parent_srv_version_3_25_non_incremental(task_request: TaskRequest) -> None: + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = True + result = task_request.refresh_req(incremental=False, parent_srv=parent_srv) + expected_xml = ET.Element("tsRequest") + ET.SubElement(expected_xml, "extractRefresh") + assert result == ET.tostring(expected_xml) + + +def test_refresh_req_with_parent_srv_version_below_3_25(task_request: TaskRequest) -> None: + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = False + with pytest.raises(ValueError): + task_request.refresh_req(incremental=True, parent_srv=parent_srv) + + +def test_refresh_req_with_parent_srv_version_below_3_25_non_incremental( + task_request: TaskRequest, xml_request: ET.Element +) -> None: + parent_srv = Mock() + parent_srv.check_at_least_version.return_value = False + result = task_request.refresh_req(incremental=False, parent_srv=parent_srv) + assert result == ET.tostring(xml_request) diff --git a/test/request_factory/test_workbook_requests.py b/test/request_factory/test_workbook_requests.py index 332b6defa..b114e04a0 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 diff --git a/test/test_auth.py b/test/test_auth.py index 09e3e251d..c50f4d29b 100644 --- a/test/test_auth.py +++ b/test/test_auth.py @@ -1,133 +1,146 @@ -import os.path -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -SIGN_IN_XML = os.path.join(TEST_ASSET_DIR, "auth_sign_in.xml") -SIGN_IN_IMPERSONATE_XML = os.path.join(TEST_ASSET_DIR, "auth_sign_in_impersonate.xml") -SIGN_IN_ERROR_XML = os.path.join(TEST_ASSET_DIR, "auth_sign_in_error.xml") - - -class AuthTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.baseurl = self.server.auth.baseurl - - def test_sign_in(self): - with open(SIGN_IN_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml) - tableau_auth = TSC.TableauAuth("testuser", "password", site_id="Samples") - self.server.auth.sign_in(tableau_auth) - - self.assertEqual("eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l", self.server.auth_token) - self.assertEqual("6b7179ba-b82b-4f0f-91ed-812074ac5da6", self.server.site_id) - self.assertEqual("Samples", self.server.site_url) - self.assertEqual("1a96d216-e9b8-497b-a82a-0b899a965e01", self.server.user_id) - - def test_sign_in_with_personal_access_tokens(self): - with open(SIGN_IN_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml) - tableau_auth = TSC.PersonalAccessTokenAuth( - token_name="mytoken", personal_access_token="Random123Generated", site_id="Samples" - ) - self.server.auth.sign_in(tableau_auth) - - self.assertEqual("eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l", self.server.auth_token) - self.assertEqual("6b7179ba-b82b-4f0f-91ed-812074ac5da6", self.server.site_id) - self.assertEqual("Samples", self.server.site_url) - self.assertEqual("1a96d216-e9b8-497b-a82a-0b899a965e01", self.server.user_id) - - def test_sign_in_impersonate(self): - with open(SIGN_IN_IMPERSONATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml) - tableau_auth = TSC.TableauAuth( - "testuser", "password", user_id_to_impersonate="dd2239f6-ddf1-4107-981a-4cf94e415794" - ) - self.server.auth.sign_in(tableau_auth) - - self.assertEqual("MJonFA6HDyy2C3oqR13fRGqE6cmgzwq3", self.server.auth_token) - self.assertEqual("dad65087-b08b-4603-af4e-2887b8aafc67", self.server.site_id) - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", self.server.user_id) - - def test_sign_in_error(self): - with open(SIGN_IN_ERROR_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml, status_code=401) - tableau_auth = TSC.TableauAuth("testuser", "wrongpassword") - self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth) - - def test_sign_in_invalid_token(self): - with open(SIGN_IN_ERROR_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml, status_code=401) - tableau_auth = TSC.PersonalAccessTokenAuth(token_name="mytoken", personal_access_token="invalid") - self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth) - - def test_sign_in_without_auth(self): - with open(SIGN_IN_ERROR_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml, status_code=401) - tableau_auth = TSC.TableauAuth("", "") - self.assertRaises(TSC.FailedSignInError, self.server.auth.sign_in, tableau_auth) - - def test_sign_out(self): - with open(SIGN_IN_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/signin", text=response_xml) - m.post(self.baseurl + "/signout", text="") - tableau_auth = TSC.TableauAuth("testuser", "password") - self.server.auth.sign_in(tableau_auth) - self.server.auth.sign_out() - - self.assertIsNone(self.server._auth_token) - self.assertIsNone(self.server._site_id) - self.assertIsNone(self.server._site_url) - self.assertIsNone(self.server._user_id) - - def test_switch_site(self): - self.server.version = "2.6" - baseurl = self.server.auth.baseurl - site_id, user_id, auth_token = list("123") - self.server._set_auth(site_id, user_id, auth_token) - with open(SIGN_IN_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(baseurl + "/switchSite", text=response_xml) - site = TSC.SiteItem("Samples", "Samples") - self.server.auth.switch_site(site) - - self.assertEqual("eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l", self.server.auth_token) - self.assertEqual("6b7179ba-b82b-4f0f-91ed-812074ac5da6", self.server.site_id) - self.assertEqual("Samples", self.server.site_url) - self.assertEqual("1a96d216-e9b8-497b-a82a-0b899a965e01", self.server.user_id) - - def test_revoke_all_server_admin_tokens(self): - self.server.version = "3.10" - baseurl = self.server.auth.baseurl - with open(SIGN_IN_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(baseurl + "/signin", text=response_xml) - m.post(baseurl + "/revokeAllServerAdminTokens", text="") - tableau_auth = TSC.TableauAuth("testuser", "password") - self.server.auth.sign_in(tableau_auth) - self.server.auth.revoke_all_server_admin_tokens() - - self.assertEqual("eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l", self.server.auth_token) - self.assertEqual("6b7179ba-b82b-4f0f-91ed-812074ac5da6", self.server.site_id) - self.assertEqual("Samples", self.server.site_url) - self.assertEqual("1a96d216-e9b8-497b-a82a-0b899a965e01", self.server.user_id) +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +SIGN_IN_XML = TEST_ASSET_DIR / "auth_sign_in.xml" +SIGN_IN_IMPERSONATE_XML = TEST_ASSET_DIR / "auth_sign_in_impersonate.xml" +SIGN_IN_ERROR_XML = TEST_ASSET_DIR / "auth_sign_in_error.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + """Fixture to create a Tableau Server instance for testing.""" + server_instance = TSC.Server("http://test", False) + return server_instance + + +def test_sign_in(server: TSC.Server) -> None: + with open(SIGN_IN_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml) + tableau_auth = TSC.TableauAuth("testuser", "password", site_id="Samples") + server.auth.sign_in(tableau_auth) + + assert "eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l" == server.auth_token + assert "6b7179ba-b82b-4f0f-91ed-812074ac5da6" == server.site_id + assert "Samples" == server.site_url + assert "1a96d216-e9b8-497b-a82a-0b899a965e01" == server.user_id + + +def test_sign_in_with_personal_access_tokens(server: TSC.Server) -> None: + with open(SIGN_IN_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml) + tableau_auth = TSC.PersonalAccessTokenAuth( + token_name="mytoken", personal_access_token="Random123Generated", site_id="Samples" + ) + server.auth.sign_in(tableau_auth) + + assert "eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l" == server.auth_token + assert "6b7179ba-b82b-4f0f-91ed-812074ac5da6" == server.site_id + assert "Samples" == server.site_url + assert "1a96d216-e9b8-497b-a82a-0b899a965e01" == server.user_id + + +def test_sign_in_impersonate(server: TSC.Server) -> None: + with open(SIGN_IN_IMPERSONATE_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml) + tableau_auth = TSC.TableauAuth( + "testuser", "password", user_id_to_impersonate="dd2239f6-ddf1-4107-981a-4cf94e415794" + ) + server.auth.sign_in(tableau_auth) + + assert "MJonFA6HDyy2C3oqR13fRGqE6cmgzwq3" == server.auth_token + assert "dad65087-b08b-4603-af4e-2887b8aafc67" == server.site_id + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == server.user_id + + +def test_sign_in_error(server: TSC.Server) -> None: + with open(SIGN_IN_ERROR_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml, status_code=401) + tableau_auth = TSC.TableauAuth("testuser", "wrongpassword") + with pytest.raises(TSC.FailedSignInError): + server.auth.sign_in(tableau_auth) + + +def test_sign_in_invalid_token(server: TSC.Server) -> None: + with open(SIGN_IN_ERROR_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml, status_code=401) + tableau_auth = TSC.PersonalAccessTokenAuth(token_name="mytoken", personal_access_token="invalid") + with pytest.raises(TSC.FailedSignInError): + server.auth.sign_in(tableau_auth) + + +def test_sign_in_without_auth(server: TSC.Server) -> None: + with open(SIGN_IN_ERROR_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml, status_code=401) + tableau_auth = TSC.TableauAuth("", "") + with pytest.raises(TSC.FailedSignInError): + server.auth.sign_in(tableau_auth) + + +def test_sign_out(server: TSC.Server) -> None: + with open(SIGN_IN_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(server.auth.baseurl + "/signin", text=response_xml) + m.post(server.auth.baseurl + "/signout", text="") + tableau_auth = TSC.TableauAuth("testuser", "password") + server.auth.sign_in(tableau_auth) + server.auth.sign_out() + + assert server._auth_token is None + assert server._site_id is None + assert server._site_url is None + assert server._user_id is None + + +def test_switch_site(server: TSC.Server) -> None: + server.version = "2.6" + baseurl = server.auth.baseurl + site_id, user_id, auth_token = list("123") + server._set_auth(site_id, user_id, auth_token) + with open(SIGN_IN_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(baseurl + "/switchSite", text=response_xml) + site = TSC.SiteItem("Samples", "Samples") + server.auth.switch_site(site) + + assert "eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l" == server.auth_token + assert "6b7179ba-b82b-4f0f-91ed-812074ac5da6" == server.site_id + assert "Samples" == server.site_url + assert "1a96d216-e9b8-497b-a82a-0b899a965e01" == server.user_id + + +def test_revoke_all_server_admin_tokens(server: TSC.Server) -> None: + server.version = "3.10" + baseurl = server.auth.baseurl + with open(SIGN_IN_XML, "rb") as f: + response_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.post(baseurl + "/signin", text=response_xml) + m.post(baseurl + "/revokeAllServerAdminTokens", text="") + tableau_auth = TSC.TableauAuth("testuser", "password") + server.auth.sign_in(tableau_auth) + server.auth.revoke_all_server_admin_tokens() + + assert "eIX6mvFsqyansa4KqEI1UwOpS8ggRs2l" == server.auth_token + assert "6b7179ba-b82b-4f0f-91ed-812074ac5da6" == server.site_id + assert "Samples" == server.site_url + assert "1a96d216-e9b8-497b-a82a-0b899a965e01" == server.user_id diff --git a/test/test_connection_.py b/test/test_connection_.py index 47b796ebe..8bfed79c7 100644 --- a/test/test_connection_.py +++ b/test/test_connection_.py @@ -1,34 +1,25 @@ -import unittest import tableauserverclient as TSC +import pytest -class DatasourceModelTests(unittest.TestCase): - def test_require_boolean_query_tag_fails(self): - conn = TSC.ConnectionItem() - conn._connection_type = "postgres" - with self.assertRaises(ValueError): - conn.query_tagging = "no" - def test_set_query_tag_normal_conn(self): - conn = TSC.ConnectionItem() - conn._connection_type = "postgres" - conn.query_tagging = True - self.assertEqual(conn.query_tagging, True) +def test_require_boolean_query_tag_fails() -> None: + conn = TSC.ConnectionItem() + conn._connection_type = "postgres" + with pytest.raises(ValueError): + conn.query_tagging = "no" # type: ignore[assignment] - def test_ignore_query_tag_for_hyper(self): - conn = TSC.ConnectionItem() - conn._connection_type = "hyper" - conn.query_tagging = True - self.assertEqual(conn.query_tagging, None) - def test_ignore_query_tag_for_teradata(self): - conn = TSC.ConnectionItem() - conn._connection_type = "teradata" - conn.query_tagging = True - self.assertEqual(conn.query_tagging, None) +def test_set_query_tag_normal_conn() -> None: + conn = TSC.ConnectionItem() + conn._connection_type = "postgres" + conn.query_tagging = True + assert conn.query_tagging - def test_ignore_query_tag_for_snowflake(self): - conn = TSC.ConnectionItem() - conn._connection_type = "snowflake" - conn.query_tagging = True - self.assertEqual(conn.query_tagging, None) + +@pytest.mark.parametrize("conn_type", ["hyper", "teradata", "snowflake"]) +def test_ignore_query_tag(conn_type: str) -> None: + conn = TSC.ConnectionItem() + conn._connection_type = conn_type + conn.query_tagging = True + assert conn.query_tagging is None diff --git a/test/test_custom_view.py b/test/test_custom_view.py index 6e863a863..2a3932726 100644 --- a/test/test_custom_view.py +++ b/test/test_custom_view.py @@ -3,318 +3,340 @@ import os from pathlib import Path from tempfile import TemporaryDirectory -import unittest +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.config import BYTES_PER_MB from tableauserverclient.datetime_helpers import format_datetime -from tableauserverclient.server.endpoint.exceptions import MissingRequiredFieldError TEST_ASSET_DIR = Path(__file__).parent / "assets" -GET_XML = os.path.join(TEST_ASSET_DIR, "custom_view_get.xml") -GET_XML_ID = os.path.join(TEST_ASSET_DIR, "custom_view_get_id.xml") -POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, "Sample View Image.png") -CUSTOM_VIEW_UPDATE_XML = os.path.join(TEST_ASSET_DIR, "custom_view_update.xml") -CUSTOM_VIEW_POPULATE_PDF = os.path.join(TEST_ASSET_DIR, "populate_pdf.pdf") -CUSTOM_VIEW_POPULATE_CSV = os.path.join(TEST_ASSET_DIR, "populate_csv.csv") +GET_XML = TEST_ASSET_DIR / "custom_view_get.xml" +GET_XML_ID = TEST_ASSET_DIR / "custom_view_get_id.xml" +POPULATE_PREVIEW_IMAGE = TEST_ASSET_DIR / "Sample View Image.png" +CUSTOM_VIEW_UPDATE_XML = TEST_ASSET_DIR / "custom_view_update.xml" +CUSTOM_VIEW_POPULATE_PDF = TEST_ASSET_DIR / "populate_pdf.pdf" +CUSTOM_VIEW_POPULATE_CSV = TEST_ASSET_DIR / "populate_csv.csv" CUSTOM_VIEW_DOWNLOAD = TEST_ASSET_DIR / "custom_view_download.json" FILE_UPLOAD_INIT = TEST_ASSET_DIR / "fileupload_initialize.xml" FILE_UPLOAD_APPEND = TEST_ASSET_DIR / "fileupload_append.xml" -class CustomViewTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.server.version = "3.21" # custom views only introduced in 3.19 - - # Fake sign in - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.custom_views.baseurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - print(response_xml) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_views, pagination_item = self.server.custom_views.get() - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", all_views[0].id) - self.assertEqual("ENDANGERED SAFARI", all_views[0].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", all_views[0].content_url) - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", all_views[0].workbook.id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_views[0].owner.id) - self.assertIsNone(all_views[0].created_at) - self.assertIsNone(all_views[0].updated_at) - self.assertFalse(all_views[0].shared) - - self.assertEqual("fd252f73-593c-4c4e-8584-c032b8022adc", all_views[1].id) - self.assertEqual("Overview", all_views[1].name) - self.assertEqual("6d13b0ca-043d-4d42-8c9d-3f3313ea3a00", all_views[1].workbook.id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_views[1].owner.id) - self.assertEqual("2002-05-30T09:00:00Z", format_datetime(all_views[1].created_at)) - self.assertEqual("2002-06-05T08:00:59Z", format_datetime(all_views[1].updated_at)) - self.assertTrue(all_views[1].shared) - - def test_get_by_id(self) -> None: - with open(GET_XML_ID, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5", text=response_xml) - view: TSC.CustomViewItem = self.server.custom_views.get_by_id("d79634e1-6063-4ec9-95ff-50acbf609ff5") - - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", view.id) - self.assertEqual("ENDANGERED SAFARI", view.name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", view.content_url) - if view.workbook: - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", view.workbook.id) - if view.owner: - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", view.owner.id) - if view.view: - self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", view.view.id) - self.assertEqual("2002-05-30T09:00:00Z", format_datetime(view.created_at)) - self.assertEqual("2002-06-05T08:00:59Z", format_datetime(view.updated_at)) - - def test_get_by_id_missing_id(self) -> None: - self.assertRaises(TSC.MissingRequiredFieldError, self.server.custom_views.get_by_id, None) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.custom_views.get) - - def test_populate_image(self) -> None: - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image", content=response) - single_view = TSC.CustomViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - self.server.custom_views.populate_image(single_view) - self.assertEqual(response, single_view.image) - - def test_populate_image_with_options(self) -> None: - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?resolution=high&maxAge=10", content=response - ) - single_view = TSC.CustomViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High, maxage=10) - self.server.custom_views.populate_image(single_view, req_option) - self.assertEqual(response, single_view.image) - - def test_populate_image_missing_id(self) -> None: +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + server.version = "3.21" # custom views only introduced in 3.19 + + # Fake sign in + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + print(response_xml) + with requests_mock.mock() as m: + m.get(server.custom_views.baseurl, text=response_xml) + all_views, pagination_item = server.custom_views.get() + + assert 2 == pagination_item.total_available + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == all_views[0].id + assert "ENDANGERED SAFARI" == all_views[0].name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == all_views[0].content_url + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == all_views[0].workbook.id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_views[0].owner.id + assert all_views[0].created_at is None + assert all_views[0].updated_at is None + assert not all_views[0].shared + + assert "fd252f73-593c-4c4e-8584-c032b8022adc" == all_views[1].id + assert "Overview" == all_views[1].name + assert "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" == all_views[1].workbook.id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_views[1].owner.id + assert "2002-05-30T09:00:00Z" == format_datetime(all_views[1].created_at) + assert "2002-06-05T08:00:59Z" == format_datetime(all_views[1].updated_at) + assert all_views[1].shared + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = GET_XML_ID.read_text() + with requests_mock.mock() as m: + m.get(server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5", text=response_xml) + view: TSC.CustomViewItem = server.custom_views.get_by_id("d79634e1-6063-4ec9-95ff-50acbf609ff5") + + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == view.id + assert "ENDANGERED SAFARI" == view.name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == view.content_url + if view.workbook: + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == view.workbook.id + if view.owner: + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == view.owner.id + if view.view: + assert "5241e88d-d384-4fd7-9c2f-648b5247efc5" == view.view.id + assert "2002-05-30T09:00:00Z" == format_datetime(view.created_at) + assert "2002-06-05T08:00:59Z" == format_datetime(view.updated_at) + + +def test_get_by_id_missing_id(server: TSC.Server) -> None: + with pytest.raises(TSC.MissingRequiredFieldError): + server.custom_views.get_by_id(None) + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.custom_views.get() + + +def test_populate_image(server: TSC.Server) -> None: + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get(server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image", content=response) single_view = TSC.CustomViewItem() - single_view._id = None - self.assertRaises(TSC.MissingRequiredFieldError, self.server.custom_views.populate_image, single_view) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", status_code=204) - self.server.custom_views.delete("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - - def test_delete_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.custom_views.delete, "") - - def test_update(self) -> None: - with open(CUSTOM_VIEW_UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - the_custom_view = TSC.CustomViewItem("1d0304cd-3796-429f-b815-7258370b9b74", name="Best test ever") - the_custom_view._id = "1f951daf-4061-451a-9df1-69a8062664f2" - the_custom_view.owner = TSC.UserItem() - the_custom_view.owner.id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - the_custom_view = self.server.custom_views.update(the_custom_view) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", the_custom_view.id) - if the_custom_view.owner: - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", the_custom_view.owner.id) - self.assertEqual("Best test ever", the_custom_view.name) - - def test_update_missing_id(self) -> None: - cv = TSC.CustomViewItem(name="test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.custom_views.update, cv) - - def test_download(self) -> None: - cv = TSC.CustomViewItem(name="test") - cv._id = "1f951daf-4061-451a-9df1-69a8062664f2" - content = CUSTOM_VIEW_DOWNLOAD.read_bytes() - data = io.BytesIO() - with requests_mock.mock() as m: - m.get(f"{self.server.custom_views.expurl}/1f951daf-4061-451a-9df1-69a8062664f2/content", content=content) - self.server.custom_views.download(cv, data) - - assert data.getvalue() == content - - def test_publish_filepath(self) -> None: - cv = TSC.CustomViewItem(name="test") - cv._owner = TSC.UserItem() - cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - cv._workbook = TSC.WorkbookItem() - cv._workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - with requests_mock.mock() as m: - m.post(self.server.custom_views.expurl, status_code=201, text=Path(GET_XML).read_text()) - view = self.server.custom_views.publish(cv, CUSTOM_VIEW_DOWNLOAD) - - assert view is not None - assert isinstance(view, TSC.CustomViewItem) - assert view.id is not None - assert view.name is not None - - def test_publish_file_str(self) -> None: - cv = TSC.CustomViewItem(name="test") - cv._owner = TSC.UserItem() - cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - cv._workbook = TSC.WorkbookItem() - cv._workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - with requests_mock.mock() as m: - m.post(self.server.custom_views.expurl, status_code=201, text=Path(GET_XML).read_text()) - view = self.server.custom_views.publish(cv, str(CUSTOM_VIEW_DOWNLOAD)) - - assert view is not None - assert isinstance(view, TSC.CustomViewItem) - assert view.id is not None - assert view.name is not None - - def test_publish_file_io(self) -> None: - cv = TSC.CustomViewItem(name="test") - cv._owner = TSC.UserItem() - cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - cv._workbook = TSC.WorkbookItem() - cv._workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - data = io.BytesIO(CUSTOM_VIEW_DOWNLOAD.read_bytes()) - with requests_mock.mock() as m: - m.post(self.server.custom_views.expurl, status_code=201, text=Path(GET_XML).read_text()) - view = self.server.custom_views.publish(cv, data) - - assert view is not None - assert isinstance(view, TSC.CustomViewItem) - assert view.id is not None - assert view.name is not None - - def test_publish_missing_owner_id(self) -> None: - cv = TSC.CustomViewItem(name="test") - cv._owner = TSC.UserItem() - cv._workbook = TSC.WorkbookItem() - cv._workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - with requests_mock.mock() as m: - m.post(self.server.custom_views.expurl, status_code=201, text=Path(GET_XML).read_text()) - with self.assertRaises(ValueError): - self.server.custom_views.publish(cv, CUSTOM_VIEW_DOWNLOAD) - - def test_publish_missing_wb_id(self) -> None: - cv = TSC.CustomViewItem(name="test") - cv._owner = TSC.UserItem() - cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - cv._workbook = TSC.WorkbookItem() - with requests_mock.mock() as m: - m.post(self.server.custom_views.expurl, status_code=201, text=Path(GET_XML).read_text()) - with self.assertRaises(ValueError): - self.server.custom_views.publish(cv, CUSTOM_VIEW_DOWNLOAD) - - def test_large_publish(self): - cv = TSC.CustomViewItem(name="test") - cv._owner = TSC.UserItem() - cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - cv._workbook = TSC.WorkbookItem() - cv._workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - with ExitStack() as stack: - temp_dir = stack.enter_context(TemporaryDirectory()) - file_path = Path(temp_dir) / "test_file" - file_path.write_bytes(os.urandom(65 * BYTES_PER_MB)) - mock = stack.enter_context(requests_mock.mock()) - # Mock initializing upload - mock.post(self.server.fileuploads.baseurl, status_code=201, text=FILE_UPLOAD_INIT.read_text()) - # Mock the upload - mock.put( - f"{self.server.fileuploads.baseurl}/7720:170fe6b1c1c7422dadff20f944d58a52-1:0", - text=FILE_UPLOAD_APPEND.read_text(), - ) - # Mock the publish - mock.post(self.server.custom_views.expurl, status_code=201, text=Path(GET_XML).read_text()) - - view = self.server.custom_views.publish(cv, file_path) - - assert view is not None - assert isinstance(view, TSC.CustomViewItem) - assert view.id is not None - assert view.name is not None - - def test_populate_pdf(self) -> None: - self.server.version = "3.23" - self.baseurl = self.server.custom_views.baseurl - with open(CUSTOM_VIEW_POPULATE_PDF, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait&maxAge=5", - content=response, - ) - custom_view = TSC.CustomViewItem() - custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - - size = TSC.PDFRequestOptions.PageType.Letter - orientation = TSC.PDFRequestOptions.Orientation.Portrait - req_option = TSC.PDFRequestOptions(size, orientation, 5) - - self.server.custom_views.populate_pdf(custom_view, req_option) - self.assertEqual(response, custom_view.pdf) - - def test_populate_csv(self) -> None: - self.server.version = "3.23" - self.baseurl = self.server.custom_views.baseurl - with open(CUSTOM_VIEW_POPULATE_CSV, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data?maxAge=1", content=response) - custom_view = TSC.CustomViewItem() - custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - request_option = TSC.CSVRequestOptions(maxage=1) - self.server.custom_views.populate_csv(custom_view, request_option) - - csv_file = b"".join(custom_view.csv) - self.assertEqual(response, csv_file) - - def test_populate_csv_default_maxage(self) -> None: - self.server.version = "3.23" - self.baseurl = self.server.custom_views.baseurl - with open(CUSTOM_VIEW_POPULATE_CSV, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data", content=response) - custom_view = TSC.CustomViewItem() - custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - self.server.custom_views.populate_csv(custom_view) - - csv_file = b"".join(custom_view.csv) - self.assertEqual(response, csv_file) - - def test_pdf_height(self) -> None: - self.server.version = "3.23" - self.baseurl = self.server.custom_views.baseurl - with open(CUSTOM_VIEW_POPULATE_PDF, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?vizHeight=1080&vizWidth=1920", - content=response, - ) - custom_view = TSC.CustomViewItem() - custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - - req_option = TSC.PDFRequestOptions( - viz_height=1080, - viz_width=1920, - ) - - self.server.custom_views.populate_pdf(custom_view, req_option) - self.assertEqual(response, custom_view.pdf) + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + server.custom_views.populate_image(single_view) + assert response == single_view.image + + +def test_populate_image_with_options(server: TSC.Server) -> None: + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?resolution=high&maxAge=10", + content=response, + ) + single_view = TSC.CustomViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High, maxage=10) + server.custom_views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_image_missing_id(server: TSC.Server) -> None: + single_view = TSC.CustomViewItem() + single_view._id = None + with pytest.raises(TSC.MissingRequiredFieldError): + server.custom_views.populate_image(single_view) + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.custom_views.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", status_code=204) + server.custom_views.delete("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + +def test_delete_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.custom_views.delete("") + + +def test_update(server: TSC.Server) -> None: + response_xml = CUSTOM_VIEW_UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.custom_views.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + the_custom_view = TSC.CustomViewItem("1d0304cd-3796-429f-b815-7258370b9b74", name="Best test ever") + the_custom_view._id = "1f951daf-4061-451a-9df1-69a8062664f2" + the_custom_view.owner = TSC.UserItem() + assert the_custom_view.owner is not None # for mypy + the_custom_view.owner.id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + the_custom_view = server.custom_views.update(the_custom_view) + + assert isinstance(the_custom_view, TSC.CustomViewItem) + assert "1f951daf-4061-451a-9df1-69a8062664f2" == the_custom_view.id + if the_custom_view.owner: + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == the_custom_view.owner.id + assert "Best test ever" == the_custom_view.name + + +def test_update_missing_id(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.custom_views.update(cv) + + +def test_download(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + cv._id = "1f951daf-4061-451a-9df1-69a8062664f2" + content = CUSTOM_VIEW_DOWNLOAD.read_bytes() + data = io.BytesIO() + with requests_mock.mock() as m: + m.get(f"{server.custom_views.expurl}/1f951daf-4061-451a-9df1-69a8062664f2/content", content=content) + server.custom_views.download(cv, data) + + assert data.getvalue() == content + + +def test_publish_filepath(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + cv._owner = TSC.UserItem() + cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None + cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + with requests_mock.mock() as m: + m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) + view = server.custom_views.publish(cv, CUSTOM_VIEW_DOWNLOAD) + + assert view is not None + assert isinstance(view, TSC.CustomViewItem) + assert view.id is not None + assert view.name is not None + + +def test_publish_file_str(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + cv._owner = TSC.UserItem() + cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None + cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + with requests_mock.mock() as m: + m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) + view = server.custom_views.publish(cv, str(CUSTOM_VIEW_DOWNLOAD)) + + assert view is not None + assert isinstance(view, TSC.CustomViewItem) + assert view.id is not None + assert view.name is not None + + +def test_publish_file_io(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + cv._owner = TSC.UserItem() + cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None + cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + data = io.BytesIO(CUSTOM_VIEW_DOWNLOAD.read_bytes()) + with requests_mock.mock() as m: + m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) + view = server.custom_views.publish(cv, data) + + assert view is not None + assert isinstance(view, TSC.CustomViewItem) + assert view.id is not None + assert view.name is not None + + +def test_publish_missing_owner_id(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + cv._owner = TSC.UserItem() + cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None + cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + with requests_mock.mock() as m: + m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) + with pytest.raises(ValueError): + server.custom_views.publish(cv, CUSTOM_VIEW_DOWNLOAD) + + +def test_publish_missing_wb_id(server: TSC.Server) -> None: + cv = TSC.CustomViewItem(name="test") + cv._owner = TSC.UserItem() + cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + cv.workbook = TSC.WorkbookItem() + with requests_mock.mock() as m: + m.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) + with pytest.raises(ValueError): + server.custom_views.publish(cv, CUSTOM_VIEW_DOWNLOAD) + + +def test_large_publish(server: TSC.Server): + cv = TSC.CustomViewItem(name="test") + cv._owner = TSC.UserItem() + cv._owner._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + cv.workbook = TSC.WorkbookItem() + assert cv.workbook is not None + cv.workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + with ExitStack() as stack: + temp_dir = stack.enter_context(TemporaryDirectory()) + file_path = Path(temp_dir) / "test_file" + file_path.write_bytes(os.urandom(65 * BYTES_PER_MB)) + mock = stack.enter_context(requests_mock.mock()) + # Mock initializing upload + mock.post(server.fileuploads.baseurl, status_code=201, text=FILE_UPLOAD_INIT.read_text()) + # Mock the upload + mock.put( + f"{server.fileuploads.baseurl}/7720:170fe6b1c1c7422dadff20f944d58a52-1:0", + text=FILE_UPLOAD_APPEND.read_text(), + ) + # Mock the publish + mock.post(server.custom_views.expurl, status_code=201, text=GET_XML.read_text()) + + view = server.custom_views.publish(cv, file_path) + + assert view is not None + assert isinstance(view, TSC.CustomViewItem) + assert view.id is not None + assert view.name is not None + + +def test_populate_pdf(server: TSC.Server) -> None: + server.version = "3.23" + response = CUSTOM_VIEW_POPULATE_PDF.read_bytes() + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait&maxAge=5", + content=response, + ) + custom_view = TSC.CustomViewItem() + custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + size = TSC.PDFRequestOptions.PageType.Letter + orientation = TSC.PDFRequestOptions.Orientation.Portrait + req_option = TSC.PDFRequestOptions(size, orientation, 5) + + server.custom_views.populate_pdf(custom_view, req_option) + assert response == custom_view.pdf + + +def test_populate_csv(server: TSC.Server) -> None: + server.version = "3.23" + response = CUSTOM_VIEW_POPULATE_CSV.read_bytes() + with requests_mock.mock() as m: + m.get(server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data?maxAge=1", content=response) + custom_view = TSC.CustomViewItem() + custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + request_option = TSC.CSVRequestOptions(maxage=1) + server.custom_views.populate_csv(custom_view, request_option) + + csv_file = b"".join(custom_view.csv) + assert response == csv_file + + +def test_populate_csv_default_maxage(server: TSC.Server) -> None: + server.version = "3.23" + response = CUSTOM_VIEW_POPULATE_CSV.read_bytes() + with requests_mock.mock() as m: + m.get(server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data", content=response) + custom_view = TSC.CustomViewItem() + custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + server.custom_views.populate_csv(custom_view) + + csv_file = b"".join(custom_view.csv) + assert response == csv_file + + +def test_pdf_height(server: TSC.Server) -> None: + server.version = "3.23" + response = CUSTOM_VIEW_POPULATE_PDF.read_bytes() + with requests_mock.mock() as m: + m.get( + server.custom_views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?vizHeight=1080&vizWidth=1920", + content=response, + ) + custom_view = TSC.CustomViewItem() + custom_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + req_option = TSC.PDFRequestOptions( + viz_height=1080, + viz_width=1920, + ) + + server.custom_views.populate_pdf(custom_view, req_option) + assert response == custom_view.pdf diff --git a/test/test_data_acceleration_report.py b/test/test_data_acceleration_report.py index 8f9f5a49e..c0589a84b 100644 --- a/test/test_data_acceleration_report.py +++ b/test/test_data_acceleration_report.py @@ -1,42 +1,45 @@ -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -from ._utils import read_xml_asset -GET_XML = "data_acceleration_report.xml" +TEST_ASSETS_DIR = Path(__file__).parent / "assets" +GET_XML = TEST_ASSETS_DIR / "data_acceleration_report.xml" -class DataAccelerationReportTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.8" +@pytest.fixture(scope="function") +def server(): + server = TSC.Server("http://test", False) - self.baseurl = self.server.data_acceleration_report.baseurl + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.8" - def test_get(self): - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - data_acceleration_report = self.server.data_acceleration_report.get() + return server - self.assertEqual(2, len(data_acceleration_report.comparison_records)) - self.assertEqual("site-1", data_acceleration_report.comparison_records[0].site) - self.assertEqual("sheet-1", data_acceleration_report.comparison_records[0].sheet_uri) - self.assertEqual("0", data_acceleration_report.comparison_records[0].unaccelerated_session_count) - self.assertEqual("0.0", data_acceleration_report.comparison_records[0].avg_non_accelerated_plt) - self.assertEqual("1", data_acceleration_report.comparison_records[0].accelerated_session_count) - self.assertEqual("0.166", data_acceleration_report.comparison_records[0].avg_accelerated_plt) +def test_get_data_acceleration_report(server): + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.data_acceleration_report.baseurl, text=response_xml) + data_acceleration_report = server.data_acceleration_report.get() - self.assertEqual("site-2", data_acceleration_report.comparison_records[1].site) - self.assertEqual("sheet-2", data_acceleration_report.comparison_records[1].sheet_uri) - self.assertEqual("2", data_acceleration_report.comparison_records[1].unaccelerated_session_count) - self.assertEqual("1.29", data_acceleration_report.comparison_records[1].avg_non_accelerated_plt) - self.assertEqual("3", data_acceleration_report.comparison_records[1].accelerated_session_count) - self.assertEqual("0.372", data_acceleration_report.comparison_records[1].avg_accelerated_plt) + assert 2 == len(data_acceleration_report.comparison_records) + + assert "site-1" == data_acceleration_report.comparison_records[0].site + assert "sheet-1" == data_acceleration_report.comparison_records[0].sheet_uri + assert "0" == data_acceleration_report.comparison_records[0].unaccelerated_session_count + assert "0.0" == data_acceleration_report.comparison_records[0].avg_non_accelerated_plt + assert "1" == data_acceleration_report.comparison_records[0].accelerated_session_count + assert "0.166" == data_acceleration_report.comparison_records[0].avg_accelerated_plt + + assert "site-2" == data_acceleration_report.comparison_records[1].site + assert "sheet-2" == data_acceleration_report.comparison_records[1].sheet_uri + assert "2" == data_acceleration_report.comparison_records[1].unaccelerated_session_count + assert "1.29" == data_acceleration_report.comparison_records[1].avg_non_accelerated_plt + assert "3" == data_acceleration_report.comparison_records[1].accelerated_session_count + assert "0.372" == data_acceleration_report.comparison_records[1].avg_accelerated_plt diff --git a/test/test_data_freshness_policy.py b/test/test_data_freshness_policy.py index 9591a6380..3c5bf5cc2 100644 --- a/test/test_data_freshness_policy.py +++ b/test/test_data_freshness_policy.py @@ -1,189 +1,182 @@ -import os +from pathlib import Path import requests_mock -import unittest + +import pytest import tableauserverclient as TSC -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -UPDATE_DFP_ALWAYS_LIVE_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_data_freshness_policy.xml") -UPDATE_DFP_SITE_DEFAULT_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_data_freshness_policy2.xml") -UPDATE_DFP_FRESH_EVERY_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_data_freshness_policy3.xml") -UPDATE_DFP_FRESH_AT_DAILY_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_data_freshness_policy4.xml") -UPDATE_DFP_FRESH_AT_WEEKLY_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_data_freshness_policy5.xml") -UPDATE_DFP_FRESH_AT_MONTHLY_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_data_freshness_policy6.xml") - - -class WorkbookTests(unittest.TestCase): - def setUp(self) -> None: - 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_update_DFP_always_live(self) -> None: - with open(UPDATE_DFP_ALWAYS_LIVE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.AlwaysLive - ) - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("AlwaysLive", single_workbook.data_freshness_policy.option) - - def test_update_DFP_site_default(self) -> None: - with open(UPDATE_DFP_SITE_DEFAULT_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.SiteDefault - ) - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("SiteDefault", single_workbook.data_freshness_policy.option) - - def test_update_DFP_fresh_every(self) -> None: - with open(UPDATE_DFP_FRESH_EVERY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshEvery - ) - fresh_every_ten_hours = TSC.DataFreshnessPolicyItem.FreshEvery( - TSC.DataFreshnessPolicyItem.FreshEvery.Frequency.Hours, 10 - ) - single_workbook.data_freshness_policy.fresh_every_schedule = fresh_every_ten_hours - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("FreshEvery", single_workbook.data_freshness_policy.option) - self.assertEqual("Hours", single_workbook.data_freshness_policy.fresh_every_schedule.frequency) - self.assertEqual(10, single_workbook.data_freshness_policy.fresh_every_schedule.value) - - def test_update_DFP_fresh_every_missing_attributes(self) -> None: - with open(UPDATE_DFP_FRESH_EVERY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshEvery - ) - - self.assertRaises(ValueError, self.server.workbooks.update, single_workbook) - - def test_update_DFP_fresh_at_day(self) -> None: - with open(UPDATE_DFP_FRESH_AT_DAILY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshAt - ) - fresh_at_10pm_daily = TSC.DataFreshnessPolicyItem.FreshAt( - TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Day, "22:00:00", " Asia/Singapore" - ) - single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_10pm_daily - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("FreshAt", single_workbook.data_freshness_policy.option) - self.assertEqual("Day", single_workbook.data_freshness_policy.fresh_at_schedule.frequency) - self.assertEqual("22:00:00", single_workbook.data_freshness_policy.fresh_at_schedule.time) - self.assertEqual("Asia/Singapore", single_workbook.data_freshness_policy.fresh_at_schedule.timezone) - - def test_update_DFP_fresh_at_week(self) -> None: - with open(UPDATE_DFP_FRESH_AT_WEEKLY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshAt - ) - fresh_at_10am_mon_wed = TSC.DataFreshnessPolicyItem.FreshAt( - TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Week, - "10:00:00", - "America/Los_Angeles", - ["Monday", "Wednesday"], - ) - single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_10am_mon_wed - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("FreshAt", single_workbook.data_freshness_policy.option) - self.assertEqual("Week", single_workbook.data_freshness_policy.fresh_at_schedule.frequency) - self.assertEqual("10:00:00", single_workbook.data_freshness_policy.fresh_at_schedule.time) - self.assertEqual("Wednesday", single_workbook.data_freshness_policy.fresh_at_schedule.interval_item[0]) - self.assertEqual("Monday", single_workbook.data_freshness_policy.fresh_at_schedule.interval_item[1]) - - def test_update_DFP_fresh_at_month(self) -> None: - with open(UPDATE_DFP_FRESH_AT_MONTHLY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshAt - ) - fresh_at_00am_lastDayOfMonth = TSC.DataFreshnessPolicyItem.FreshAt( - TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Month, "00:00:00", "America/Los_Angeles", ["LastDay"] - ) - single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_00am_lastDayOfMonth - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("FreshAt", single_workbook.data_freshness_policy.option) - self.assertEqual("Month", single_workbook.data_freshness_policy.fresh_at_schedule.frequency) - self.assertEqual("00:00:00", single_workbook.data_freshness_policy.fresh_at_schedule.time) - self.assertEqual("LastDay", single_workbook.data_freshness_policy.fresh_at_schedule.interval_item[0]) - - def test_update_DFP_fresh_at_missing_params(self) -> None: - with open(UPDATE_DFP_FRESH_AT_DAILY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshAt - ) - - self.assertRaises(ValueError, self.server.workbooks.update, single_workbook) - - def test_update_DFP_fresh_at_missing_interval(self) -> None: - with open(UPDATE_DFP_FRESH_AT_DAILY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( - TSC.DataFreshnessPolicyItem.Option.FreshAt - ) - fresh_at_month_no_interval = TSC.DataFreshnessPolicyItem.FreshAt( - TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Month, "00:00:00", "America/Los_Angeles" - ) - single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_month_no_interval - - self.assertRaises(ValueError, self.server.workbooks.update, single_workbook) +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +UPDATE_DFP_ALWAYS_LIVE_XML = TEST_ASSET_DIR / "workbook_update_data_freshness_policy.xml" +UPDATE_DFP_SITE_DEFAULT_XML = TEST_ASSET_DIR / "workbook_update_data_freshness_policy2.xml" +UPDATE_DFP_FRESH_EVERY_XML = TEST_ASSET_DIR / "workbook_update_data_freshness_policy3.xml" +UPDATE_DFP_FRESH_AT_DAILY_XML = TEST_ASSET_DIR / "workbook_update_data_freshness_policy4.xml" +UPDATE_DFP_FRESH_AT_WEEKLY_XML = TEST_ASSET_DIR / "workbook_update_data_freshness_policy5.xml" +UPDATE_DFP_FRESH_AT_MONTHLY_XML = TEST_ASSET_DIR / "workbook_update_data_freshness_policy6.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + # Fake sign in + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + + return server + + +def test_update_DFP_always_live(server) -> None: + response_xml = UPDATE_DFP_ALWAYS_LIVE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( + TSC.DataFreshnessPolicyItem.Option.AlwaysLive + ) + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "AlwaysLive" == single_workbook.data_freshness_policy.option + + +def test_update_DFP_site_default(server) -> None: + response_xml = UPDATE_DFP_SITE_DEFAULT_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( + TSC.DataFreshnessPolicyItem.Option.SiteDefault + ) + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "SiteDefault" == single_workbook.data_freshness_policy.option + + +def test_update_DFP_fresh_every(server) -> None: + response_xml = UPDATE_DFP_FRESH_EVERY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( + TSC.DataFreshnessPolicyItem.Option.FreshEvery + ) + fresh_every_ten_hours = TSC.DataFreshnessPolicyItem.FreshEvery( + TSC.DataFreshnessPolicyItem.FreshEvery.Frequency.Hours, 10 + ) + single_workbook.data_freshness_policy.fresh_every_schedule = fresh_every_ten_hours + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "FreshEvery" == single_workbook.data_freshness_policy.option + assert "Hours" == single_workbook.data_freshness_policy.fresh_every_schedule.frequency + assert 10 == single_workbook.data_freshness_policy.fresh_every_schedule.value + + +def test_update_DFP_fresh_every_missing_attributes(server) -> None: + response_xml = UPDATE_DFP_FRESH_EVERY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem( + TSC.DataFreshnessPolicyItem.Option.FreshEvery + ) + + with pytest.raises(ValueError): + server.workbooks.update(single_workbook) + + +def test_update_DFP_fresh_at_day(server) -> None: + response_xml = UPDATE_DFP_FRESH_AT_DAILY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem(TSC.DataFreshnessPolicyItem.Option.FreshAt) + fresh_at_10pm_daily = TSC.DataFreshnessPolicyItem.FreshAt( + TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Day, "22:00:00", " Asia/Singapore" + ) + single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_10pm_daily + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "FreshAt" == single_workbook.data_freshness_policy.option + assert "Day" == single_workbook.data_freshness_policy.fresh_at_schedule.frequency + assert "22:00:00" == single_workbook.data_freshness_policy.fresh_at_schedule.time + assert "Asia/Singapore" == single_workbook.data_freshness_policy.fresh_at_schedule.timezone + + +def test_update_DFP_fresh_at_week(server) -> None: + response_xml = UPDATE_DFP_FRESH_AT_WEEKLY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem(TSC.DataFreshnessPolicyItem.Option.FreshAt) + fresh_at_10am_mon_wed = TSC.DataFreshnessPolicyItem.FreshAt( + TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Week, + "10:00:00", + "America/Los_Angeles", + ["Monday", "Wednesday"], + ) + single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_10am_mon_wed + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "FreshAt" == single_workbook.data_freshness_policy.option + assert "Week" == single_workbook.data_freshness_policy.fresh_at_schedule.frequency + assert "10:00:00" == single_workbook.data_freshness_policy.fresh_at_schedule.time + assert "Wednesday" == single_workbook.data_freshness_policy.fresh_at_schedule.interval_item[0] + assert "Monday" == single_workbook.data_freshness_policy.fresh_at_schedule.interval_item[1] + + +def test_update_DFP_fresh_at_month(server) -> None: + response_xml = UPDATE_DFP_FRESH_AT_MONTHLY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem(TSC.DataFreshnessPolicyItem.Option.FreshAt) + fresh_at_00am_lastDayOfMonth = TSC.DataFreshnessPolicyItem.FreshAt( + TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Month, "00:00:00", "America/Los_Angeles", ["LastDay"] + ) + single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_00am_lastDayOfMonth + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "FreshAt" == single_workbook.data_freshness_policy.option + assert "Month" == single_workbook.data_freshness_policy.fresh_at_schedule.frequency + assert "00:00:00" == single_workbook.data_freshness_policy.fresh_at_schedule.time + assert "LastDay" == single_workbook.data_freshness_policy.fresh_at_schedule.interval_item[0] + + +def test_update_DFP_fresh_at_missing_params(server) -> None: + response_xml = UPDATE_DFP_FRESH_AT_DAILY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem(TSC.DataFreshnessPolicyItem.Option.FreshAt) + + with pytest.raises(ValueError): + server.workbooks.update(single_workbook) + + +def test_update_DFP_fresh_at_missing_interval(server) -> None: + response_xml = UPDATE_DFP_FRESH_AT_DAILY_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_freshness_policy = TSC.DataFreshnessPolicyItem(TSC.DataFreshnessPolicyItem.Option.FreshAt) + fresh_at_month_no_interval = TSC.DataFreshnessPolicyItem.FreshAt( + TSC.DataFreshnessPolicyItem.FreshAt.Frequency.Month, "00:00:00", "America/Los_Angeles" + ) + single_workbook.data_freshness_policy.fresh_at_schedule = fresh_at_month_no_interval + + with pytest.raises(ValueError): + server.workbooks.update(single_workbook) diff --git a/test/test_dataalert.py b/test/test_dataalert.py index 6f6f1683c..879f5ed00 100644 --- a/test/test_dataalert.py +++ b/test/test_dataalert.py @@ -1,112 +1,120 @@ -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -from ._utils import read_xml_asset - -GET_XML = "data_alerts_get.xml" -GET_BY_ID_XML = "data_alerts_get_by_id.xml" -ADD_USER_TO_ALERT = "data_alerts_add_user.xml" -UPDATE_XML = "data_alerts_update.xml" - - -class DataAlertTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.2" - - self.baseurl = self.server.data_alerts.baseurl - - def test_get(self) -> None: - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_alerts, pagination_item = self.server.data_alerts.get() - - self.assertEqual(1, pagination_item.total_available) - self.assertEqual("5ea59b45-e497-5673-8809-bfe213236f75", all_alerts[0].id) - self.assertEqual("Data Alert test", all_alerts[0].subject) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_alerts[0].creatorId) - self.assertEqual("2020-08-10T23:17:06Z", all_alerts[0].createdAt) - self.assertEqual("2020-08-10T23:17:06Z", all_alerts[0].updatedAt) - self.assertEqual("Daily", all_alerts[0].frequency) - self.assertEqual("true", all_alerts[0].public) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_alerts[0].owner_id) - self.assertEqual("Bob", all_alerts[0].owner_name) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", all_alerts[0].view_id) - self.assertEqual("ENDANGERED SAFARI", all_alerts[0].view_name) - self.assertEqual("6d13b0ca-043d-4d42-8c9d-3f3313ea3a00", all_alerts[0].workbook_id) - self.assertEqual("Safari stats", all_alerts[0].workbook_name) - self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", all_alerts[0].project_id) - self.assertEqual("Default", all_alerts[0].project_name) - - def test_get_by_id(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_XML) - with requests_mock.mock() as m: - m.get(self.baseurl + "/5ea59b45-e497-5673-8809-bfe213236f75", text=response_xml) - alert = self.server.data_alerts.get_by_id("5ea59b45-e497-5673-8809-bfe213236f75") - - self.assertTrue(isinstance(alert.recipients, list)) - self.assertEqual(len(alert.recipients), 1) - self.assertEqual(alert.recipients[0], "dd2239f6-ddf1-4107-981a-4cf94e415794") - - def test_update(self) -> None: - response_xml = read_xml_asset(UPDATE_XML) - with requests_mock.mock() as m: - m.put(self.baseurl + "/5ea59b45-e497-5673-8809-bfe213236f75", text=response_xml) - single_alert = TSC.DataAlertItem() - single_alert._id = "5ea59b45-e497-5673-8809-bfe213236f75" - single_alert._subject = "Data Alert test" - single_alert._frequency = "Daily" - single_alert._public = True - single_alert._owner_id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" - single_alert = self.server.data_alerts.update(single_alert) - - self.assertEqual("5ea59b45-e497-5673-8809-bfe213236f75", single_alert.id) - self.assertEqual("Data Alert test", single_alert.subject) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_alert.creatorId) - self.assertEqual("2020-08-10T23:17:06Z", single_alert.createdAt) - self.assertEqual("2020-08-10T23:17:06Z", single_alert.updatedAt) - self.assertEqual("Daily", single_alert.frequency) - self.assertEqual("true", single_alert.public) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_alert.owner_id) - self.assertEqual("Bob", single_alert.owner_name) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_alert.view_id) - self.assertEqual("ENDANGERED SAFARI", single_alert.view_name) - self.assertEqual("6d13b0ca-043d-4d42-8c9d-3f3313ea3a00", single_alert.workbook_id) - self.assertEqual("Safari stats", single_alert.workbook_name) - self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", single_alert.project_id) - self.assertEqual("Default", single_alert.project_name) - - def test_add_user_to_alert(self) -> None: - response_xml = read_xml_asset(ADD_USER_TO_ALERT) + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "data_alerts_get.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "data_alerts_get_by_id.xml" +ADD_USER_TO_ALERT = TEST_ASSET_DIR / "data_alerts_add_user.xml" +UPDATE_XML = TEST_ASSET_DIR / "data_alerts_update.xml" + + +@pytest.fixture(scope="function") +def server(): + server = TSC.Server("http://test", False) + + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.2" + + return server + + +def test_get(server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.data_alerts.baseurl, text=response_xml) + all_alerts, pagination_item = server.data_alerts.get() + + assert 1 == pagination_item.total_available + assert "5ea59b45-e497-5673-8809-bfe213236f75" == all_alerts[0].id + assert "Data Alert test" == all_alerts[0].subject + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_alerts[0].creatorId + assert "2020-08-10T23:17:06Z" == all_alerts[0].createdAt + assert "2020-08-10T23:17:06Z" == all_alerts[0].updatedAt + assert "Daily" == all_alerts[0].frequency + assert "true" == all_alerts[0].public + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_alerts[0].owner_id + assert "Bob" == all_alerts[0].owner_name + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == all_alerts[0].view_id + assert "ENDANGERED SAFARI" == all_alerts[0].view_name + assert "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" == all_alerts[0].workbook_id + assert "Safari stats" == all_alerts[0].workbook_name + assert "5241e88d-d384-4fd7-9c2f-648b5247efc5" == all_alerts[0].project_id + assert "Default" == all_alerts[0].project_name + + +def test_get_by_id(server) -> None: + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + m.get(server.data_alerts.baseurl + "/5ea59b45-e497-5673-8809-bfe213236f75", text=response_xml) + alert = server.data_alerts.get_by_id("5ea59b45-e497-5673-8809-bfe213236f75") + + assert isinstance(alert.recipients, list) + assert len(alert.recipients) == 1 + assert alert.recipients[0] == "dd2239f6-ddf1-4107-981a-4cf94e415794" + + +def test_update(server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.data_alerts.baseurl + "/5ea59b45-e497-5673-8809-bfe213236f75", text=response_xml) single_alert = TSC.DataAlertItem() - single_alert._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" - in_user = TSC.UserItem("Bob", TSC.UserItem.Roles.Explorer) - in_user._id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" - - with requests_mock.mock() as m: - m.post(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/users", text=response_xml) - - out_user = self.server.data_alerts.add_user_to_alert(single_alert, in_user) - - self.assertEqual(out_user.id, in_user.id) - self.assertEqual(out_user.name, in_user.name) - self.assertEqual(out_user.site_role, in_user.site_role) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) - self.server.data_alerts.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") - - def test_delete_user_from_alert(self) -> None: - alert_id = "5ea59b45-e497-5673-8809-bfe213236f75" - user_id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" - with requests_mock.mock() as m: - m.delete(self.baseurl + f"/{alert_id}/users/{user_id}", status_code=204) - self.server.data_alerts.delete_user_from_alert(alert_id, user_id) + single_alert._id = "5ea59b45-e497-5673-8809-bfe213236f75" + single_alert._subject = "Data Alert test" + single_alert._frequency = "Daily" + single_alert._public = True + single_alert._owner_id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" + single_alert = server.data_alerts.update(single_alert) + + assert "5ea59b45-e497-5673-8809-bfe213236f75" == single_alert.id + assert "Data Alert test" == single_alert.subject + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == single_alert.creatorId + assert "2020-08-10T23:17:06Z" == single_alert.createdAt + assert "2020-08-10T23:17:06Z" == single_alert.updatedAt + assert "Daily" == single_alert.frequency + assert "true" == single_alert.public + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == single_alert.owner_id + assert "Bob" == single_alert.owner_name + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == single_alert.view_id + assert "ENDANGERED SAFARI" == single_alert.view_name + assert "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" == single_alert.workbook_id + assert "Safari stats" == single_alert.workbook_name + assert "5241e88d-d384-4fd7-9c2f-648b5247efc5" == single_alert.project_id + assert "Default" == single_alert.project_name + + +def test_add_user_to_alert(server) -> None: + response_xml = ADD_USER_TO_ALERT.read_text() + single_alert = TSC.DataAlertItem() + single_alert._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + in_user = TSC.UserItem("Bob", TSC.UserItem.Roles.Explorer) + in_user._id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" + + with requests_mock.mock() as m: + m.post(server.data_alerts.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/users", text=response_xml) + + out_user = server.data_alerts.add_user_to_alert(single_alert, in_user) + + assert out_user.id == in_user.id + assert out_user.name == in_user.name + assert out_user.site_role == in_user.site_role + + +def test_delete(server) -> None: + with requests_mock.mock() as m: + m.delete(server.data_alerts.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) + server.data_alerts.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") + + +def test_delete_user_from_alert(server) -> None: + alert_id = "5ea59b45-e497-5673-8809-bfe213236f75" + user_id = "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" + with requests_mock.mock() as m: + m.delete(server.data_alerts.baseurl + f"/{alert_id}/users/{user_id}", status_code=204) + server.data_alerts.delete_user_from_alert(alert_id, user_id) diff --git a/test/test_database.py b/test/test_database.py index 3fd2c9a67..8eb03c737 100644 --- a/test/test_database.py +++ b/test/test_database.py @@ -1,113 +1,112 @@ -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -from ._utils import read_xml_asset, asset - -GET_XML = "database_get.xml" -POPULATE_PERMISSIONS_XML = "database_populate_permissions.xml" -UPDATE_XML = "database_update.xml" -GET_DQW_BY_CONTENT = "dqw_by_content_type.xml" - - -class DatabaseTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.5" - - self.baseurl = self.server.databases.baseurl - - def test_get(self): - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_databases, pagination_item = self.server.databases.get() - - self.assertEqual(5, pagination_item.total_available) - self.assertEqual("5ea59b45-e497-4827-8809-bfe213236f75", all_databases[0].id) - self.assertEqual("hyper", all_databases[0].connection_type) - self.assertEqual("hyper_0.hyper", all_databases[0].name) - - self.assertEqual("23591f2c-4802-4d6a-9e28-574a8ea9bc4c", all_databases[1].id) - self.assertEqual("sqlserver", all_databases[1].connection_type) - self.assertEqual("testv1", all_databases[1].name) - self.assertEqual("9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0", all_databases[1].contact_id) - self.assertEqual(True, all_databases[1].certified) - - def test_update(self): - response_xml = read_xml_asset(UPDATE_XML) - with requests_mock.mock() as m: - m.put(self.baseurl + "/23591f2c-4802-4d6a-9e28-574a8ea9bc4c", text=response_xml) - single_database = TSC.DatabaseItem("test") - single_database.contact_id = "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" - single_database._id = "23591f2c-4802-4d6a-9e28-574a8ea9bc4c" - single_database.certified = True - single_database.certification_note = "Test" - single_database = self.server.databases.update(single_database) - - self.assertEqual("23591f2c-4802-4d6a-9e28-574a8ea9bc4c", single_database.id) - self.assertEqual("9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0", single_database.contact_id) - self.assertEqual(True, single_database.certified) - self.assertEqual("Test", single_database.certification_note) - - def test_populate_permissions(self): - with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) - single_database = TSC.DatabaseItem("test") - single_database._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" - - self.server.databases.populate_permissions(single_database) - permissions = single_database.permissions - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "5e5e1978-71fa-11e4-87dd-7382f5c437af") - self.assertDictEqual( - permissions[0].capabilities, - { - TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - }, - ) - - self.assertEqual(permissions[1].grantee.tag_name, "user") - self.assertEqual(permissions[1].grantee.id, "7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - self.assertDictEqual( - permissions[1].capabilities, - { - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - }, - ) - - def test_populate_data_quality_warning(self): - with open(asset(GET_DQW_BY_CONTENT), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get( - self.server.databases._data_quality_warnings.baseurl + "/94441d26-9a52-4a42-b0fb-3f94792d1aac", - text=response_xml, - ) - single_database = TSC.DatabaseItem("test") - single_database._id = "94441d26-9a52-4a42-b0fb-3f94792d1aac" - - self.server.databases.populate_dqw(single_database) - dqws = single_database.dqws - first_dqw = dqws.pop() - self.assertEqual(first_dqw.id, "c2e0e406-84fb-4f4e-9998-f20dd9306710") - self.assertEqual(first_dqw.warning_type, "WARNING") - self.assertEqual(first_dqw.message, "Hello, World!") - self.assertEqual(first_dqw.owner_id, "eddc8c5f-6af0-40be-b6b0-2c790290a43f") - self.assertEqual(first_dqw.active, True) - self.assertEqual(first_dqw.severe, True) - self.assertEqual(str(first_dqw.created_at), "2021-04-09 18:39:54+00:00") - self.assertEqual(str(first_dqw.updated_at), "2021-04-09 18:39:54+00:00") - - def test_delete(self): - with requests_mock.mock() as m: - m.delete(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) - self.server.databases.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "database_get.xml" +POPULATE_PERMISSIONS_XML = TEST_ASSET_DIR / "database_populate_permissions.xml" +UPDATE_XML = TEST_ASSET_DIR / "database_update.xml" +GET_DQW_BY_CONTENT = TEST_ASSET_DIR / "dqw_by_content_type.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.5" + + return server + + +def test_get(server): + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.databases.baseurl, text=response_xml) + all_databases, pagination_item = server.databases.get() + + assert 5 == pagination_item.total_available + assert "5ea59b45-e497-4827-8809-bfe213236f75" == all_databases[0].id + assert "hyper" == all_databases[0].connection_type + assert "hyper_0.hyper" == all_databases[0].name + + assert "23591f2c-4802-4d6a-9e28-574a8ea9bc4c" == all_databases[1].id + assert "sqlserver" == all_databases[1].connection_type + assert "testv1" == all_databases[1].name + assert "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" == all_databases[1].contact_id + assert all_databases[1].certified + + +def test_update(server): + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.databases.baseurl + "/23591f2c-4802-4d6a-9e28-574a8ea9bc4c", text=response_xml) + single_database = TSC.DatabaseItem("test") + single_database.contact_id = "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" + single_database._id = "23591f2c-4802-4d6a-9e28-574a8ea9bc4c" + single_database.certified = True + single_database.certification_note = "Test" + single_database = server.databases.update(single_database) + + assert "23591f2c-4802-4d6a-9e28-574a8ea9bc4c" == single_database.id + assert "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" == single_database.contact_id + assert single_database.certified + assert "Test" == single_database.certification_note + + +def test_populate_permissions(server): + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.databases.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) + single_database = TSC.DatabaseItem("test") + single_database._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + + server.databases.populate_permissions(single_database) + permissions = single_database.permissions + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "5e5e1978-71fa-11e4-87dd-7382f5c437af" + assert permissions[0].capabilities == { + TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + } + + assert permissions[1].grantee.tag_name == "user" + assert permissions[1].grantee.id == "7c37ee24-c4b1-42b6-a154-eaeab7ee330a" + assert permissions[1].capabilities == { + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + } + + +def test_populate_data_quality_warning(server): + response_xml = GET_DQW_BY_CONTENT.read_text() + with requests_mock.mock() as m: + m.get( + server.databases._data_quality_warnings.baseurl + "/94441d26-9a52-4a42-b0fb-3f94792d1aac", + text=response_xml, + ) + single_database = TSC.DatabaseItem("test") + single_database._id = "94441d26-9a52-4a42-b0fb-3f94792d1aac" + + server.databases.populate_dqw(single_database) + dqws = single_database.dqws + first_dqw = dqws.pop() + assert first_dqw.id == "c2e0e406-84fb-4f4e-9998-f20dd9306710" + assert first_dqw.warning_type == "WARNING" + assert first_dqw.message, "Hello == World!" + assert first_dqw.owner_id == "eddc8c5f-6af0-40be-b6b0-2c790290a43f" + assert first_dqw.active + assert first_dqw.severe + assert str(first_dqw.created_at) == "2021-04-09 18:39:54+00:00" + assert str(first_dqw.updated_at) == "2021-04-09 18:39:54+00:00" + + +def test_delete(server): + with requests_mock.mock() as m: + m.delete(server.databases.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) + server.databases.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") diff --git a/test/test_datasource.py b/test/test_datasource.py index a604ba8b0..56eb11ab7 100644 --- a/test/test_datasource.py +++ b/test/test_datasource.py @@ -1,12 +1,14 @@ +from io import BytesIO import os +from pathlib import Path import tempfile -import unittest -from io import BytesIO from typing import Optional +import unittest from zipfile import ZipFile -import requests_mock from defusedxml.ElementTree import fromstring +import pytest +import requests_mock import tableauserverclient as TSC from tableauserverclient import ConnectionItem @@ -14,759 +16,892 @@ from tableauserverclient.server.endpoint.exceptions import InternalServerError from tableauserverclient.server.endpoint.fileuploads_endpoint import Fileuploads from tableauserverclient.server.request_factory import RequestFactory -from ._utils import read_xml_asset, read_xml_assets, asset - -ADD_TAGS_XML = "datasource_add_tags.xml" -GET_XML = "datasource_get.xml" -GET_EMPTY_XML = "datasource_get_empty.xml" -GET_BY_ID_XML = "datasource_get_by_id.xml" -GET_XML_ALL_FIELDS = "datasource_get_all_fields.xml" -POPULATE_CONNECTIONS_XML = "datasource_populate_connections.xml" -POPULATE_PERMISSIONS_XML = "datasource_populate_permissions.xml" -PUBLISH_XML = "datasource_publish.xml" -PUBLISH_XML_ASYNC = "datasource_publish_async.xml" -REFRESH_XML = "datasource_refresh.xml" -REVISION_XML = "datasource_revision.xml" -UPDATE_XML = "datasource_update.xml" -UPDATE_HYPER_DATA_XML = "datasource_data_update.xml" -UPDATE_CONNECTION_XML = "datasource_connection_update.xml" - - -class DatasourceTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.datasources.baseurl - - def test_get(self) -> None: - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_datasources, pagination_item = self.server.datasources.get() - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("e76a1461-3b1d-4588-bf1b-17551a879ad9", all_datasources[0].id) - self.assertEqual("dataengine", all_datasources[0].datasource_type) - self.assertEqual("SampleDsDescription", all_datasources[0].description) - self.assertEqual("SampleDS", all_datasources[0].content_url) - self.assertEqual(4096, all_datasources[0].size) - self.assertEqual("2016-08-11T21:22:40Z", format_datetime(all_datasources[0].created_at)) - self.assertEqual("2016-08-11T21:34:17Z", format_datetime(all_datasources[0].updated_at)) - self.assertEqual("default", all_datasources[0].project_name) - self.assertEqual("SampleDS", all_datasources[0].name) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_datasources[0].project_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_datasources[0].owner_id) - self.assertEqual("https://web.com", all_datasources[0].webpage_url) - self.assertFalse(all_datasources[0].encrypt_extracts) - self.assertTrue(all_datasources[0].has_extracts) - self.assertFalse(all_datasources[0].use_remote_query_agent) - - self.assertEqual("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", all_datasources[1].id) - self.assertEqual("dataengine", all_datasources[1].datasource_type) - self.assertEqual("description Sample", all_datasources[1].description) - self.assertEqual("Sampledatasource", all_datasources[1].content_url) - self.assertEqual(10240, all_datasources[1].size) - self.assertEqual("2016-08-04T21:31:55Z", format_datetime(all_datasources[1].created_at)) - self.assertEqual("2016-08-04T21:31:55Z", format_datetime(all_datasources[1].updated_at)) - self.assertEqual("default", all_datasources[1].project_name) - self.assertEqual("Sample datasource", all_datasources[1].name) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_datasources[1].project_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_datasources[1].owner_id) - self.assertEqual({"world", "indicators", "sample"}, all_datasources[1].tags) - self.assertEqual("https://page.com", all_datasources[1].webpage_url) - self.assertTrue(all_datasources[1].encrypt_extracts) - self.assertFalse(all_datasources[1].has_extracts) - self.assertTrue(all_datasources[1].use_remote_query_agent) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.datasources.get) - - def test_get_empty(self) -> None: - response_xml = read_xml_asset(GET_EMPTY_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_datasources, pagination_item = self.server.datasources.get() - self.assertEqual(0, pagination_item.total_available) - self.assertEqual([], all_datasources) +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +ADD_TAGS_XML = TEST_ASSET_DIR / "datasource_add_tags.xml" +GET_XML = TEST_ASSET_DIR / "datasource_get.xml" +GET_EMPTY_XML = TEST_ASSET_DIR / "datasource_get_empty.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "datasource_get_by_id.xml" +GET_XML_ALL_FIELDS = TEST_ASSET_DIR / "datasource_get_all_fields.xml" +GET_NO_OWNER = TEST_ASSET_DIR / "datasource_get_no_owner.xml" +POPULATE_CONNECTIONS_XML = TEST_ASSET_DIR / "datasource_populate_connections.xml" +POPULATE_PERMISSIONS_XML = TEST_ASSET_DIR / "datasource_populate_permissions.xml" +PUBLISH_XML = TEST_ASSET_DIR / "datasource_publish.xml" +PUBLISH_XML_ASYNC = TEST_ASSET_DIR / "datasource_publish_async.xml" +REFRESH_XML = TEST_ASSET_DIR / "datasource_refresh.xml" +REVISION_XML = TEST_ASSET_DIR / "datasource_revision.xml" +UPDATE_XML = TEST_ASSET_DIR / "datasource_update.xml" +UPDATE_HYPER_DATA_XML = TEST_ASSET_DIR / "datasource_data_update.xml" +UPDATE_CONNECTION_XML = TEST_ASSET_DIR / "datasource_connection_update.xml" +UPDATE_CONNECTIONS_XML = TEST_ASSET_DIR / "datasource_connections_update.xml" + + +@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_get(server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.datasources.baseurl, text=response_xml) + all_datasources, pagination_item = server.datasources.get() + + assert 2 == pagination_item.total_available + assert "e76a1461-3b1d-4588-bf1b-17551a879ad9" == all_datasources[0].id + assert "dataengine" == all_datasources[0].datasource_type + assert "SampleDsDescription" == all_datasources[0].description + assert "SampleDS" == all_datasources[0].content_url + assert 4096 == all_datasources[0].size + assert "2016-08-11T21:22:40Z" == format_datetime(all_datasources[0].created_at) + assert "2016-08-11T21:34:17Z" == format_datetime(all_datasources[0].updated_at) + assert "default" == all_datasources[0].project_name + assert "SampleDS" == all_datasources[0].name + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == all_datasources[0].project_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_datasources[0].owner_id + assert "https://web.com" == all_datasources[0].webpage_url + assert not all_datasources[0].encrypt_extracts + assert all_datasources[0].has_extracts + assert not all_datasources[0].use_remote_query_agent + + assert "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" == all_datasources[1].id + assert "dataengine" == all_datasources[1].datasource_type + assert "description Sample" == all_datasources[1].description + assert "Sampledatasource" == all_datasources[1].content_url + assert 10240 == all_datasources[1].size + assert "2016-08-04T21:31:55Z" == format_datetime(all_datasources[1].created_at) + assert "2016-08-04T21:31:55Z" == format_datetime(all_datasources[1].updated_at) + assert "default" == all_datasources[1].project_name + assert "Sample datasource" == all_datasources[1].name + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == all_datasources[1].project_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_datasources[1].owner_id + assert {"world", "indicators", "sample"} == all_datasources[1].tags + assert "https://page.com" == all_datasources[1].webpage_url + assert all_datasources[1].encrypt_extracts + assert not all_datasources[1].has_extracts + assert all_datasources[1].use_remote_query_agent + + +def test_get_before_signin(server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.datasources.get() + + +def test_get_empty(server) -> None: + response_xml = GET_EMPTY_XML.read_text() + with requests_mock.mock() as m: + m.get(server.datasources.baseurl, text=response_xml) + all_datasources, pagination_item = server.datasources.get() + + assert 0 == pagination_item.total_available + assert [] == all_datasources + + +def test_get_by_id(server) -> None: + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + m.get(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) + single_datasource = server.datasources.get_by_id("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") + + assert "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" == single_datasource.id + assert "dataengine" == single_datasource.datasource_type + assert "abc description xyz" == single_datasource.description + assert "Sampledatasource" == single_datasource.content_url + assert "2016-08-04T21:31:55Z" == format_datetime(single_datasource.created_at) + assert "2016-08-04T21:31:55Z" == format_datetime(single_datasource.updated_at) + assert "default" == single_datasource.project_name + assert "Sample datasource" == single_datasource.name + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == single_datasource.project_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == single_datasource.owner_id + assert {"world", "indicators", "sample"} == single_datasource.tags + assert TSC.DatasourceItem.AskDataEnablement.SiteDefault == single_datasource.ask_data_enablement + + +def test_update(server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "Sample datasource") + single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_datasource._content_url = "Sampledatasource" + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + single_datasource.certified = True + single_datasource.certification_note = "Warning, here be dragons." + updated_datasource = server.datasources.update(single_datasource) + + assert updated_datasource.id == single_datasource.id + assert updated_datasource.name == single_datasource.name + assert updated_datasource.content_url == single_datasource.content_url + assert updated_datasource.project_id == single_datasource.project_id + assert updated_datasource.owner_id == single_datasource.owner_id + assert updated_datasource.certified == single_datasource.certified + assert updated_datasource.certification_note == single_datasource.certification_note + + +def test_update_copy_fields(server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "test") + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + single_datasource._project_name = "Tester" + updated_datasource = server.datasources.update(single_datasource) + + assert single_datasource.tags == updated_datasource.tags + assert single_datasource._project_name == updated_datasource._project_name + + +def test_update_tags(server) -> None: + add_tags_xml = ADD_TAGS_XML.read_text() + update_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.delete(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/b", status_code=204) + m.delete(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/d", status_code=204) + m.put(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags", text=add_tags_xml) + m.put(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=update_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74") + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + single_datasource._initial_tags.update(["a", "b", "c", "d"]) + single_datasource.tags.update(["a", "c", "e"]) + updated_datasource = server.datasources.update(single_datasource) + + assert single_datasource.tags == updated_datasource.tags + assert single_datasource._initial_tags == updated_datasource._initial_tags + + +def test_populate_connections(server) -> None: + response_xml = POPULATE_CONNECTIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=response_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "test") + single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + server.datasources.populate_connections(single_datasource) + assert "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" == single_datasource.id + connections: Optional[list[ConnectionItem]] = single_datasource.connections + + assert connections is not None + ds1, ds2 = connections + assert "be786ae0-d2bf-4a4b-9b34-e2de8d2d4488" == ds1.id + assert "textscan" == ds1.connection_type + assert "forty-two.net" == ds1.server_address + assert "duo" == ds1.username + assert True == ds1.embed_password + assert ds1.datasource_id == single_datasource.id + assert single_datasource.name == ds1.datasource_name + assert "970e24bc-e200-4841-a3e9-66e7d122d77e" == ds2.id + assert "sqlserver" == ds2.connection_type + assert "database.com" == ds2.server_address + assert "heero" == ds2.username + assert False == ds2.embed_password + assert ds2.datasource_id == single_datasource.id + assert single_datasource.name == ds2.datasource_name + + +def test_update_connection(server) -> None: + populate_xml = POPULATE_CONNECTIONS_XML.read_text() + response_xml = UPDATE_CONNECTION_XML.read_text() + + with requests_mock.mock() as m: + m.get(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=populate_xml) + m.put( + server.datasources.baseurl + + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/be786ae0-d2bf-4a4b-9b34-e2de8d2d4488", + text=response_xml, + ) + single_datasource = TSC.DatasourceItem("be786ae0-d2bf-4a4b-9b34-e2de8d2d4488") + single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + server.datasources.populate_connections(single_datasource) - def test_get_by_id(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_XML) - with requests_mock.mock() as m: - m.get(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) - single_datasource = self.server.datasources.get_by_id("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") - - self.assertEqual("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", single_datasource.id) - self.assertEqual("dataengine", single_datasource.datasource_type) - self.assertEqual("abc description xyz", single_datasource.description) - self.assertEqual("Sampledatasource", single_datasource.content_url) - self.assertEqual("2016-08-04T21:31:55Z", format_datetime(single_datasource.created_at)) - self.assertEqual("2016-08-04T21:31:55Z", format_datetime(single_datasource.updated_at)) - self.assertEqual("default", single_datasource.project_name) - self.assertEqual("Sample datasource", single_datasource.name) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", single_datasource.project_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_datasource.owner_id) - self.assertEqual({"world", "indicators", "sample"}, single_datasource.tags) - self.assertEqual(TSC.DatasourceItem.AskDataEnablement.SiteDefault, single_datasource.ask_data_enablement) - - def test_update(self) -> None: - response_xml = read_xml_asset(UPDATE_XML) - with requests_mock.mock() as m: - m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) - single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "Sample datasource") - single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_datasource._content_url = "Sampledatasource" - single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - single_datasource.certified = True - single_datasource.certification_note = "Warning, here be dragons." - updated_datasource = self.server.datasources.update(single_datasource) - - self.assertEqual(updated_datasource.id, single_datasource.id) - self.assertEqual(updated_datasource.name, single_datasource.name) - self.assertEqual(updated_datasource.content_url, single_datasource.content_url) - self.assertEqual(updated_datasource.project_id, single_datasource.project_id) - self.assertEqual(updated_datasource.owner_id, single_datasource.owner_id) - self.assertEqual(updated_datasource.certified, single_datasource.certified) - self.assertEqual(updated_datasource.certification_note, single_datasource.certification_note) - - def test_update_copy_fields(self) -> None: - with open(asset(UPDATE_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) - single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "test") - single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - single_datasource._project_name = "Tester" - updated_datasource = self.server.datasources.update(single_datasource) + connection = single_datasource.connections[0] # type: ignore[index] + connection.server_address = "bar" + connection.server_port = "9876" + connection.username = "foo" + new_connection = server.datasources.update_connection(single_datasource, connection) + assert connection.id == new_connection.id + assert connection.connection_type == new_connection.connection_type + assert "bar" == new_connection.server_address + assert "9876" == new_connection.server_port + assert "foo" == new_connection.username - self.assertEqual(single_datasource.tags, updated_datasource.tags) - self.assertEqual(single_datasource._project_name, updated_datasource._project_name) - def test_update_tags(self) -> None: - add_tags_xml, update_xml = read_xml_assets(ADD_TAGS_XML, UPDATE_XML) - with requests_mock.mock() as m: - m.delete(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/b", status_code=204) - m.delete(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags/d", status_code=204) - m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/tags", text=add_tags_xml) - m.put(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=update_xml) - single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74") - single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - single_datasource._initial_tags.update(["a", "b", "c", "d"]) - single_datasource.tags.update(["a", "c", "e"]) - updated_datasource = self.server.datasources.update(single_datasource) - - self.assertEqual(single_datasource.tags, updated_datasource.tags) - self.assertEqual(single_datasource._initial_tags, updated_datasource._initial_tags) - - def test_populate_connections(self) -> None: - response_xml = read_xml_asset(POPULATE_CONNECTIONS_XML) - with requests_mock.mock() as m: - m.get(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=response_xml) - single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "test") - single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - self.server.datasources.populate_connections(single_datasource) - self.assertEqual("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", single_datasource.id) - connections: Optional[list[ConnectionItem]] = single_datasource.connections - - self.assertIsNotNone(connections) - assert connections is not None - ds1, ds2 = connections - self.assertEqual("be786ae0-d2bf-4a4b-9b34-e2de8d2d4488", ds1.id) - self.assertEqual("textscan", ds1.connection_type) - self.assertEqual("forty-two.net", ds1.server_address) - self.assertEqual("duo", ds1.username) - self.assertEqual(True, ds1.embed_password) - self.assertEqual(ds1.datasource_id, single_datasource.id) - self.assertEqual(single_datasource.name, ds1.datasource_name) - self.assertEqual("970e24bc-e200-4841-a3e9-66e7d122d77e", ds2.id) - self.assertEqual("sqlserver", ds2.connection_type) - self.assertEqual("database.com", ds2.server_address) - self.assertEqual("heero", ds2.username) - self.assertEqual(False, ds2.embed_password) - self.assertEqual(ds2.datasource_id, single_datasource.id) - self.assertEqual(single_datasource.name, ds2.datasource_name) - - def test_update_connection(self) -> None: - populate_xml, response_xml = read_xml_assets(POPULATE_CONNECTIONS_XML, UPDATE_CONNECTION_XML) +def test_update_connections(server) -> None: + populate_xml = POPULATE_CONNECTIONS_XML.read_text() + response_xml = UPDATE_CONNECTIONS_XML.read_text() - with requests_mock.mock() as m: - m.get(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=populate_xml) - m.put( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/be786ae0-d2bf-4a4b-9b34-e2de8d2d4488", - text=response_xml, - ) - single_datasource = TSC.DatasourceItem("be786ae0-d2bf-4a4b-9b34-e2de8d2d4488") - single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - self.server.datasources.populate_connections(single_datasource) - - connection = single_datasource.connections[0] # type: ignore[index] - connection.server_address = "bar" - connection.server_port = "9876" - connection.username = "foo" - new_connection = self.server.datasources.update_connection(single_datasource, connection) - self.assertEqual(connection.id, new_connection.id) - self.assertEqual(connection.connection_type, new_connection.connection_type) - self.assertEqual("bar", new_connection.server_address) - self.assertEqual("9876", new_connection.server_port) - self.assertEqual("foo", new_connection.username) - - def test_populate_permissions(self) -> None: - with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) - single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "test") - single_datasource._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" - - self.server.datasources.populate_permissions(single_datasource) - permissions = single_datasource.permissions - - self.assertEqual(permissions[0].grantee.tag_name, "group") # type: ignore[index] - self.assertEqual(permissions[0].grantee.id, "5e5e1978-71fa-11e4-87dd-7382f5c437af") # type: ignore[index] - self.assertDictEqual( - permissions[0].capabilities, # type: ignore[index] - { - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - }, - ) + with requests_mock.Mocker() as m: - self.assertEqual(permissions[1].grantee.tag_name, "user") # type: ignore[index] - self.assertEqual(permissions[1].grantee.id, "7c37ee24-c4b1-42b6-a154-eaeab7ee330a") # type: ignore[index] - self.assertDictEqual( - permissions[1].capabilities, # type: ignore[index] - { - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - }, - ) + datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + connection_luids = ["be786ae0-d2bf-4a4b-9b34-e2de8d2d4488", "a1b2c3d4-e5f6-7a8b-9c0d-123456789abc"] - def test_publish(self) -> None: - response_xml = read_xml_asset(PUBLISH_XML) - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") - publish_mode = self.server.PublishMode.CreateNew - - new_datasource = self.server.datasources.publish(new_datasource, asset("SampleDS.tds"), mode=publish_mode) - - self.assertEqual("e76a1461-3b1d-4588-bf1b-17551a879ad9", new_datasource.id) - self.assertEqual("SampleDS", new_datasource.name) - self.assertEqual("SampleDS", new_datasource.content_url) - self.assertEqual("dataengine", new_datasource.datasource_type) - self.assertEqual("2016-08-11T21:22:40Z", format_datetime(new_datasource.created_at)) - self.assertEqual("2016-08-17T23:37:08Z", format_datetime(new_datasource.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_datasource.project_id) - self.assertEqual("default", new_datasource.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_datasource.owner_id) - - def test_publish_a_non_packaged_file_object(self) -> None: - response_xml = read_xml_asset(PUBLISH_XML) - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") - publish_mode = self.server.PublishMode.CreateNew - - with open(asset("SampleDS.tds"), "rb") as file_object: - new_datasource = self.server.datasources.publish(new_datasource, file_object, mode=publish_mode) - - self.assertEqual("e76a1461-3b1d-4588-bf1b-17551a879ad9", new_datasource.id) - self.assertEqual("SampleDS", new_datasource.name) - self.assertEqual("SampleDS", new_datasource.content_url) - self.assertEqual("dataengine", new_datasource.datasource_type) - self.assertEqual("2016-08-11T21:22:40Z", format_datetime(new_datasource.created_at)) - self.assertEqual("2016-08-17T23:37:08Z", format_datetime(new_datasource.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_datasource.project_id) - self.assertEqual("default", new_datasource.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_datasource.owner_id) - - def test_publish_a_packaged_file_object(self) -> None: - response_xml = read_xml_asset(PUBLISH_XML) - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") - publish_mode = self.server.PublishMode.CreateNew - - # Create a dummy tdsx file in memory - with BytesIO() as zip_archive: - with ZipFile(zip_archive, "w") as zf: - zf.write(asset("SampleDS.tds")) - - zip_archive.seek(0) - - new_datasource = self.server.datasources.publish(new_datasource, zip_archive, mode=publish_mode) - - self.assertEqual("e76a1461-3b1d-4588-bf1b-17551a879ad9", new_datasource.id) - self.assertEqual("SampleDS", new_datasource.name) - self.assertEqual("SampleDS", new_datasource.content_url) - self.assertEqual("dataengine", new_datasource.datasource_type) - self.assertEqual("2016-08-11T21:22:40Z", format_datetime(new_datasource.created_at)) - self.assertEqual("2016-08-17T23:37:08Z", format_datetime(new_datasource.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_datasource.project_id) - self.assertEqual("default", new_datasource.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_datasource.owner_id) - - def test_publish_async(self) -> None: - self.server.version = "3.0" - baseurl = self.server.datasources.baseurl - response_xml = read_xml_asset(PUBLISH_XML_ASYNC) - with requests_mock.mock() as m: - m.post(baseurl, text=response_xml) - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") - publish_mode = self.server.PublishMode.CreateNew + datasource = TSC.DatasourceItem(datasource_id) + datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + server.version = "3.26" - new_job = self.server.datasources.publish( - new_datasource, asset("SampleDS.tds"), mode=publish_mode, as_job=True - ) + url = f"{server.baseurl}/{datasource.id}/connections" + m.get( + "http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", + text=populate_xml, + ) + m.put( + "http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", + text=response_xml, + ) - self.assertEqual("9a373058-af5f-4f83-8662-98b3e0228a73", new_job.id) - self.assertEqual("PublishDatasource", new_job.type) - self.assertEqual("0", new_job.progress) - self.assertEqual("2018-06-30T00:54:54Z", format_datetime(new_job.created_at)) - self.assertEqual(1, new_job.finish_code) + print("BASEURL:", server.baseurl) + print("Calling PUT on:", f"{server.baseurl}/{datasource.id}/connections") - def test_publish_unnamed_file_object(self) -> None: - new_datasource = TSC.DatasourceItem("test") - publish_mode = self.server.PublishMode.CreateNew + connection_items = server.datasources.update_connections( + datasource_item=datasource, + connection_luids=connection_luids, + authentication_type="auth-keypair", + username="testuser", + password="testpass", + embed_password=True, + ) + updated_ids = [conn.id for conn in connection_items] + + assert updated_ids == connection_luids + assert "auth-keypair" == connection_items[0].auth_type + + +def test_populate_permissions(server) -> None: + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.datasources.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "test") + single_datasource._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + + server.datasources.populate_permissions(single_datasource) + permissions = single_datasource.permissions + + assert permissions is not None + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "5e5e1978-71fa-11e4-87dd-7382f5c437af" + assert permissions[0].capabilities == { # type: ignore[index] + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + } + + assert permissions[1].grantee.tag_name == "user" + assert permissions[1].grantee.id == "7c37ee24-c4b1-42b6-a154-eaeab7ee330a" + assert permissions[1].capabilities == { # type: ignore[index] + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + } + + +def test_publish(server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.datasources.baseurl, text=response_xml) + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") + publish_mode = server.PublishMode.CreateNew + + new_datasource = server.datasources.publish(new_datasource, TEST_ASSET_DIR / "SampleDS.tds", mode=publish_mode) + + assert "e76a1461-3b1d-4588-bf1b-17551a879ad9" == new_datasource.id + assert "SampleDS" == new_datasource.name + assert "SampleDS" == new_datasource.content_url + assert "dataengine" == new_datasource.datasource_type + assert "2016-08-11T21:22:40Z" == format_datetime(new_datasource.created_at) + assert "2016-08-17T23:37:08Z" == format_datetime(new_datasource.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_datasource.project_id + assert "default" == new_datasource.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_datasource.owner_id + + +def test_publish_a_non_packaged_file_object(server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.datasources.baseurl, text=response_xml) + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") + publish_mode = server.PublishMode.CreateNew + + with open(TEST_ASSET_DIR / "SampleDS.tds", "rb") as file_object: + new_datasource = server.datasources.publish(new_datasource, file_object, mode=publish_mode) + + assert "e76a1461-3b1d-4588-bf1b-17551a879ad9" == new_datasource.id + assert "SampleDS" == new_datasource.name + assert "SampleDS" == new_datasource.content_url + assert "dataengine" == new_datasource.datasource_type + assert "2016-08-11T21:22:40Z" == format_datetime(new_datasource.created_at) + assert "2016-08-17T23:37:08Z" == format_datetime(new_datasource.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_datasource.project_id + assert "default" == new_datasource.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_datasource.owner_id + + +def test_publish_a_packaged_file_object(server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.datasources.baseurl, text=response_xml) + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") + publish_mode = server.PublishMode.CreateNew + + # Create a dummy tdsx file in memory + with BytesIO() as zip_archive: + with ZipFile(zip_archive, "w") as zf: + zf.write(str(TEST_ASSET_DIR / "SampleDS.tds"), arcname="SampleDS.tds") + + zip_archive.seek(0) + + new_datasource = server.datasources.publish(new_datasource, zip_archive, mode=publish_mode) + + assert "e76a1461-3b1d-4588-bf1b-17551a879ad9" == new_datasource.id + assert "SampleDS" == new_datasource.name + assert "SampleDS" == new_datasource.content_url + assert "dataengine" == new_datasource.datasource_type + assert "2016-08-11T21:22:40Z" == format_datetime(new_datasource.created_at) + assert "2016-08-17T23:37:08Z" == format_datetime(new_datasource.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_datasource.project_id + assert "default" == new_datasource.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_datasource.owner_id + + +def test_publish_async(server) -> None: + server.version = "3.0" + baseurl = server.datasources.baseurl + response_xml = PUBLISH_XML_ASYNC.read_text() + with requests_mock.mock() as m: + m.post(baseurl, text=response_xml) + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "SampleDS") + publish_mode = server.PublishMode.CreateNew + + new_job = server.datasources.publish( + new_datasource, TEST_ASSET_DIR / "SampleDS.tds", mode=publish_mode, as_job=True + ) - with open(asset("SampleDS.tds"), "rb") as file_object: - self.assertRaises(ValueError, self.server.datasources.publish, new_datasource, file_object, publish_mode) + assert "9a373058-af5f-4f83-8662-98b3e0228a73" == new_job.id + assert "PublishDatasource" == new_job.type + assert "0" == new_job.progress + assert "2018-06-30T00:54:54Z" == format_datetime(new_job.created_at) + assert 1 == new_job.finish_code - def test_refresh_id(self) -> None: - self.server.version = "2.8" - self.baseurl = self.server.datasources.baseurl - response_xml = read_xml_asset(REFRESH_XML) - with requests_mock.mock() as m: - m.post(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/refresh", status_code=202, text=response_xml) - new_job = self.server.datasources.refresh("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") - - self.assertEqual("7c3d599e-949f-44c3-94a1-f30ba85757e4", new_job.id) - self.assertEqual("RefreshExtract", new_job.type) - self.assertEqual(None, new_job.progress) - self.assertEqual("2020-03-05T22:05:32Z", format_datetime(new_job.created_at)) - self.assertEqual(-1, new_job.finish_code) - - def test_refresh_object(self) -> None: - self.server.version = "2.8" - self.baseurl = self.server.datasources.baseurl - datasource = TSC.DatasourceItem("") - datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - response_xml = read_xml_asset(REFRESH_XML) - with requests_mock.mock() as m: - m.post(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/refresh", status_code=202, text=response_xml) - new_job = self.server.datasources.refresh(datasource) - - # We only check the `id`; remaining fields are already tested in `test_refresh_id` - self.assertEqual("7c3d599e-949f-44c3-94a1-f30ba85757e4", new_job.id) - - def test_datasource_refresh_request_empty(self) -> None: - self.server.version = "2.8" - self.baseurl = self.server.datasources.baseurl - item = TSC.DatasourceItem("") - item._id = "1234" - text = read_xml_asset(REFRESH_XML) - - def match_request_body(request): - try: - root = fromstring(request.body) - assert root.tag == "tsRequest" - assert len(root) == 0 - return True - except Exception: - return False - with requests_mock.mock() as m: - m.post(f"{self.baseurl}/1234/refresh", text=text, additional_matcher=match_request_body) +def test_publish_unnamed_file_object(server) -> None: + new_datasource = TSC.DatasourceItem("test") + publish_mode = server.PublishMode.CreateNew - def test_update_hyper_data_datasource_object(self) -> None: - """Calling `update_hyper_data` with a `DatasourceItem` should update that datasource""" - self.server.version = "3.13" - self.baseurl = self.server.datasources.baseurl + with open(TEST_ASSET_DIR / "SampleDS.tds", "rb") as file_object: + with pytest.raises(ValueError): + server.datasources.publish(new_datasource, file_object, publish_mode) - datasource = TSC.DatasourceItem("") - datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - response_xml = read_xml_asset(UPDATE_HYPER_DATA_XML) - with requests_mock.mock() as m: - m.patch( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/data", - status_code=202, - headers={"requestid": "test_id"}, - text=response_xml, - ) - new_job = self.server.datasources.update_hyper_data(datasource, request_id="test_id", actions=[]) - - self.assertEqual("5c0ba560-c959-424e-b08a-f32ef0bfb737", new_job.id) - self.assertEqual("UpdateUploadedFile", new_job.type) - self.assertEqual(None, new_job.progress) - self.assertEqual("2021-09-18T09:40:12Z", format_datetime(new_job.created_at)) - self.assertEqual(-1, new_job.finish_code) - - def test_update_hyper_data_connection_object(self) -> None: - """Calling `update_hyper_data` with a `ConnectionItem` should update that connection""" - self.server.version = "3.13" - self.baseurl = self.server.datasources.baseurl - - connection = TSC.ConnectionItem() - connection._datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - connection._id = "7ecaccd8-39b0-4875-a77d-094f6e930019" - response_xml = read_xml_asset(UPDATE_HYPER_DATA_XML) - with requests_mock.mock() as m: - m.patch( - self.baseurl - + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/7ecaccd8-39b0-4875-a77d-094f6e930019/data", - status_code=202, - headers={"requestid": "test_id"}, - text=response_xml, - ) - new_job = self.server.datasources.update_hyper_data(connection, request_id="test_id", actions=[]) - # We only check the `id`; remaining fields are already tested in `test_update_hyper_data_datasource_object` - self.assertEqual("5c0ba560-c959-424e-b08a-f32ef0bfb737", new_job.id) +def test_refresh_id(server) -> None: + server.version = "2.8" + response_xml = REFRESH_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/refresh", + status_code=202, + text=response_xml, + ) + new_job = server.datasources.refresh("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") + + assert "7c3d599e-949f-44c3-94a1-f30ba85757e4" == new_job.id + assert "RefreshExtract" == new_job.type + assert None == new_job.progress + assert "2020-03-05T22:05:32Z" == format_datetime(new_job.created_at) + assert -1 == new_job.finish_code + + +def test_refresh_object(server) -> None: + server.version = "2.8" + datasource = TSC.DatasourceItem("") + datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + response_xml = REFRESH_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/refresh", + status_code=202, + text=response_xml, + ) + new_job = server.datasources.refresh(datasource) + + # We only check the `id`; remaining fields are already tested in `test_refresh_id` + assert "7c3d599e-949f-44c3-94a1-f30ba85757e4" == new_job.id + + +def test_datasource_refresh_request_empty(server) -> None: + server.version = "2.8" + item = TSC.DatasourceItem("") + item._id = "1234" + text = REFRESH_XML.read_text() + + def match_request_body(request): + try: + root = fromstring(request.body) + assert root.tag == "tsRequest" + assert len(root) == 0 + return True + except Exception: + return False + + with requests_mock.mock() as m: + m.post(f"{server.datasources.baseurl}/1234/refresh", text=text, additional_matcher=match_request_body) + + +def test_update_hyper_data_datasource_object(server) -> None: + """Calling `update_hyper_data` with a `DatasourceItem` should update that datasource""" + server.version = "3.13" + + datasource = TSC.DatasourceItem("") + datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + response_xml = UPDATE_HYPER_DATA_XML.read_text() + with requests_mock.mock() as m: + m.patch( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/data", + status_code=202, + headers={"requestid": "test_id"}, + text=response_xml, + ) + new_job = server.datasources.update_hyper_data(datasource, request_id="test_id", actions=[]) + + assert "5c0ba560-c959-424e-b08a-f32ef0bfb737" == new_job.id + assert "UpdateUploadedFile" == new_job.type + assert None == new_job.progress + assert "2021-09-18T09:40:12Z" == format_datetime(new_job.created_at) + assert -1 == new_job.finish_code + + +def test_update_hyper_data_connection_object(server) -> None: + """Calling `update_hyper_data` with a `ConnectionItem` should update that connection""" + server.version = "3.13" + + connection = TSC.ConnectionItem() + connection._datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + connection._id = "7ecaccd8-39b0-4875-a77d-094f6e930019" + response_xml = UPDATE_HYPER_DATA_XML.read_text() + with requests_mock.mock() as m: + m.patch( + server.datasources.baseurl + + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections/7ecaccd8-39b0-4875-a77d-094f6e930019/data", + status_code=202, + headers={"requestid": "test_id"}, + text=response_xml, + ) + new_job = server.datasources.update_hyper_data(connection, request_id="test_id", actions=[]) - def test_update_hyper_data_datasource_string(self) -> None: - """For convenience, calling `update_hyper_data` with a `str` should update the datasource with the corresponding UUID""" - self.server.version = "3.13" - self.baseurl = self.server.datasources.baseurl + # We only check the `id`; remaining fields are already tested in `test_update_hyper_data_datasource_object` + assert "5c0ba560-c959-424e-b08a-f32ef0bfb737" == new_job.id - datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - response_xml = read_xml_asset(UPDATE_HYPER_DATA_XML) - with requests_mock.mock() as m: - m.patch( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/data", - status_code=202, - headers={"requestid": "test_id"}, - text=response_xml, - ) - new_job = self.server.datasources.update_hyper_data(datasource_id, request_id="test_id", actions=[]) - # We only check the `id`; remaining fields are already tested in `test_update_hyper_data_datasource_object` - self.assertEqual("5c0ba560-c959-424e-b08a-f32ef0bfb737", new_job.id) +def test_update_hyper_data_datasource_string(server) -> None: + """For convenience, calling `update_hyper_data` with a `str` should update the datasource with the corresponding UUID""" + server.version = "3.13" - def test_update_hyper_data_datasource_payload_file(self) -> None: - """If `payload` is present, we upload it and associate the job with it""" - self.server.version = "3.13" - self.baseurl = self.server.datasources.baseurl + datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + response_xml = UPDATE_HYPER_DATA_XML.read_text() + with requests_mock.mock() as m: + m.patch( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/data", + status_code=202, + headers={"requestid": "test_id"}, + text=response_xml, + ) + new_job = server.datasources.update_hyper_data(datasource_id, request_id="test_id", actions=[]) - datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - mock_upload_id = "10051:c3e56879876842d4b3600f20c1f79876-0:0" - response_xml = read_xml_asset(UPDATE_HYPER_DATA_XML) - with requests_mock.mock() as rm, unittest.mock.patch.object(Fileuploads, "upload", return_value=mock_upload_id): - rm.patch( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/data?uploadSessionId=" + mock_upload_id, - status_code=202, - headers={"requestid": "test_id"}, - text=response_xml, - ) - new_job = self.server.datasources.update_hyper_data( - datasource_id, request_id="test_id", actions=[], payload=asset("World Indicators.hyper") - ) + # We only check the `id`; remaining fields are already tested in `test_update_hyper_data_datasource_object` + assert "5c0ba560-c959-424e-b08a-f32ef0bfb737" == new_job.id - # We only check the `id`; remaining fields are already tested in `test_update_hyper_data_datasource_object` - self.assertEqual("5c0ba560-c959-424e-b08a-f32ef0bfb737", new_job.id) - def test_update_hyper_data_datasource_invalid_payload_file(self) -> None: - """If `payload` points to a non-existing file, we report an error""" - self.server.version = "3.13" - self.baseurl = self.server.datasources.baseurl - datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - with self.assertRaises(IOError) as cm: - self.server.datasources.update_hyper_data( - datasource_id, request_id="test_id", actions=[], payload="no/such/file.missing" - ) - exception = cm.exception - self.assertEqual(str(exception), "File path does not lead to an existing file.") +def test_update_hyper_data_datasource_payload_file(server) -> None: + """If `payload` is present, we upload it and associate the job with it""" + server.version = "3.13" - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", status_code=204) - self.server.datasources.delete("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") + datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + mock_upload_id = "10051:c3e56879876842d4b3600f20c1f79876-0:0" + response_xml = UPDATE_HYPER_DATA_XML.read_text() + with requests_mock.mock() as rm, unittest.mock.patch.object(Fileuploads, "upload", return_value=mock_upload_id): + rm.patch( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/data?uploadSessionId=" + mock_upload_id, + status_code=202, + headers={"requestid": "test_id"}, + text=response_xml, + ) + new_job = server.datasources.update_hyper_data( + datasource_id, request_id="test_id", actions=[], payload=(TEST_ASSET_DIR / "World Indicators.hyper") + ) - def test_download(self) -> None: - with requests_mock.mock() as m: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, - ) - file_path = self.server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) - - def test_download_object(self) -> None: - with BytesIO() as file_object: - with requests_mock.mock() as m: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, - ) - file_path = self.server.datasources.download( - "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", filepath=file_object - ) - self.assertTrue(isinstance(file_path, BytesIO)) - - def test_download_sanitizes_name(self) -> None: - filename = "Name,With,Commas.tds" - disposition = f'name="tableau_workbook"; filename="{filename}"' - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", - headers={"Content-Disposition": disposition}, - ) - file_path = self.server.datasources.download("1f951daf-4061-451a-9df1-69a8062664f2") - self.assertEqual(os.path.basename(file_path), "NameWithCommas.tds") - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) + # We only check the `id`; remaining fields are already tested in `test_update_hyper_data_datasource_object` + assert "5c0ba560-c959-424e-b08a-f32ef0bfb737" == new_job.id + + +def test_update_hyper_data_datasource_invalid_payload_file(server) -> None: + """If `payload` points to a non-existing file, we report an error""" + server.version = "3.13" + datasource_id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + with pytest.raises(IOError, match="File path does not lead to an existing file."): + server.datasources.update_hyper_data( + datasource_id, request_id="test_id", actions=[], payload="no/such/file.missing" + ) + + +def test_delete(server) -> None: + with requests_mock.mock() as m: + m.delete(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", status_code=204) + server.datasources.delete("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") - def test_download_extract_only(self) -> None: - # Pretend we're 2.5 for 'extract_only' - self.server.version = "2.5" - self.baseurl = self.server.datasources.baseurl +def test_download(server) -> None: + with requests_mock.mock() as m: + m.get( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, + ) + file_path = server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb") + assert os.path.exists(file_path) + os.remove(file_path) + + +def test_download_object(server) -> None: + with BytesIO() as file_object: with requests_mock.mock() as m: m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content?includeExtract=False", + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, - complete_qs=True, ) - file_path = self.server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", include_extract=False) - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) - - def test_update_missing_id(self) -> None: - single_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.datasources.update, single_datasource) - - def test_publish_missing_path(self) -> None: - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - self.assertRaises( - IOError, self.server.datasources.publish, new_datasource, "", self.server.PublishMode.CreateNew + file_path = server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", filepath=file_object) + assert isinstance(file_path, BytesIO) + + +def test_download_sanitizes_name(server) -> None: + filename = "Name,With,Commas.tds" + disposition = f'name="tableau_workbook"; filename="{filename}"' + with requests_mock.mock() as m: + m.get( + server.datasources.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", + headers={"Content-Disposition": disposition}, + ) + file_path = server.datasources.download("1f951daf-4061-451a-9df1-69a8062664f2") + assert os.path.basename(file_path) == "NameWithCommas.tds" + assert os.path.exists(file_path) + os.remove(file_path) + + +def test_download_extract_only(server) -> None: + # Pretend we're 2.5 for 'extract_only' + server.version = "2.5" + + with requests_mock.mock() as m: + m.get( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content?includeExtract=False", + headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, + complete_qs=True, ) + file_path = server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", include_extract=False) + assert os.path.exists(file_path) + os.remove(file_path) - def test_publish_missing_mode(self) -> None: - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - self.assertRaises(ValueError, self.server.datasources.publish, new_datasource, asset("SampleDS.tds"), None) - def test_publish_invalid_file_type(self) -> None: - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - self.assertRaises( - ValueError, - self.server.datasources.publish, +def test_update_missing_id(server) -> None: + single_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.datasources.update(single_datasource) + + +def test_publish_missing_path(server) -> None: + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + with pytest.raises(IOError): + server.datasources.publish(new_datasource, "", server.PublishMode.CreateNew) + + +def test_publish_missing_mode(server) -> None: + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + with pytest.raises(ValueError): + server.datasources.publish(new_datasource, TEST_ASSET_DIR / "SampleDS.tds", None) + + +def test_publish_invalid_file_type(server) -> None: + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + with pytest.raises(ValueError): + server.datasources.publish( new_datasource, - asset("SampleWB.twbx"), - self.server.PublishMode.Append, + TEST_ASSET_DIR / "SampleWB.twbx", + server.PublishMode.Append, ) - def test_publish_hyper_file_object_raises_exception(self) -> None: - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - with open(asset("World Indicators.hyper"), "rb") as file_object: - self.assertRaises( - ValueError, self.server.datasources.publish, new_datasource, file_object, self.server.PublishMode.Append - ) - def test_publish_tde_file_object_raises_exception(self) -> None: - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - tds_asset = asset(os.path.join("Data", "Tableau Samples", "World Indicators.tde")) - with open(tds_asset, "rb") as file_object: - self.assertRaises( - ValueError, self.server.datasources.publish, new_datasource, file_object, self.server.PublishMode.Append - ) +def test_publish_hyper_file_object_raises_exception(server) -> None: + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + with open(TEST_ASSET_DIR / "World Indicators.hyper", "rb") as file_object: + with pytest.raises(ValueError): + server.datasources.publish(new_datasource, file_object, server.PublishMode.Append) - def test_publish_file_object_of_unknown_type_raises_exception(self) -> None: - new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") - with BytesIO() as file_object: - file_object.write(bytes.fromhex("89504E470D0A1A0A")) - file_object.seek(0) - self.assertRaises( - ValueError, self.server.datasources.publish, new_datasource, file_object, self.server.PublishMode.Append - ) +def test_publish_tde_file_object_raises_exception(server) -> None: + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + tds_asset = TEST_ASSET_DIR / "Data" / "Tableau Samples" / "World Indicators.tde" + with open(tds_asset, "rb") as file_object: + with pytest.raises(ValueError): + server.datasources.publish(new_datasource, file_object, server.PublishMode.Append) - def test_publish_multi_connection(self) -> None: - new_datasource = TSC.DatasourceItem(name="Sample", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - connection1 = TSC.ConnectionItem() - connection1.server_address = "mysql.test.com" - connection1.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) - connection2 = TSC.ConnectionItem() - connection2.server_address = "pgsql.test.com" - connection2.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) - - response = RequestFactory.Datasource._generate_xml(new_datasource, connections=[connection1, connection2]) - # Can't use ConnectionItem parser due to xml namespace problems - connection_results = fromstring(response).findall(".//connection") - - self.assertEqual(connection_results[0].get("serverAddress", None), "mysql.test.com") - self.assertEqual(connection_results[0].find("connectionCredentials").get("name", None), "test") # type: ignore[union-attr] - self.assertEqual(connection_results[1].get("serverAddress", None), "pgsql.test.com") - self.assertEqual(connection_results[1].find("connectionCredentials").get("password", None), "secret") # type: ignore[union-attr] - - def test_publish_single_connection(self) -> None: - new_datasource = TSC.DatasourceItem(name="Sample", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - connection_creds = TSC.ConnectionCredentials("test", "secret", True) - - response = RequestFactory.Datasource._generate_xml(new_datasource, connection_credentials=connection_creds) - # Can't use ConnectionItem parser due to xml namespace problems - credentials = fromstring(response).findall(".//connectionCredentials") - - self.assertEqual(len(credentials), 1) - self.assertEqual(credentials[0].get("name", None), "test") - self.assertEqual(credentials[0].get("password", None), "secret") - self.assertEqual(credentials[0].get("embed", None), "true") - - def test_credentials_and_multi_connect_raises_exception(self) -> None: - new_datasource = TSC.DatasourceItem(name="Sample", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - connection_creds = TSC.ConnectionCredentials("test", "secret", True) - - connection1 = TSC.ConnectionItem() - connection1.server_address = "mysql.test.com" - connection1.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) - - with self.assertRaises(RuntimeError): - response = RequestFactory.Datasource._generate_xml( - new_datasource, connection_credentials=connection_creds, connections=[connection1] - ) - def test_synchronous_publish_timeout_error(self) -> None: - with requests_mock.mock() as m: - m.register_uri("POST", self.baseurl, status_code=504) - - new_datasource = TSC.DatasourceItem(project_id="") - publish_mode = self.server.PublishMode.CreateNew - # http://test/api/2.4/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources?datasourceType=tds - self.assertRaisesRegex( - InternalServerError, - "Please use asynchronous publishing to avoid timeouts.", - self.server.datasources.publish, +def test_publish_file_object_of_unknown_type_raises_exception(server) -> None: + new_datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "test") + + with BytesIO() as file_object: + file_object.write(bytes.fromhex("89504E470D0A1A0A")) + file_object.seek(0) + with pytest.raises(ValueError): + server.datasources.publish(new_datasource, file_object, server.PublishMode.Append) + + +def test_publish_multi_connection(server) -> None: + new_datasource = TSC.DatasourceItem(name="Sample", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + connection1 = TSC.ConnectionItem() + connection1.server_address = "mysql.test.com" + connection1.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) + connection2 = TSC.ConnectionItem() + connection2.server_address = "pgsql.test.com" + connection2.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) + + response = RequestFactory.Datasource._generate_xml(new_datasource, connections=[connection1, connection2]) + # Can't use ConnectionItem parser due to xml namespace problems + connection_results = fromstring(response).findall(".//connection") + + assert connection_results[0].get("serverAddress", None) == "mysql.test.com" + assert connection_results[0].find("connectionCredentials").get("name", None) == "test" + assert connection_results[1].get("serverAddress", None) == "pgsql.test.com" + assert connection_results[1].find("connectionCredentials").get("password", None) == "secret" + + +def test_publish_single_connection(server) -> None: + new_datasource = TSC.DatasourceItem(name="Sample", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + connection_creds = TSC.ConnectionCredentials("test", "secret", True) + + response = RequestFactory.Datasource._generate_xml(new_datasource, connection_credentials=connection_creds) + # Can't use ConnectionItem parser due to xml namespace problems + credentials = fromstring(response).findall(".//connectionCredentials") + + assert len(credentials) == 1 + assert credentials[0].get("name", None) == "test" + assert credentials[0].get("password", None) == "secret" + assert credentials[0].get("embed", None) == "true" + + +def test_credentials_and_multi_connect_raises_exception(server) -> None: + new_datasource = TSC.DatasourceItem(name="Sample", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + connection_creds = TSC.ConnectionCredentials("test", "secret", True) + + connection1 = TSC.ConnectionItem() + connection1.server_address = "mysql.test.com" + connection1.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) + + with pytest.raises(RuntimeError): + response = RequestFactory.Datasource._generate_xml( + new_datasource, connection_credentials=connection_creds, connections=[connection1] + ) + + +def test_synchronous_publish_timeout_error(server) -> None: + with requests_mock.mock() as m: + m.register_uri("POST", server.datasources.baseurl, status_code=504) + + new_datasource = TSC.DatasourceItem(project_id="") + publish_mode = server.PublishMode.CreateNew + # http://test/api/2.4/sites/dad65087-b08b-4603-af4e-2887b8aafc67/datasources?datasourceType=tds + + with pytest.raises(InternalServerError, match="Please use asynchronous publishing to avoid timeouts."): + server.datasources.publish( new_datasource, - asset("SampleDS.tds"), + TEST_ASSET_DIR / "SampleDS.tds", publish_mode, ) - def test_delete_extracts(self) -> None: - self.server.version = "3.10" - self.baseurl = self.server.datasources.baseurl - with requests_mock.mock() as m: - m.post(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/deleteExtract", status_code=200) - self.server.datasources.delete_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - def test_create_extracts(self) -> None: - self.server.version = "3.10" - self.baseurl = self.server.datasources.baseurl +def test_delete_extracts(server) -> None: + server.version = "3.10" + with requests_mock.mock() as m: + m.post(server.datasources.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/deleteExtract", status_code=200) + server.datasources.delete_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - response_xml = read_xml_asset(PUBLISH_XML_ASYNC) - with requests_mock.mock() as m: - m.post( - self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", status_code=200, text=response_xml - ) - self.server.datasources.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - def test_create_extracts_encrypted(self) -> None: - self.server.version = "3.10" - self.baseurl = self.server.datasources.baseurl +def test_create_extracts(server) -> None: + server.version = "3.10" - response_xml = read_xml_asset(PUBLISH_XML_ASYNC) - with requests_mock.mock() as m: - m.post( - self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", status_code=200, text=response_xml - ) - self.server.datasources.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42", True) + response_xml = PUBLISH_XML_ASYNC.read_text() + with requests_mock.mock() as m: + m.post( + server.datasources.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", + status_code=200, + text=response_xml, + ) + server.datasources.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - def test_revisions(self) -> None: - datasource = TSC.DatasourceItem("project", "test") - datasource._id = "06b944d2-959d-4604-9305-12323c95e70e" - response_xml = read_xml_asset(REVISION_XML) - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{datasource.id}/revisions", text=response_xml) - self.server.datasources.populate_revisions(datasource) - revisions = datasource.revisions - - self.assertEqual(len(revisions), 3) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(revisions[0].created_at)) - self.assertEqual("2016-07-27T20:34:56Z", format_datetime(revisions[1].created_at)) - self.assertEqual("2016-07-28T20:34:56Z", format_datetime(revisions[2].created_at)) - - self.assertEqual(False, revisions[0].deleted) - self.assertEqual(False, revisions[0].current) - self.assertEqual(False, revisions[1].deleted) - self.assertEqual(False, revisions[1].current) - self.assertEqual(False, revisions[2].deleted) - self.assertEqual(True, revisions[2].current) - - self.assertEqual("Cassie", revisions[0].user_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[0].user_id) - self.assertIsNone(revisions[1].user_name) - self.assertIsNone(revisions[1].user_id) - self.assertEqual("Cassie", revisions[2].user_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[2].user_id) - - def test_delete_revision(self) -> None: - datasource = TSC.DatasourceItem("project", "test") - datasource._id = "06b944d2-959d-4604-9305-12323c95e70e" +def test_create_extracts_encrypted(server) -> None: + server.version = "3.10" - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{datasource.id}/revisions/3") - self.server.datasources.delete_revision(datasource.id, "3") + response_xml = PUBLISH_XML_ASYNC.read_text() + with requests_mock.mock() as m: + m.post( + server.datasources.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", + status_code=200, + text=response_xml, + ) + server.datasources.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42", True) - def test_download_revision(self) -> None: - with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/revisions/3/content", - headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, - ) - file_path = self.server.datasources.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) - self.assertTrue(os.path.exists(file_path)) - def test_bad_download_response(self) -> None: - with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={ - "Content-Disposition": '''name="tableau_datasource"; filename*=UTF-8''"Sample datasource.tds"''' - }, - ) - file_path = self.server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) - self.assertTrue(os.path.exists(file_path)) +def test_revisions(server) -> None: + datasource = TSC.DatasourceItem("project", "test") + datasource._id = "06b944d2-959d-4604-9305-12323c95e70e" - def test_get_datasource_all_fields(self) -> None: - ro = TSC.RequestOptions() - ro.all_fields = True - with requests_mock.mock() as m: - m.get(f"{self.baseurl}?fields=_all_", text=read_xml_asset(GET_XML_ALL_FIELDS)) - datasources, _ = self.server.datasources.get(req_options=ro) - - assert datasources[0].connected_workbooks_count == 0 - assert datasources[0].content_url == "SuperstoreDatasource" - assert datasources[0].created_at == parse_datetime("2024-02-14T04:42:13Z") - assert not datasources[0].encrypt_extracts - assert datasources[0].favorites_total == 0 - assert not datasources[0].has_alert - assert not datasources[0].has_extracts - assert datasources[0].id == "a71cdd15-3a23-4ec1-b3ce-9956f5e00bb7" - assert not datasources[0].certified - assert datasources[0].is_published - assert datasources[0].name == "Superstore Datasource" - assert datasources[0].size == 1 - assert datasources[0].datasource_type == "excel-direct" - assert datasources[0].updated_at == parse_datetime("2024-02-14T04:42:14Z") - assert not datasources[0].use_remote_query_agent - assert datasources[0].server_name == "localhost" - assert datasources[0].webpage_url == "https://10ax.online.tableau.com/#/site/example/datasources/3566752" - assert isinstance(datasources[0].project, TSC.ProjectItem) - assert datasources[0].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert datasources[0].project.name == "Samples" - assert datasources[0].project.description == "This project includes automatically uploaded samples." - assert datasources[0].owner.email == "bob@example.com" - assert isinstance(datasources[0].owner, TSC.UserItem) - assert datasources[0].owner.fullname == "Bob Smith" - assert datasources[0].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert datasources[0].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert datasources[0].owner.name == "bob@example.com" - assert datasources[0].owner.site_role == "SiteAdministratorCreator" + response_xml = REVISION_XML.read_text() + with requests_mock.mock() as m: + m.get(f"{server.datasources.baseurl}/{datasource.id}/revisions", text=response_xml) + server.datasources.populate_revisions(datasource) + revisions = datasource.revisions + + assert len(revisions) == 3 + assert "2016-07-26T20:34:56Z" == format_datetime(revisions[0].created_at) + assert "2016-07-27T20:34:56Z" == format_datetime(revisions[1].created_at) + assert "2016-07-28T20:34:56Z" == format_datetime(revisions[2].created_at) + + assert False == revisions[0].deleted + assert False == revisions[0].current + assert False == revisions[1].deleted + assert False == revisions[1].current + assert False == revisions[2].deleted + assert True == revisions[2].current + + assert "Cassie" == revisions[0].user_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == revisions[0].user_id + assert revisions[1].user_name is None + assert revisions[1].user_id is None + assert "Cassie" == revisions[2].user_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == revisions[2].user_id + + +def test_delete_revision(server) -> None: + datasource = TSC.DatasourceItem("project", "test") + datasource._id = "06b944d2-959d-4604-9305-12323c95e70e" + + with requests_mock.mock() as m: + m.delete(f"{server.datasources.baseurl}/{datasource.id}/revisions/3") + server.datasources.delete_revision(datasource.id, "3") + + +def test_download_revision(server) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/revisions/3/content", + headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, + ) + file_path = server.datasources.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) + assert os.path.exists(file_path) + + +def test_bad_download_response(server) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={"Content-Disposition": '''name="tableau_datasource"; filename*=UTF-8''"Sample datasource.tds"'''}, + ) + file_path = server.datasources.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + assert os.path.exists(file_path) + + +def test_get_datasource_all_fields(server) -> None: + ro = TSC.RequestOptions() + ro.all_fields = True + with requests_mock.mock() as m: + m.get(f"{server.datasources.baseurl}?fields=_all_", text=GET_XML_ALL_FIELDS.read_text()) + datasources, _ = server.datasources.get(req_options=ro) + + assert datasources[0].connected_workbooks_count == 0 + assert datasources[0].content_url == "SuperstoreDatasource" + assert datasources[0].created_at == parse_datetime("2024-02-14T04:42:13Z") + assert not datasources[0].encrypt_extracts + assert datasources[0].favorites_total == 0 + assert not datasources[0].has_alert + assert not datasources[0].has_extracts + assert datasources[0].id == "a71cdd15-3a23-4ec1-b3ce-9956f5e00bb7" + assert not datasources[0].certified + assert datasources[0].is_published + assert datasources[0].name == "Superstore Datasource" + assert datasources[0].size == 1 + assert datasources[0].datasource_type == "excel-direct" + assert datasources[0].updated_at == parse_datetime("2024-02-14T04:42:14Z") + assert not datasources[0].use_remote_query_agent + assert datasources[0].server_name == "localhost" + assert datasources[0].webpage_url == "https://10ax.online.tableau.com/#/site/example/datasources/3566752" + assert isinstance(datasources[0].project, TSC.ProjectItem) + assert datasources[0].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert datasources[0].project.name == "Samples" + assert datasources[0].project.description == "This project includes automatically uploaded samples." + assert datasources[0].owner.email == "bob@example.com" + assert isinstance(datasources[0].owner, TSC.UserItem) + assert datasources[0].owner.fullname == "Bob Smith" + assert datasources[0].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert datasources[0].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert datasources[0].owner.name == "bob@example.com" + assert datasources[0].owner.site_role == "SiteAdministratorCreator" + + +def test_update_description(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.datasources.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", text=response_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "Sample datasource") + single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_datasource._content_url = "Sampledatasource" + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + single_datasource.certified = True + single_datasource.certification_note = "Warning, here be dragons." + single_datasource.description = "Sample description" + _ = server.datasources.update(single_datasource) + + history = m.request_history[0] + body = fromstring(history.body) + ds_elem = body.find(".//datasource") + assert ds_elem is not None + assert ds_elem.attrib["description"] == "Sample description" + + +def test_publish_description(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.datasources.baseurl, text=response_xml) + single_datasource = TSC.DatasourceItem("1d0304cd-3796-429f-b815-7258370b9b74", "Sample datasource") + single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_datasource._content_url = "Sampledatasource" + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + single_datasource.certified = True + single_datasource.certification_note = "Warning, here be dragons." + single_datasource.description = "Sample description" + _ = server.datasources.publish(single_datasource, TEST_ASSET_DIR / "SampleDS.tds", server.PublishMode.CreateNew) + + history = m.request_history[0] + boundary = history.body[: history.body.index(b"\r\n")].strip() + parts = history.body.split(boundary) + request_payload = next(part for part in parts if b"request_payload" in part) + xml_payload = request_payload.strip().split(b"\r\n")[-1] + body = fromstring(xml_payload) + 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()) + datasources, _ = server.datasources.get() + + datasource = datasources[0] + assert datasource.owner is None + assert datasource.project is None diff --git a/test/test_datasource_model.py b/test/test_datasource_model.py index 655284194..c74805fa6 100644 --- a/test/test_datasource_model.py +++ b/test/test_datasource_model.py @@ -1,18 +1,20 @@ -import unittest +import pytest + import tableauserverclient as TSC -class DatasourceModelTests(unittest.TestCase): - def test_nullable_project_id(self): - datasource = TSC.DatasourceItem(name="10") - self.assertEqual(datasource.project_id, None) +def test_nullable_project_id(): + datasource = TSC.DatasourceItem(name="10") + assert datasource.project_id is None + + +def test_require_boolean_flag_bridge_fail(): + datasource = TSC.DatasourceItem("10") + with pytest.raises(ValueError): + datasource.use_remote_query_agent = "yes" - def test_require_boolean_flag_bridge_fail(self): - datasource = TSC.DatasourceItem("10") - with self.assertRaises(ValueError): - datasource.use_remote_query_agent = "yes" - def test_require_boolean_flag_bridge_ok(self): - datasource = TSC.DatasourceItem("10") - datasource.use_remote_query_agent = True - self.assertEqual(datasource.use_remote_query_agent, True) +def test_require_boolean_flag_bridge_ok(): + datasource = TSC.DatasourceItem("10") + datasource.use_remote_query_agent = True + assert datasource.use_remote_query_agent diff --git a/test/test_dqw.py b/test/test_dqw.py index 6d1219f66..5cb17221a 100644 --- a/test/test_dqw.py +++ b/test/test_dqw.py @@ -1,11 +1,9 @@ -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 +def test_dqw_existence(): + 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_endpoint.py b/test/test_endpoint.py index ff1ef0f72..0b852ab0e 100644 --- a/test/test_endpoint.py +++ b/test/test_endpoint.py @@ -1,83 +1,93 @@ from pathlib import Path import pytest import requests -import unittest import tableauserverclient as TSC +from tableauserverclient.server.endpoint import Endpoint import requests_mock ASSETS = Path(__file__).parent / "assets" -class TestEndpoint(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test/", use_server_version=False) +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - return super().setUp() + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvS" - def test_fallback_request_logic(self) -> None: - url = "http://test/" - endpoint = TSC.server.Endpoint(self.server) - with requests_mock.mock() as m: - m.get(url) - response = endpoint.get_request(url=url) - self.assertIsNotNone(response) + return server - def test_user_friendly_request_returns(self) -> None: - url = "http://test/" - endpoint = TSC.server.Endpoint(self.server) - with requests_mock.mock() as m: - m.get(url) - response = endpoint.send_request_while_show_progress_threaded( - endpoint.parent_srv.session.get, url=url, request_timeout=2 - ) - self.assertIsNotNone(response) - - def test_blocking_request_raises_request_error(self) -> None: - with pytest.raises(requests.exceptions.ConnectionError): - url = "http://test/" - endpoint = TSC.server.Endpoint(self.server) - response = endpoint._blocking_request(endpoint.parent_srv.session.get, url=url) - self.assertIsNotNone(response) - - def test_get_request_stream(self) -> None: + +def test_fallback_request_logic(server: TSC.Server) -> None: + url = "http://test/" + endpoint = Endpoint(server) + with requests_mock.mock() as m: + m.get(url) + response = endpoint.get_request(url=url) + assert response is not None + + +def test_user_friendly_request_returns(server: TSC.Server) -> None: + url = "http://test/" + endpoint = Endpoint(server) + with requests_mock.mock() as m: + m.get(url) + response = endpoint.send_request_while_show_progress_threaded( + endpoint.parent_srv.session.get, url=url, request_timeout=2 + ) + assert response is not None + + +def test_blocking_request_raises_request_error(server: TSC.Server) -> None: + with pytest.raises(requests.exceptions.ConnectionError): url = "http://test/" - endpoint = TSC.server.Endpoint(self.server) - with requests_mock.mock() as m: - m.get(url, headers={"Content-Type": "application/octet-stream"}) - response = endpoint.get_request(url, parameters={"stream": True}) - - self.assertFalse(response._content_consumed) - - def test_binary_log_truncated(self): - class FakeResponse: - headers = {"Content-Type": "application/octet-stream"} - content = b"\x1337" * 1000 - status_code = 200 - - endpoint = TSC.server.Endpoint(self.server) - server_response = FakeResponse() - log = endpoint.log_response_safely(server_response) - self.assertTrue(log.find("[Truncated File Contents]") > 0, log) - - def test_set_user_agent_from_options_headers(self): - params = {"User-Agent": "1", "headers": {"User-Agent": "2"}} - result = TSC.server.Endpoint.set_user_agent(params) - # it should use the value under 'headers' if more than one is given - print(result) - print(result["headers"]["User-Agent"]) - self.assertTrue(result["headers"]["User-Agent"] == "2") - - def test_set_user_agent_from_options(self): - params = {"headers": {"User-Agent": "2"}} - result = TSC.server.Endpoint.set_user_agent(params) - self.assertTrue(result["headers"]["User-Agent"] == "2") - - def test_set_user_agent_when_blank(self): - params = {"headers": {}} - result = TSC.server.Endpoint.set_user_agent(params) - self.assertTrue(result["headers"]["User-Agent"].startswith("Tableau Server Client")) + endpoint = Endpoint(server) + response = endpoint._blocking_request(endpoint.parent_srv.session.get, url=url) + assert response is not None + + +def test_get_request_stream(server: TSC.Server) -> None: + url = "http://test/" + endpoint = Endpoint(server) + with requests_mock.mock() as m: + m.get(url, headers={"Content-Type": "application/octet-stream"}) + response = endpoint.get_request(url, parameters={"stream": True}) + + assert response._content_consumed is False + + +def test_binary_log_truncated(server: TSC.Server) -> None: + class FakeResponse: + headers = {"Content-Type": "application/octet-stream"} + content = b"\x1337" * 1000 + status_code = 200 + + endpoint = Endpoint(server) + server_response = FakeResponse() + log = endpoint.log_response_safely(server_response) # type: ignore + assert log.find("[Truncated File Contents]") > 0 + + +def test_set_user_agent_from_options_headers(server: TSC.Server) -> None: + params = {"User-Agent": "1", "headers": {"User-Agent": "2"}} + result = Endpoint.set_user_agent(params) + # it should use the value under 'headers' if more than one is given + print(result) + print(result["headers"]["User-Agent"]) + assert result["headers"]["User-Agent"] == "2" + + +def test_set_user_agent_from_options(server: TSC.Server) -> None: + params = {"headers": {"User-Agent": "2"}} + result = Endpoint.set_user_agent(params) + assert result["headers"]["User-Agent"] == "2" + + +def test_set_user_agent_when_blank(server: TSC.Server) -> None: + params = {"headers": {}} # type: ignore + result = Endpoint.set_user_agent(params) + assert result["headers"]["User-Agent"].startswith("Tableau Server Client") diff --git a/test/test_exponential_backoff.py b/test/test_exponential_backoff.py index a07eb5d3a..b5c37002f 100644 --- a/test/test_exponential_backoff.py +++ b/test/test_exponential_backoff.py @@ -1,60 +1,62 @@ -import unittest +import pytest from tableauserverclient.exponential_backoff import ExponentialBackoffTimer from ._utils import mocked_time -class ExponentialBackoffTests(unittest.TestCase): - def test_exponential(self): - with mocked_time() as mock_time: - exponentialBackoff = ExponentialBackoffTimer() - # The creation of our mock shouldn't sleep - self.assertAlmostEqual(mock_time(), 0) - # The first sleep sleeps for a rather short time, the following sleeps become longer - exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 0.5) +def test_exponential() -> None: + with mocked_time() as mock_time: + exponentialBackoff = ExponentialBackoffTimer() + # The creation of our mock shouldn't sleep + pytest.approx(mock_time(), 0) + # The first sleep sleeps for a rather short time, the following sleeps become longer + exponentialBackoff.sleep() + pytest.approx(mock_time(), 0.5) + exponentialBackoff.sleep() + pytest.approx(mock_time(), 1.2) + exponentialBackoff.sleep() + pytest.approx(mock_time(), 2.18) + exponentialBackoff.sleep() + pytest.approx(mock_time(), 3.552) + exponentialBackoff.sleep() + pytest.approx(mock_time(), 5.4728) + + +def test_exponential_saturation() -> None: + with mocked_time() as mock_time: + exponentialBackoff = ExponentialBackoffTimer() + for _ in range(99): exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 1.2) + # We don't increase the sleep time above 30 seconds. + # Otherwise, the exponential sleep time could easily + # reach minutes or even hours between polls + for _ in range(5): + s = mock_time() exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 2.18) + slept = mock_time() - s + pytest.approx(slept, 30) + + +def test_timeout() -> None: + with mocked_time() as mock_time: + exponentialBackoff = ExponentialBackoffTimer(timeout=4.5) + for _ in range(4): exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 3.552) + pytest.approx(mock_time(), 3.552) + # Usually, the following sleep would sleep until 5.5, but due to + # the timeout we wait less; thereby we make sure to take the timeout + # into account as good as possible + exponentialBackoff.sleep() + pytest.approx(mock_time(), 4.5) + # The next call to `sleep` will raise a TimeoutError + with pytest.raises(TimeoutError): exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 5.4728) - - def test_exponential_saturation(self): - with mocked_time() as mock_time: - exponentialBackoff = ExponentialBackoffTimer() - for _ in range(99): - exponentialBackoff.sleep() - # We don't increase the sleep time above 30 seconds. - # Otherwise, the exponential sleep time could easily - # reach minutes or even hours between polls - for _ in range(5): - s = mock_time() - exponentialBackoff.sleep() - slept = mock_time() - s - self.assertAlmostEqual(slept, 30) - - def test_timeout(self): - with mocked_time() as mock_time: - exponentialBackoff = ExponentialBackoffTimer(timeout=4.5) - for _ in range(4): - exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 3.552) - # Usually, the following sleep would sleep until 5.5, but due to - # the timeout we wait less; thereby we make sure to take the timeout - # into account as good as possible + + +def test_timeout_zero() -> None: + with mocked_time() as mock_time: + # The construction of the timer doesn't throw, yet + exponentialBackoff = ExponentialBackoffTimer(timeout=0) + # But the first `sleep` immediately throws + with pytest.raises(TimeoutError): exponentialBackoff.sleep() - self.assertAlmostEqual(mock_time(), 4.5) - # The next call to `sleep` will raise a TimeoutError - with self.assertRaises(TimeoutError): - exponentialBackoff.sleep() - - def test_timeout_zero(self): - with mocked_time() as mock_time: - # The construction of the timer doesn't throw, yet - exponentialBackoff = ExponentialBackoffTimer(timeout=0) - # But the first `sleep` immediately throws - with self.assertRaises(TimeoutError): - exponentialBackoff.sleep() diff --git a/test/test_extensions.py b/test/test_extensions.py new file mode 100644 index 000000000..9dc001876 --- /dev/null +++ b/test/test_extensions.py @@ -0,0 +1,195 @@ +from pathlib import Path + +from defusedxml.ElementTree import fromstring +import requests_mock +import pytest + +import tableauserverclient as TSC + + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_SERVER_EXT_SETTINGS = TEST_ASSET_DIR / "extensions_server_settings_true.xml" +GET_SERVER_EXT_SETTINGS_FALSE = TEST_ASSET_DIR / "extensions_server_settings_false.xml" +GET_SITE_SETTINGS = TEST_ASSET_DIR / "extensions_site_settings.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + + # Fake sign in + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.21" + + return server + + +def test_get_server_extensions_settings(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.extensions._server_baseurl, text=GET_SERVER_EXT_SETTINGS.read_text()) + ext_settings = server.extensions.get_server_settings() + + assert ext_settings.enabled is True + assert ext_settings.block_list is not None + assert set(ext_settings.block_list) == {"https://test.com", "https://example.com"} + + +def test_get_server_extensions_settings_false(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.extensions._server_baseurl, text=GET_SERVER_EXT_SETTINGS_FALSE.read_text()) + ext_settings = server.extensions.get_server_settings() + + assert ext_settings.enabled is False + assert ext_settings.block_list is not None + assert len(ext_settings.block_list) == 0 + + +def test_update_server_extensions_settings(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.put(server.extensions._server_baseurl, text=GET_SERVER_EXT_SETTINGS_FALSE.read_text()) + + ext_settings = TSC.ExtensionsServer() + ext_settings.enabled = False + ext_settings.block_list = [] + + updated_settings = server.extensions.update_server_settings(ext_settings) + + assert updated_settings.enabled is False + assert updated_settings.block_list is not None + assert len(updated_settings.block_list) == 0 + + +def test_get_site_settings(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.extensions.baseurl, text=GET_SITE_SETTINGS.read_text()) + site_settings = server.extensions.get() + + assert isinstance(site_settings, TSC.ExtensionsSiteSettings) + assert site_settings.enabled is True + assert site_settings.use_default_setting is False + assert site_settings.safe_list is not None + assert site_settings.allow_trusted is True + assert site_settings.include_partner_built is False + assert site_settings.include_sandboxed is False + assert site_settings.include_tableau_built is False + assert len(site_settings.safe_list) == 1 + first_safe = site_settings.safe_list[0] + assert first_safe.url == "http://localhost:9123/Dynamic.html" + assert first_safe.full_data_allowed is True + assert first_safe.prompt_needed is True + + +def test_update_site_settings(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.put(server.extensions.baseurl, text=GET_SITE_SETTINGS.read_text()) + + site_settings = TSC.ExtensionsSiteSettings() + site_settings.enabled = True + site_settings.use_default_setting = False + safe_extension = TSC.SafeExtension( + url="http://localhost:9123/Dynamic.html", + full_data_allowed=True, + prompt_needed=True, + ) + site_settings.safe_list = [safe_extension] + + updated_settings = server.extensions.update(site_settings) + history = m.request_history + + assert isinstance(updated_settings, TSC.ExtensionsSiteSettings) + assert updated_settings.enabled is True + assert updated_settings.use_default_setting is False + assert updated_settings.safe_list is not None + assert len(updated_settings.safe_list) == 1 + first_safe = updated_settings.safe_list[0] + assert first_safe.url == "http://localhost:9123/Dynamic.html" + assert first_safe.full_data_allowed is True + assert first_safe.prompt_needed is True + + # Verify that the request body was as expected + assert len(history) == 1 + xml_payload = fromstring(history[0].body) + extensions_site_settings_elem = xml_payload.find(".//extensionsSiteSettings") + assert extensions_site_settings_elem is not None + enabled_elem = extensions_site_settings_elem.find("extensionsEnabled") + assert enabled_elem is not None + assert enabled_elem.text == "true" + use_default_elem = extensions_site_settings_elem.find("useDefaultSetting") + assert use_default_elem is not None + assert use_default_elem.text == "false" + safe_list_elements = list(extensions_site_settings_elem.findall("safeList")) + assert len(safe_list_elements) == 1 + safe_extension_elem = safe_list_elements[0] + url_elem = safe_extension_elem.find("url") + assert url_elem is not None + assert url_elem.text == "http://localhost:9123/Dynamic.html" + full_data_allowed_elem = safe_extension_elem.find("fullDataAllowed") + assert full_data_allowed_elem is not None + assert full_data_allowed_elem.text == "true" + prompt_needed_elem = safe_extension_elem.find("promptNeeded") + assert prompt_needed_elem is not None + assert prompt_needed_elem.text == "true" + + +def test_update_safe_list_none(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.put(server.extensions.baseurl, text=GET_SITE_SETTINGS.read_text()) + + site_settings = TSC.ExtensionsSiteSettings() + site_settings.enabled = True + site_settings.use_default_setting = False + + updated_settings = server.extensions.update(site_settings) + history = m.request_history + + assert isinstance(updated_settings, TSC.ExtensionsSiteSettings) + assert updated_settings.enabled is True + assert updated_settings.use_default_setting is False + assert updated_settings.safe_list is not None + assert len(updated_settings.safe_list) == 1 + first_safe = updated_settings.safe_list[0] + assert first_safe.url == "http://localhost:9123/Dynamic.html" + assert first_safe.full_data_allowed is True + assert first_safe.prompt_needed is True + + # Verify that the request body was as expected + assert len(history) == 1 + xml_payload = fromstring(history[0].body) + extensions_site_settings_elem = xml_payload.find(".//extensionsSiteSettings") + assert extensions_site_settings_elem is not None + safe_list_element = extensions_site_settings_elem.find("safeList") + assert safe_list_element is None + + +def test_update_safe_list_empty(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.put(server.extensions.baseurl, text=GET_SITE_SETTINGS.read_text()) + + site_settings = TSC.ExtensionsSiteSettings() + site_settings.enabled = True + site_settings.use_default_setting = False + site_settings.safe_list = [] + + updated_settings = server.extensions.update(site_settings) + history = m.request_history + + assert isinstance(updated_settings, TSC.ExtensionsSiteSettings) + assert updated_settings.enabled is True + assert updated_settings.use_default_setting is False + assert updated_settings.safe_list is not None + assert len(updated_settings.safe_list) == 1 + first_safe = updated_settings.safe_list[0] + assert first_safe.url == "http://localhost:9123/Dynamic.html" + assert first_safe.full_data_allowed is True + assert first_safe.prompt_needed is True + + # Verify that the request body was as expected + assert len(history) == 1 + xml_payload = fromstring(history[0].body) + extensions_site_settings_elem = xml_payload.find(".//extensionsSiteSettings") + assert extensions_site_settings_elem is not None + safe_list_element = extensions_site_settings_elem.find("safeList") + assert safe_list_element is not None + assert len(safe_list_element) == 0 diff --git a/test/test_favorites.py b/test/test_favorites.py index 87332d70f..a7bed8d9b 100644 --- a/test/test_favorites.py +++ b/test/test_favorites.py @@ -1,119 +1,146 @@ -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -from ._utils import read_xml_asset - -GET_FAVORITES_XML = "favorites_get.xml" -ADD_FAVORITE_WORKBOOK_XML = "favorites_add_workbook.xml" -ADD_FAVORITE_VIEW_XML = "favorites_add_view.xml" -ADD_FAVORITE_DATASOURCE_XML = "favorites_add_datasource.xml" -ADD_FAVORITE_PROJECT_XML = "favorites_add_project.xml" - - -class FavoritesTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.server.version = "2.5" - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.favorites.baseurl - self.user = TSC.UserItem("alice", TSC.UserItem.Roles.Viewer) - self.user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - - def test_get(self) -> None: - response_xml = read_xml_asset(GET_FAVORITES_XML) - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{self.user.id}", text=response_xml) - self.server.favorites.get(self.user) - self.assertIsNotNone(self.user._favorites) - self.assertEqual(len(self.user.favorites["workbooks"]), 1) - self.assertEqual(len(self.user.favorites["views"]), 1) - self.assertEqual(len(self.user.favorites["projects"]), 1) - self.assertEqual(len(self.user.favorites["datasources"]), 1) - - workbook = self.user.favorites["workbooks"][0] - print("favorited: ") - print(workbook) - view = self.user.favorites["views"][0] - datasource = self.user.favorites["datasources"][0] - project = self.user.favorites["projects"][0] - - self.assertEqual(workbook.id, "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00") - self.assertEqual(view.id, "d79634e1-6063-4ec9-95ff-50acbf609ff5") - self.assertEqual(datasource.id, "e76a1461-3b1d-4588-bf1b-17551a879ad9") - self.assertEqual(project.id, "1d0304cd-3796-429f-b815-7258370b9b74") - - def test_add_favorite_workbook(self) -> None: - response_xml = read_xml_asset(ADD_FAVORITE_WORKBOOK_XML) - workbook = TSC.WorkbookItem("") - workbook._id = "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" - workbook.name = "Superstore" - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{self.user.id}", text=response_xml) - self.server.favorites.add_favorite_workbook(self.user, workbook) - - def test_add_favorite_view(self) -> None: - response_xml = read_xml_asset(ADD_FAVORITE_VIEW_XML) - view = TSC.ViewItem() - view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - view._name = "ENDANGERED SAFARI" - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{self.user.id}", text=response_xml) - self.server.favorites.add_favorite_view(self.user, view) - - def test_add_favorite_datasource(self) -> None: - response_xml = read_xml_asset(ADD_FAVORITE_DATASOURCE_XML) - datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - datasource._id = "e76a1461-3b1d-4588-bf1b-17551a879ad9" - datasource.name = "SampleDS" - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{self.user.id}", text=response_xml) - self.server.favorites.add_favorite_datasource(self.user, datasource) - - def test_add_favorite_project(self) -> None: - self.server.version = "3.1" - baseurl = self.server.favorites.baseurl - response_xml = read_xml_asset(ADD_FAVORITE_PROJECT_XML) - project = TSC.ProjectItem("Tableau") - project._id = "1d0304cd-3796-429f-b815-7258370b9b74" - with requests_mock.mock() as m: - m.put(f"{baseurl}/{self.user.id}", text=response_xml) - self.server.favorites.add_favorite_project(self.user, project) - - def test_delete_favorite_workbook(self) -> None: - workbook = TSC.WorkbookItem("") - workbook._id = "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" - workbook.name = "Superstore" - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{self.user.id}/workbooks/{workbook.id}") - self.server.favorites.delete_favorite_workbook(self.user, workbook) - - def test_delete_favorite_view(self) -> None: - view = TSC.ViewItem() - view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - view._name = "ENDANGERED SAFARI" - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{self.user.id}/views/{view.id}") - self.server.favorites.delete_favorite_view(self.user, view) - - def test_delete_favorite_datasource(self) -> None: - datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - datasource._id = "e76a1461-3b1d-4588-bf1b-17551a879ad9" - datasource.name = "SampleDS" - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{self.user.id}/datasources/{datasource.id}") - self.server.favorites.delete_favorite_datasource(self.user, datasource) - - def test_delete_favorite_project(self) -> None: - self.server.version = "3.1" - baseurl = self.server.favorites.baseurl - project = TSC.ProjectItem("Tableau") - project._id = "1d0304cd-3796-429f-b815-7258370b9b74" - with requests_mock.mock() as m: - m.delete(f"{baseurl}/{self.user.id}/projects/{project.id}") - self.server.favorites.delete_favorite_project(self.user, project) +from tableauserverclient.datetime_helpers import parse_datetime + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_FAVORITES_XML = TEST_ASSET_DIR / "favorites_get.xml" +ADD_FAVORITE_WORKBOOK_XML = TEST_ASSET_DIR / "favorites_add_workbook.xml" +ADD_FAVORITE_VIEW_XML = TEST_ASSET_DIR / "favorites_add_view.xml" +ADD_FAVORITE_DATASOURCE_XML = TEST_ASSET_DIR / "favorites_add_datasource.xml" +ADD_FAVORITE_PROJECT_XML = TEST_ASSET_DIR / "favorites_add_project.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.5" + + return server + + +@pytest.fixture(scope="function") +def user() -> TSC.UserItem: + user = TSC.UserItem("alice", TSC.UserItem.Roles.Viewer) + user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + return user + + +def test_get(server: TSC.Server, user: TSC.UserItem) -> None: + response_xml = GET_FAVORITES_XML.read_text() + with requests_mock.mock() as m: + m.get(f"{server.favorites.baseurl}/{user.id}", text=response_xml) + server.favorites.get(user) + assert user._favorites is not None + assert len(user.favorites["workbooks"]) == 1 + assert len(user.favorites["views"]) == 1 + assert len(user.favorites["projects"]) == 1 + assert len(user.favorites["datasources"]) == 1 + + workbook = user.favorites["workbooks"][0] + print("favorited: ") + print(workbook) + view = user.favorites["views"][0] + datasource = user.favorites["datasources"][0] + project = user.favorites["projects"][0] + + assert workbook.id == "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" + assert view.id == "d79634e1-6063-4ec9-95ff-50acbf609ff5" + assert datasource.id == "e76a1461-3b1d-4588-bf1b-17551a879ad9" + assert project.id == "1d0304cd-3796-429f-b815-7258370b9b74" + + collection = user.favorites["collections"][0] + + assert collection.id == "8c57cb8a-d65f-4a32-813e-5a3f86e8f94e" + assert collection.name == "sample collection" + assert collection.description == "description for sample collection" + assert collection.total_item_count == 3 + assert collection.permissioned_item_count == 2 + assert collection.visibility == "Private" + assert collection.created_at == parse_datetime("2016-08-11T21:22:40Z") + assert collection.updated_at == parse_datetime("2016-08-11T21:34:17Z") + + +def test_add_favorite_workbook(server: TSC.Server, user: TSC.UserItem) -> None: + response_xml = ADD_FAVORITE_WORKBOOK_XML.read_text() + workbook = TSC.WorkbookItem("") + workbook._id = "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" + workbook.name = "Superstore" + with requests_mock.mock() as m: + m.put(f"{server.favorites.baseurl}/{user.id}", text=response_xml) + server.favorites.add_favorite_workbook(user, workbook) + + +def test_add_favorite_view(server: TSC.Server, user: TSC.UserItem) -> None: + response_xml = ADD_FAVORITE_VIEW_XML.read_text() + view = TSC.ViewItem() + view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + view._name = "ENDANGERED SAFARI" + with requests_mock.mock() as m: + m.put(f"{server.favorites.baseurl}/{user.id}", text=response_xml) + server.favorites.add_favorite_view(user, view) + + +def test_add_favorite_datasource(server: TSC.Server, user: TSC.UserItem) -> None: + response_xml = ADD_FAVORITE_DATASOURCE_XML.read_text() + datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + datasource._id = "e76a1461-3b1d-4588-bf1b-17551a879ad9" + datasource.name = "SampleDS" + with requests_mock.mock() as m: + m.put(f"{server.favorites.baseurl}/{user.id}", text=response_xml) + server.favorites.add_favorite_datasource(user, datasource) + + +def test_add_favorite_project(server: TSC.Server, user: TSC.UserItem) -> None: + server.version = "3.1" + baseurl = server.favorites.baseurl + response_xml = ADD_FAVORITE_PROJECT_XML.read_text() + project = TSC.ProjectItem("Tableau") + project._id = "1d0304cd-3796-429f-b815-7258370b9b74" + with requests_mock.mock() as m: + m.put(f"{baseurl}/{user.id}", text=response_xml) + server.favorites.add_favorite_project(user, project) + + +def test_delete_favorite_workbook(server: TSC.Server, user: TSC.UserItem) -> None: + workbook = TSC.WorkbookItem("") + workbook._id = "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" + workbook.name = "Superstore" + with requests_mock.mock() as m: + m.delete(f"{server.favorites.baseurl}/{user.id}/workbooks/{workbook.id}") + server.favorites.delete_favorite_workbook(user, workbook) + + +def test_delete_favorite_view(server: TSC.Server, user: TSC.UserItem) -> None: + view = TSC.ViewItem() + view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + view._name = "ENDANGERED SAFARI" + with requests_mock.mock() as m: + m.delete(f"{server.favorites.baseurl}/{user.id}/views/{view.id}") + server.favorites.delete_favorite_view(user, view) + + +def test_delete_favorite_datasource(server: TSC.Server, user: TSC.UserItem) -> None: + datasource = TSC.DatasourceItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + datasource._id = "e76a1461-3b1d-4588-bf1b-17551a879ad9" + datasource.name = "SampleDS" + with requests_mock.mock() as m: + m.delete(f"{server.favorites.baseurl}/{user.id}/datasources/{datasource.id}") + server.favorites.delete_favorite_datasource(user, datasource) + + +def test_delete_favorite_project(server: TSC.Server, user: TSC.UserItem) -> None: + server.version = "3.1" + baseurl = server.favorites.baseurl + project = TSC.ProjectItem("Tableau") + project._id = "1d0304cd-3796-429f-b815-7258370b9b74" + with requests_mock.mock() as m: + m.delete(f"{baseurl}/{user.id}/projects/{project.id}") + server.favorites.delete_favorite_project(user, project) diff --git a/test/test_filesys_helpers.py b/test/test_filesys_helpers.py index 0f3234d5d..aa31ae98a 100644 --- a/test/test_filesys_helpers.py +++ b/test/test_filesys_helpers.py @@ -1,99 +1,113 @@ import os -import unittest +from pathlib import Path from io import BytesIO from xml.etree import ElementTree as ET from zipfile import ZipFile +import pytest + from tableauserverclient.filesys_helpers import get_file_object_size, get_file_type -from ._utils import asset, TEST_ASSET_DIR - - -class FilesysTests(unittest.TestCase): - def test_get_file_size_returns_correct_size(self): - target_size = 1000 # bytes - - with BytesIO() as f: - f.seek(target_size - 1) - f.write(b"\0") - file_size = get_file_object_size(f) - - self.assertEqual(file_size, target_size) - - def test_get_file_size_returns_zero_for_empty_file(self): - with BytesIO() as f: - file_size = get_file_object_size(f) - - self.assertEqual(file_size, 0) - - def test_get_file_size_coincides_with_built_in_method(self): - asset_path = asset("SampleWB.twbx") - target_size = os.path.getsize(asset_path) - with open(asset_path, "rb") as f: - file_size = get_file_object_size(f) - - self.assertEqual(file_size, target_size) - - def test_get_file_type_identifies_a_zip_file(self): - with BytesIO() as file_object: - with ZipFile(file_object, "w") as zf: - with BytesIO() as stream: - stream.write(b"This is a zip file") - zf.writestr("dummy_file", stream.getbuffer()) - file_object.seek(0) - file_type = get_file_type(file_object) - - self.assertEqual(file_type, "zip") - - def test_get_file_type_identifies_tdsx_as_zip_file(self): - with open(asset("World Indicators.tdsx"), "rb") as file_object: - file_type = get_file_type(file_object) - self.assertEqual(file_type, "zip") - - def test_get_file_type_identifies_twbx_as_zip_file(self): - with open(asset("SampleWB.twbx"), "rb") as file_object: - file_type = get_file_type(file_object) - self.assertEqual(file_type, "zip") - - def test_get_file_type_identifies_xml_file(self): - root = ET.Element("root") - child = ET.SubElement(root, "child") - child.text = "This is a child element" - etree = ET.ElementTree(root) - - with BytesIO() as file_object: - etree.write(file_object, encoding="utf-8", xml_declaration=True) - - file_object.seek(0) - file_type = get_file_type(file_object) - - self.assertEqual(file_type, "xml") - - def test_get_file_type_identifies_tds_as_xml_file(self): - with open(asset("World Indicators.tds"), "rb") as file_object: - file_type = get_file_type(file_object) - self.assertEqual(file_type, "xml") - - def test_get_file_type_identifies_twb_as_xml_file(self): - with open(asset("RESTAPISample.twb"), "rb") as file_object: - file_type = get_file_type(file_object) - self.assertEqual(file_type, "xml") - - def test_get_file_type_identifies_hyper_file(self): - with open(asset("World Indicators.hyper"), "rb") as file_object: - file_type = get_file_type(file_object) - self.assertEqual(file_type, "hyper") - - def test_get_file_type_identifies_tde_file(self): - asset_path = os.path.join(TEST_ASSET_DIR, "Data", "Tableau Samples", "World Indicators.tde") - with open(asset_path, "rb") as file_object: - file_type = get_file_type(file_object) - self.assertEqual(file_type, "tde") - - def test_get_file_type_handles_unknown_file_type(self): - # Create a dummy png file - with BytesIO() as file_object: - png_signature = bytes.fromhex("89504E470D0A1A0A") - file_object.write(png_signature) - file_object.seek(0) - - self.assertRaises(ValueError, get_file_type, file_object) + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + + +def test_get_file_size_returns_correct_size() -> None: + target_size = 1000 # bytes + + with BytesIO() as f: + f.seek(target_size - 1) + f.write(b"\0") + file_size = get_file_object_size(f) + + assert file_size == target_size + + +def test_get_file_size_returns_zero_for_empty_file() -> None: + with BytesIO() as f: + file_size = get_file_object_size(f) + + assert file_size == 0 + + +def test_get_file_size_coincides_with_built_in_method() -> None: + asset_path = TEST_ASSET_DIR / "SampleWB.twbx" + target_size = os.path.getsize(asset_path) + with open(asset_path, "rb") as f: + file_size = get_file_object_size(f) + + assert file_size == target_size + + +def test_get_file_type_identifies_a_zip_file() -> None: + with BytesIO() as file_object: + with ZipFile(file_object, "w") as zf: + with BytesIO() as stream: + stream.write(b"This is a zip file") + zf.writestr("dummy_file", stream.getbuffer()) + file_object.seek(0) + file_type = get_file_type(file_object) + + assert file_type == "zip" + + +def test_get_file_type_identifies_tdsx_as_zip_file() -> None: + with open(TEST_ASSET_DIR / "World Indicators.tdsx", "rb") as file_object: + file_type = get_file_type(file_object) + assert file_type == "zip" + + +def test_get_file_type_identifies_twbx_as_zip_file() -> None: + with open(TEST_ASSET_DIR / "SampleWB.twbx", "rb") as file_object: + file_type = get_file_type(file_object) + assert file_type == "zip" + + +def test_get_file_type_identifies_xml_file() -> None: + root = ET.Element("root") + child = ET.SubElement(root, "child") + child.text = "This is a child element" + etree = ET.ElementTree(root) + + with BytesIO() as file_object: + etree.write(file_object, encoding="utf-8", xml_declaration=True) + + file_object.seek(0) + file_type = get_file_type(file_object) + + assert file_type == "xml" + + +def test_get_file_type_identifies_tds_as_xml_file() -> None: + with open(TEST_ASSET_DIR / "World Indicators.tds", "rb") as file_object: + file_type = get_file_type(file_object) + assert file_type == "xml" + + +def test_get_file_type_identifies_twb_as_xml_file() -> None: + with open(TEST_ASSET_DIR / "RESTAPISample.twb", "rb") as file_object: + file_type = get_file_type(file_object) + assert file_type == "xml" + + +def test_get_file_type_identifies_hyper_file() -> None: + with open(TEST_ASSET_DIR / "World Indicators.hyper", "rb") as file_object: + file_type = get_file_type(file_object) + assert file_type == "hyper" + + +def test_get_file_type_identifies_tde_file() -> None: + asset_path = TEST_ASSET_DIR / "Data" / "Tableau Samples" / "World Indicators.tde" + with open(asset_path, "rb") as file_object: + file_type = get_file_type(file_object) + assert file_type == "tde" + + +def test_get_file_type_handles_unknown_file_type() -> None: + # Create a dummy png file + with BytesIO() as file_object: + png_signature = bytes.fromhex("89504E470D0A1A0A") + file_object.write(png_signature) + file_object.seek(0) + + with pytest.raises(ValueError): + get_file_type(file_object) diff --git a/test/test_fileuploads.py b/test/test_fileuploads.py index 9567bc3ad..2e69b5884 100644 --- a/test/test_fileuploads.py +++ b/test/test_fileuploads.py @@ -1,17 +1,18 @@ import contextlib import io import os -import unittest +from pathlib import Path +import pytest import requests_mock +import tableauserverclient as TSC from tableauserverclient.config import BYTES_PER_MB, config -from tableauserverclient.server import Server -from ._utils import asset -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") -FILEUPLOAD_INITIALIZE = os.path.join(TEST_ASSET_DIR, "fileupload_initialize.xml") -FILEUPLOAD_APPEND = os.path.join(TEST_ASSET_DIR, "fileupload_append.xml") +TEST_ASSET_DIR = Path(__file__).parent / "assets" +FILEUPLOAD_INITIALIZE = TEST_ASSET_DIR / "fileupload_initialize.xml" +FILEUPLOAD_APPEND = TEST_ASSET_DIR / "fileupload_append.xml" +SAMPLE_WB = TEST_ASSET_DIR / "SampleWB.twbx" @contextlib.contextmanager @@ -25,65 +26,67 @@ def set_env(**environ): os.environ.update(old_environ) -class FileuploadsTests(unittest.TestCase): - def setUp(self): - self.server = Server("http://test", False) +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) - # Fake sign in - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/fileUploads" + return server - def test_read_chunks_file_path(self): - file_path = asset("SampleWB.twbx") - chunks = self.server.fileuploads._read_chunks(file_path) + +def test_read_chunks_file_path(server: TSC.Server) -> None: + file_path = str(SAMPLE_WB) + chunks = server.fileuploads._read_chunks(file_path) + for chunk in chunks: + assert chunk is not None + + +def test_read_chunks_file_object(server: TSC.Server) -> None: + with SAMPLE_WB.open("rb") as f: + chunks = server.fileuploads._read_chunks(f) for chunk in chunks: - self.assertIsNotNone(chunk) - - def test_read_chunks_file_object(self): - with open(asset("SampleWB.twbx"), "rb") as f: - chunks = self.server.fileuploads._read_chunks(f) - for chunk in chunks: - self.assertIsNotNone(chunk) - - def test_upload_chunks_file_path(self): - file_path = asset("SampleWB.twbx") - upload_id = "7720:170fe6b1c1c7422dadff20f944d58a52-1:0" - - with open(FILEUPLOAD_INITIALIZE, "rb") as f: - initialize_response_xml = f.read().decode("utf-8") - with open(FILEUPLOAD_APPEND, "rb") as f: - append_response_xml = f.read().decode("utf-8") + assert chunk is not None + + +def test_upload_chunks_file_path(server: TSC.Server) -> None: + file_path = str(SAMPLE_WB) + upload_id = "7720:170fe6b1c1c7422dadff20f944d58a52-1:0" + + initialize_response_xml = FILEUPLOAD_INITIALIZE.read_text() + append_response_xml = FILEUPLOAD_APPEND.read_text() + with requests_mock.mock() as m: + m.post(server.fileuploads.baseurl, text=initialize_response_xml) + m.put(f"{server.fileuploads.baseurl}/{upload_id}", text=append_response_xml) + actual = server.fileuploads.upload(file_path) + + assert upload_id == actual + + +def test_upload_chunks_file_object(server: TSC.Server) -> None: + upload_id = "7720:170fe6b1c1c7422dadff20f944d58a52-1:0" + + with SAMPLE_WB.open("rb") as file_content: + initialize_response_xml = FILEUPLOAD_INITIALIZE.read_text() + append_response_xml = FILEUPLOAD_APPEND.read_text() with requests_mock.mock() as m: - m.post(self.baseurl, text=initialize_response_xml) - m.put(f"{self.baseurl}/{upload_id}", text=append_response_xml) - actual = self.server.fileuploads.upload(file_path) - - self.assertEqual(upload_id, actual) - - def test_upload_chunks_file_object(self): - upload_id = "7720:170fe6b1c1c7422dadff20f944d58a52-1:0" - - with open(asset("SampleWB.twbx"), "rb") as file_content: - with open(FILEUPLOAD_INITIALIZE, "rb") as f: - initialize_response_xml = f.read().decode("utf-8") - with open(FILEUPLOAD_APPEND, "rb") as f: - append_response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=initialize_response_xml) - m.put(f"{self.baseurl}/{upload_id}", text=append_response_xml) - actual = self.server.fileuploads.upload(file_content) - - self.assertEqual(upload_id, actual) - - def test_upload_chunks_config(self): - data = io.BytesIO() - data.write(b"1" * (config.CHUNK_SIZE_MB * BYTES_PER_MB + 1)) + m.post(server.fileuploads.baseurl, text=initialize_response_xml) + m.put(f"{server.fileuploads.baseurl}/{upload_id}", text=append_response_xml) + actual = server.fileuploads.upload(file_content) + + assert upload_id == actual + + +def test_upload_chunks_config(server: TSC.Server) -> None: + data = io.BytesIO() + data.write(b"1" * (config.CHUNK_SIZE_MB * BYTES_PER_MB + 1)) + data.seek(0) + with set_env(TSC_CHUNK_SIZE_MB="1"): + chunker = server.fileuploads._read_chunks(data) + chunk = next(chunker) + assert len(chunk) == config.CHUNK_SIZE_MB * BYTES_PER_MB data.seek(0) - with set_env(TSC_CHUNK_SIZE_MB="1"): - chunker = self.server.fileuploads._read_chunks(data) - chunk = next(chunker) - assert len(chunk) == config.CHUNK_SIZE_MB * BYTES_PER_MB - data.seek(0) - assert len(chunk) < len(data.read()) + assert len(chunk) < len(data.read()) diff --git a/test/test_filter.py b/test/test_filter.py index e2121307f..460813dd5 100644 --- a/test/test_filter.py +++ b/test/test_filter.py @@ -1,22 +1,16 @@ -import os -import unittest - import tableauserverclient as TSC -class FilterTests(unittest.TestCase): - def setUp(self): - pass +def test_filter_equal(): + filter = TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "Superstore") - def test_filter_equal(self): - filter = TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "Superstore") + assert str(filter) == "name:eq:Superstore" - self.assertEqual(str(filter), "name:eq:Superstore") - def test_filter_in(self): - # create a IN filter condition with project names that - # contain spaces and "special" characters - projects_to_find = ["default", "Salesforce Sales Projeśt"] - filter = TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.In, projects_to_find) +def test_filter_in(): + # create a IN filter condition with project names that + # contain spaces and "special" characters + projects_to_find = ["default", "Salesforce Sales Projeśt"] + filter = TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.In, projects_to_find) - self.assertEqual(str(filter), "name:in:[default,Salesforce Sales Projeśt]") + assert str(filter) == "name:in:[default,Salesforce Sales Projeśt]" diff --git a/test/test_flow.py b/test/test_flow.py index d458bc77b..9ebbbe5d6 100644 --- a/test/test_flow.py +++ b/test/test_flow.py @@ -1,225 +1,242 @@ +from io import BytesIO import os +from pathlib import Path import requests_mock import tempfile -import unittest -from io import BytesIO +import pytest import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime -from ._utils import read_xml_asset, asset -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") +TEST_ASSET_DIR = Path(__file__).parent / "assets" -GET_XML = os.path.join(TEST_ASSET_DIR, "flow_get.xml") -POPULATE_CONNECTIONS_XML = os.path.join(TEST_ASSET_DIR, "flow_populate_connections.xml") -POPULATE_PERMISSIONS_XML = os.path.join(TEST_ASSET_DIR, "flow_populate_permissions.xml") -PUBLISH_XML = os.path.join(TEST_ASSET_DIR, "flow_publish.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "flow_update.xml") -REFRESH_XML = os.path.join(TEST_ASSET_DIR, "flow_refresh.xml") +GET_XML = TEST_ASSET_DIR / "flow_get.xml" +POPULATE_CONNECTIONS_XML = TEST_ASSET_DIR / "flow_populate_connections.xml" +POPULATE_PERMISSIONS_XML = TEST_ASSET_DIR / "flow_populate_permissions.xml" +PUBLISH_XML = TEST_ASSET_DIR / "flow_publish.xml" +UPDATE_XML = TEST_ASSET_DIR / "flow_update.xml" +REFRESH_XML = TEST_ASSET_DIR / "flow_refresh.xml" -class FlowTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.5" + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.3" - self.baseurl = self.server.flows.baseurl + return server - def test_download(self) -> None: - with requests_mock.mock() as m: - m.get( - self.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837/content", - headers={"Content-Disposition": 'name="tableau_flow"; filename="FlowOne.tfl"'}, - ) - file_path = self.server.flows.download("587daa37-b84d-4400-a9a2-aa90e0be7837") - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) - - def test_download_object(self) -> None: - with BytesIO() as file_object: - with requests_mock.mock() as m: - m.get( - self.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837/content", - headers={"Content-Disposition": 'name="tableau_flow"; filename="FlowOne.tfl"'}, - ) - file_path = self.server.flows.download("587daa37-b84d-4400-a9a2-aa90e0be7837", filepath=file_object) - self.assertTrue(isinstance(file_path, BytesIO)) - - def test_get(self) -> None: - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_flows, pagination_item = self.server.flows.get() - - self.assertEqual(5, pagination_item.total_available) - self.assertEqual("587daa37-b84d-4400-a9a2-aa90e0be7837", all_flows[0].id) - self.assertEqual("http://tableauserver/#/flows/1", all_flows[0].webpage_url) - self.assertEqual("2019-06-16T21:43:28Z", format_datetime(all_flows[0].created_at)) - self.assertEqual("2019-06-16T21:43:28Z", format_datetime(all_flows[0].updated_at)) - self.assertEqual("Default", all_flows[0].project_name) - self.assertEqual("FlowOne", all_flows[0].name) - self.assertEqual("aa23f4ac-906f-11e9-86fb-3f0f71412e77", all_flows[0].project_id) - self.assertEqual("7ebb3f20-0fd2-4f27-a2f6-c539470999e2", all_flows[0].owner_id) - self.assertEqual({"i_love_tags"}, all_flows[0].tags) - self.assertEqual("Descriptive", all_flows[0].description) - - self.assertEqual("5c36be69-eb30-461b-b66e-3e2a8e27cc35", all_flows[1].id) - self.assertEqual("http://tableauserver/#/flows/4", all_flows[1].webpage_url) - self.assertEqual("2019-06-18T03:08:19Z", format_datetime(all_flows[1].created_at)) - self.assertEqual("2019-06-18T03:08:19Z", format_datetime(all_flows[1].updated_at)) - self.assertEqual("Default", all_flows[1].project_name) - self.assertEqual("FlowTwo", all_flows[1].name) - self.assertEqual("aa23f4ac-906f-11e9-86fb-3f0f71412e77", all_flows[1].project_id) - self.assertEqual("9127d03f-d996-405f-b392-631b25183a0f", all_flows[1].owner_id) - - def test_update(self) -> None: - response_xml = read_xml_asset(UPDATE_XML) - with requests_mock.mock() as m: - m.put(self.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837", text=response_xml) - single_datasource = TSC.FlowItem("test", "aa23f4ac-906f-11e9-86fb-3f0f71412e77") - single_datasource.owner_id = "7ebb3f20-0fd2-4f27-a2f6-c539470999e2" - single_datasource._id = "587daa37-b84d-4400-a9a2-aa90e0be7837" - single_datasource.description = "So fun to see" - single_datasource = self.server.flows.update(single_datasource) - - self.assertEqual("587daa37-b84d-4400-a9a2-aa90e0be7837", single_datasource.id) - self.assertEqual("aa23f4ac-906f-11e9-86fb-3f0f71412e77", single_datasource.project_id) - self.assertEqual("7ebb3f20-0fd2-4f27-a2f6-c539470999e2", single_datasource.owner_id) - self.assertEqual("So fun to see", single_datasource.description) - - def test_populate_connections(self) -> None: - response_xml = read_xml_asset(POPULATE_CONNECTIONS_XML) - with requests_mock.mock() as m: - m.get(self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=response_xml) - single_datasource = TSC.FlowItem("test", "aa23f4ac-906f-11e9-86fb-3f0f71412e77") - single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - self.server.flows.populate_connections(single_datasource) - self.assertEqual("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", single_datasource.id) - connections = single_datasource.connections - - self.assertTrue(connections) - conn1, conn2, conn3 = connections - self.assertEqual("405c1e4b-60c9-499f-9c47-a4ef1af69359", conn1.id) - self.assertEqual("excel-direct", conn1.connection_type) - self.assertEqual("", conn1.server_address) - self.assertEqual("", conn1.username) - self.assertEqual(False, conn1.embed_password) - self.assertEqual("b47f41b1-2c47-41a3-8b17-a38ebe8b340c", conn2.id) - self.assertEqual("sqlserver", conn2.connection_type) - self.assertEqual("test.database.com", conn2.server_address) - self.assertEqual("bob", conn2.username) - self.assertEqual(False, conn2.embed_password) - self.assertEqual("4f4a3b78-0554-43a7-b327-9605e9df9dd2", conn3.id) - self.assertEqual("tableau-server-site", conn3.connection_type) - self.assertEqual("http://tableauserver", conn3.server_address) - self.assertEqual("sally", conn3.username) - self.assertEqual(True, conn3.embed_password) - - def test_populate_permissions(self) -> None: - with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) - single_datasource = TSC.FlowItem("test") - single_datasource._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" - - self.server.flows.populate_permissions(single_datasource) - permissions = single_datasource.permissions - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "aa42f384-906f-11e9-86fc-bb24278874b9") - self.assertDictEqual( - permissions[0].capabilities, - { - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - }, - ) - self.assertEqual(permissions[1].grantee.tag_name, "groupSet") - self.assertEqual(permissions[1].grantee.id, "7ea95a1b-6872-44d6-a969-68598a7df4a0") - self.assertDictEqual( - permissions[1].capabilities, - { - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - }, - ) +def test_download(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get( + server.flows.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837/content", + headers={"Content-Disposition": 'name="tableau_flow"; filename="FlowOne.tfl"'}, + ) + file_path = server.flows.download("587daa37-b84d-4400-a9a2-aa90e0be7837") + assert os.path.exists(file_path) is True + os.remove(file_path) - def test_publish(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - new_flow = TSC.FlowItem(name="SampleFlow", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - sample_flow = os.path.join(TEST_ASSET_DIR, "SampleFlow.tfl") - publish_mode = self.server.PublishMode.CreateNew - - new_flow = self.server.flows.publish(new_flow, sample_flow, publish_mode) - - self.assertEqual("2457c468-1b24-461a-8f95-a461b3209d32", new_flow.id) - self.assertEqual("SampleFlow", new_flow.name) - self.assertEqual("2023-01-13T09:50:55Z", format_datetime(new_flow.created_at)) - self.assertEqual("2023-01-13T09:50:55Z", format_datetime(new_flow.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_flow.project_id) - self.assertEqual("default", new_flow.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_flow.owner_id) - - def test_publish_file_object(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - - new_flow = TSC.FlowItem(name="SampleFlow", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - sample_flow = os.path.join(TEST_ASSET_DIR, "SampleFlow.tfl") - publish_mode = self.server.PublishMode.CreateNew - - with open(sample_flow, "rb") as fp: - publish_mode = self.server.PublishMode.CreateNew - - new_flow = self.server.flows.publish(new_flow, fp, publish_mode) - - self.assertEqual("2457c468-1b24-461a-8f95-a461b3209d32", new_flow.id) - self.assertEqual("SampleFlow", new_flow.name) - self.assertEqual("2023-01-13T09:50:55Z", format_datetime(new_flow.created_at)) - self.assertEqual("2023-01-13T09:50:55Z", format_datetime(new_flow.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_flow.project_id) - self.assertEqual("default", new_flow.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_flow.owner_id) - - def test_refresh(self): - with open(asset(REFRESH_XML), "rb") as f: - response_xml = f.read().decode("utf-8") +def test_download_object(server: TSC.Server) -> None: + with BytesIO() as file_object: with requests_mock.mock() as m: - m.post(self.baseurl + "/92967d2d-c7e2-46d0-8847-4802df58f484/run", text=response_xml) - flow_item = TSC.FlowItem("test") - flow_item._id = "92967d2d-c7e2-46d0-8847-4802df58f484" - refresh_job = self.server.flows.refresh(flow_item) - - self.assertEqual(refresh_job.id, "d1b2ccd0-6dfa-444a-aee4-723dbd6b7c9d") - self.assertEqual(refresh_job.mode, "Asynchronous") - self.assertEqual(refresh_job.type, "RunFlow") - self.assertEqual(format_datetime(refresh_job.created_at), "2018-05-22T13:00:29Z") - self.assertIsInstance(refresh_job.flow_run, TSC.FlowRunItem) - self.assertEqual(refresh_job.flow_run.id, "e0c3067f-2333-4eee-8028-e0a56ca496f6") - self.assertEqual(refresh_job.flow_run.flow_id, "92967d2d-c7e2-46d0-8847-4802df58f484") - self.assertEqual(format_datetime(refresh_job.flow_run.started_at), "2018-05-22T13:00:29Z") - - def test_bad_download_response(self) -> None: - with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={"Content-Disposition": '''name="tableau_flow"; filename*=UTF-8''"Sample flow.tfl"'''}, + server.flows.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837/content", + headers={"Content-Disposition": 'name="tableau_flow"; filename="FlowOne.tfl"'}, ) - file_path = self.server.flows.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) - self.assertTrue(os.path.exists(file_path)) + file_path = server.flows.download("587daa37-b84d-4400-a9a2-aa90e0be7837", filepath=file_object) + assert isinstance(file_path, BytesIO) + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.flows.baseurl, text=response_xml) + all_flows, pagination_item = server.flows.get() + + assert 5 == pagination_item.total_available + assert "587daa37-b84d-4400-a9a2-aa90e0be7837" == all_flows[0].id + assert "http://tableauserver/#/flows/1" == all_flows[0].webpage_url + assert "2019-06-16T21:43:28Z" == format_datetime(all_flows[0].created_at) + assert "2019-06-16T21:43:28Z" == format_datetime(all_flows[0].updated_at) + assert "Default" == all_flows[0].project_name + assert "FlowOne" == all_flows[0].name + assert "aa23f4ac-906f-11e9-86fb-3f0f71412e77" == all_flows[0].project_id + assert "7ebb3f20-0fd2-4f27-a2f6-c539470999e2" == all_flows[0].owner_id + assert {"i_love_tags"} == all_flows[0].tags + assert "Descriptive" == all_flows[0].description + + assert "5c36be69-eb30-461b-b66e-3e2a8e27cc35" == all_flows[1].id + assert "http://tableauserver/#/flows/4" == all_flows[1].webpage_url + assert "2019-06-18T03:08:19Z" == format_datetime(all_flows[1].created_at) + assert "2019-06-18T03:08:19Z" == format_datetime(all_flows[1].updated_at) + assert "Default" == all_flows[1].project_name + assert "FlowTwo" == all_flows[1].name + assert "aa23f4ac-906f-11e9-86fb-3f0f71412e77" == all_flows[1].project_id + assert "9127d03f-d996-405f-b392-631b25183a0f" == all_flows[1].owner_id + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.flows.baseurl + "/587daa37-b84d-4400-a9a2-aa90e0be7837", text=response_xml) + single_datasource = TSC.FlowItem("test", "aa23f4ac-906f-11e9-86fb-3f0f71412e77") + single_datasource.owner_id = "7ebb3f20-0fd2-4f27-a2f6-c539470999e2" + single_datasource._id = "587daa37-b84d-4400-a9a2-aa90e0be7837" + single_datasource.description = "So fun to see" + single_datasource = server.flows.update(single_datasource) + + assert "587daa37-b84d-4400-a9a2-aa90e0be7837" == single_datasource.id + assert "aa23f4ac-906f-11e9-86fb-3f0f71412e77" == single_datasource.project_id + assert "7ebb3f20-0fd2-4f27-a2f6-c539470999e2" == single_datasource.owner_id + assert "So fun to see" == single_datasource.description + + +def test_populate_connections(server: TSC.Server) -> None: + response_xml = POPULATE_CONNECTIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.flows.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/connections", text=response_xml) + single_datasource = TSC.FlowItem("test", "aa23f4ac-906f-11e9-86fb-3f0f71412e77") + single_datasource.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_datasource._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + server.flows.populate_connections(single_datasource) + assert "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" == single_datasource.id + connections = single_datasource.connections + + assert connections is not None + assert len(connections) > 0 + conn1, conn2, conn3 = connections + assert "405c1e4b-60c9-499f-9c47-a4ef1af69359" == conn1.id + assert "excel-direct" == conn1.connection_type + assert "" == conn1.server_address + assert "" == conn1.username + assert conn1.embed_password is False + assert "b47f41b1-2c47-41a3-8b17-a38ebe8b340c" == conn2.id + assert "sqlserver" == conn2.connection_type + assert "test.database.com" == conn2.server_address + assert "bob" == conn2.username + assert conn2.embed_password is False + assert "4f4a3b78-0554-43a7-b327-9605e9df9dd2" == conn3.id + assert "tableau-server-site" == conn3.connection_type + assert "http://tableauserver" == conn3.server_address + assert "sally" == conn3.username + assert conn3.embed_password is True + + +def test_populate_permissions(server: TSC.Server) -> None: + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.flows.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) + single_datasource = TSC.FlowItem("test") + single_datasource._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + + server.flows.populate_permissions(single_datasource) + permissions = single_datasource.permissions + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "aa42f384-906f-11e9-86fc-bb24278874b9" + assert permissions[0].capabilities == { + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + } + + assert permissions[1].grantee.tag_name == "groupSet" + assert permissions[1].grantee.id == "7ea95a1b-6872-44d6-a969-68598a7df4a0" + assert permissions[1].capabilities == { + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + } + + +def test_publish(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.flows.baseurl, text=response_xml) + + new_flow = TSC.FlowItem(name="SampleFlow", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + sample_flow = TEST_ASSET_DIR / "SampleFlow.tfl" + publish_mode = server.PublishMode.CreateNew + + new_flow = server.flows.publish(new_flow, sample_flow, publish_mode) + + assert "2457c468-1b24-461a-8f95-a461b3209d32" == new_flow.id + assert "SampleFlow" == new_flow.name + assert "2023-01-13T09:50:55Z" == format_datetime(new_flow.created_at) + assert "2023-01-13T09:50:55Z" == format_datetime(new_flow.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_flow.project_id + assert "default" == new_flow.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_flow.owner_id + + +def test_publish_file_object(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.flows.baseurl, text=response_xml) + + new_flow = TSC.FlowItem(name="SampleFlow", project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + sample_flow = os.path.join(TEST_ASSET_DIR, "SampleFlow.tfl") + publish_mode = server.PublishMode.CreateNew + + with open(sample_flow, "rb") as fp: + publish_mode = server.PublishMode.CreateNew + + new_flow = server.flows.publish(new_flow, fp, publish_mode) + + assert "2457c468-1b24-461a-8f95-a461b3209d32" == new_flow.id + assert "SampleFlow" == new_flow.name + assert "2023-01-13T09:50:55Z" == format_datetime(new_flow.created_at) + assert "2023-01-13T09:50:55Z" == format_datetime(new_flow.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_flow.project_id + assert "default" == new_flow.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_flow.owner_id + + +def test_refresh(server: TSC.Server) -> None: + response_xml = REFRESH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.flows.baseurl + "/92967d2d-c7e2-46d0-8847-4802df58f484/run", text=response_xml) + flow_item = TSC.FlowItem("test") + flow_item._id = "92967d2d-c7e2-46d0-8847-4802df58f484" + refresh_job = server.flows.refresh(flow_item) + + assert refresh_job.id == "d1b2ccd0-6dfa-444a-aee4-723dbd6b7c9d" + assert refresh_job.mode == "Asynchronous" + assert refresh_job.type == "RunFlow" + assert format_datetime(refresh_job.created_at) == "2018-05-22T13:00:29Z" + assert isinstance(refresh_job.flow_run, TSC.FlowRunItem) + assert refresh_job.flow_run.id == "e0c3067f-2333-4eee-8028-e0a56ca496f6" + assert refresh_job.flow_run.flow_id == "92967d2d-c7e2-46d0-8847-4802df58f484" + assert format_datetime(refresh_job.flow_run.started_at) == "2018-05-22T13:00:29Z" + + +def test_refresh_id_str(server: TSC.Server) -> None: + response_xml = REFRESH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.flows.baseurl + "/92967d2d-c7e2-46d0-8847-4802df58f484/run", text=response_xml) + refresh_job = server.flows.refresh("92967d2d-c7e2-46d0-8847-4802df58f484") + + assert refresh_job.id == "d1b2ccd0-6dfa-444a-aee4-723dbd6b7c9d" + assert refresh_job.mode == "Asynchronous" + assert refresh_job.type == "RunFlow" + assert format_datetime(refresh_job.created_at) == "2018-05-22T13:00:29Z" + assert isinstance(refresh_job.flow_run, TSC.FlowRunItem) + assert refresh_job.flow_run.id == "e0c3067f-2333-4eee-8028-e0a56ca496f6" + assert refresh_job.flow_run.flow_id == "92967d2d-c7e2-46d0-8847-4802df58f484" + assert format_datetime(refresh_job.flow_run.started_at) == "2018-05-22T13:00:29Z" + + +def test_bad_download_response(server: TSC.Server) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + server.flows.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={"Content-Disposition": '''name="tableau_flow"; filename*=UTF-8''"Sample flow.tfl"'''}, + ) + file_path = server.flows.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + assert os.path.exists(file_path) is True diff --git a/test/test_flowruns.py b/test/test_flowruns.py index 8af2540dc..003ee944b 100644 --- a/test/test_flowruns.py +++ b/test/test_flowruns.py @@ -1,111 +1,121 @@ +from pathlib import Path import sys -import unittest +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime from tableauserverclient.server.endpoint.exceptions import FlowRunFailedException -from ._utils import read_xml_asset, mocked_time, server_response_error_factory - -GET_XML = "flow_runs_get.xml" -GET_BY_ID_XML = "flow_runs_get_by_id.xml" -GET_BY_ID_FAILED_XML = "flow_runs_get_by_id_failed.xml" -GET_BY_ID_INPROGRESS_XML = "flow_runs_get_by_id_inprogress.xml" - - -class FlowRunTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.10" - - self.baseurl = self.server.flow_runs.baseurl - - def test_get(self) -> None: - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_flow_runs = self.server.flow_runs.get() - - self.assertEqual("cc2e652d-4a9b-4476-8c93-b238c45db968", all_flow_runs[0].id) - self.assertEqual("2021-02-11T01:42:55Z", format_datetime(all_flow_runs[0].started_at)) - self.assertEqual("2021-02-11T01:57:38Z", format_datetime(all_flow_runs[0].completed_at)) - self.assertEqual("Success", all_flow_runs[0].status) - self.assertEqual("100", all_flow_runs[0].progress) - self.assertEqual("aa23f4ac-906f-11e9-86fb-3f0f71412e77", all_flow_runs[0].background_job_id) - - self.assertEqual("a3104526-c0c6-4ea5-8362-e03fc7cbd7ee", all_flow_runs[1].id) - self.assertEqual("2021-02-13T04:05:30Z", format_datetime(all_flow_runs[1].started_at)) - self.assertEqual("2021-02-13T04:05:35Z", format_datetime(all_flow_runs[1].completed_at)) - self.assertEqual("Failed", all_flow_runs[1].status) - self.assertEqual("100", all_flow_runs[1].progress) - self.assertEqual("1ad21a9d-2530-4fbf-9064-efd3c736e023", all_flow_runs[1].background_job_id) - - def test_get_by_id(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_XML) - with requests_mock.mock() as m: - m.get(self.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968", text=response_xml) - flow_run = self.server.flow_runs.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") - - self.assertEqual("cc2e652d-4a9b-4476-8c93-b238c45db968", flow_run.id) - self.assertEqual("2021-02-11T01:42:55Z", format_datetime(flow_run.started_at)) - self.assertEqual("2021-02-11T01:57:38Z", format_datetime(flow_run.completed_at)) - self.assertEqual("Success", flow_run.status) - self.assertEqual("100", flow_run.progress) - self.assertEqual("1ad21a9d-2530-4fbf-9064-efd3c736e023", flow_run.background_job_id) - - def test_cancel_id(self) -> None: - with requests_mock.mock() as m: - m.put(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) - self.server.flow_runs.cancel("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - def test_cancel_item(self) -> None: - run = TSC.FlowRunItem() - run._id = "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - with requests_mock.mock() as m: - m.put(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) - self.server.flow_runs.cancel(run) - - def test_wait_for_job_finished(self) -> None: - # Waiting for an already finished job, directly returns that job's info - response_xml = read_xml_asset(GET_BY_ID_XML) - flow_run_id = "cc2e652d-4a9b-4476-8c93-b238c45db968" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{flow_run_id}", text=response_xml) - flow_run = self.server.flow_runs.wait_for_job(flow_run_id) - - self.assertEqual(flow_run_id, flow_run.id) - self.assertEqual(flow_run.progress, "100") - - def test_wait_for_job_failed(self) -> None: - # Waiting for a failed job raises an exception - response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) - flow_run_id = "c2b35d5a-e130-471a-aec8-7bc5435fe0e7" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{flow_run_id}", text=response_xml) - with self.assertRaises(FlowRunFailedException): - self.server.flow_runs.wait_for_job(flow_run_id) - - def test_wait_for_job_timeout(self) -> None: - # Waiting for a job which doesn't terminate will throw an exception - response_xml = read_xml_asset(GET_BY_ID_INPROGRESS_XML) - flow_run_id = "71afc22c-9c06-40be-8d0f-4c4166d29e6c" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{flow_run_id}", text=response_xml) - with self.assertRaises(TimeoutError): - self.server.flow_runs.wait_for_job(flow_run_id, timeout=30) - - def test_queryset(self) -> None: - response_xml = read_xml_asset(GET_XML) - error_response = server_response_error_factory( - "400006", "Bad Request", "0xB4EAB088 : The start index '9900' is greater than or equal to the total count.)" - ) - with requests_mock.mock() as m: - m.get(f"{self.baseurl}?pageNumber=1", text=response_xml) - m.get(f"{self.baseurl}?pageNumber=2", text=error_response) - queryset = self.server.flow_runs.all() - assert len(queryset) == sys.maxsize +from ._utils import mocked_time, server_response_error_factory + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "flow_runs_get.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "flow_runs_get_by_id.xml" +GET_BY_ID_FAILED_XML = TEST_ASSET_DIR / "flow_runs_get_by_id_failed.xml" +GET_BY_ID_INPROGRESS_XML = TEST_ASSET_DIR / "flow_runs_get_by_id_inprogress.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.10" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.flow_runs.baseurl, text=response_xml) + all_flow_runs = server.flow_runs.get() + + assert "cc2e652d-4a9b-4476-8c93-b238c45db968" == all_flow_runs[0].id + assert "2021-02-11T01:42:55Z" == format_datetime(all_flow_runs[0].started_at) + assert "2021-02-11T01:57:38Z" == format_datetime(all_flow_runs[0].completed_at) + assert "Success" == all_flow_runs[0].status + assert "100" == all_flow_runs[0].progress + assert "aa23f4ac-906f-11e9-86fb-3f0f71412e77" == all_flow_runs[0].background_job_id + + assert "a3104526-c0c6-4ea5-8362-e03fc7cbd7ee" == all_flow_runs[1].id + assert "2021-02-13T04:05:30Z" == format_datetime(all_flow_runs[1].started_at) + assert "2021-02-13T04:05:35Z" == format_datetime(all_flow_runs[1].completed_at) + assert "Failed" == all_flow_runs[1].status + assert "100" == all_flow_runs[1].progress + assert "1ad21a9d-2530-4fbf-9064-efd3c736e023" == all_flow_runs[1].background_job_id + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + m.get(server.flow_runs.baseurl + "/cc2e652d-4a9b-4476-8c93-b238c45db968", text=response_xml) + flow_run = server.flow_runs.get_by_id("cc2e652d-4a9b-4476-8c93-b238c45db968") + + assert "cc2e652d-4a9b-4476-8c93-b238c45db968" == flow_run.id + assert "2021-02-11T01:42:55Z" == format_datetime(flow_run.started_at) + assert "2021-02-11T01:57:38Z" == format_datetime(flow_run.completed_at) + assert "Success" == flow_run.status + assert "100" == flow_run.progress + assert "1ad21a9d-2530-4fbf-9064-efd3c736e023" == flow_run.background_job_id + + +def test_cancel_id(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.put(server.flow_runs.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) + server.flow_runs.cancel("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + +def test_cancel_item(server: TSC.Server) -> None: + run = TSC.FlowRunItem() + run._id = "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + with requests_mock.mock() as m: + m.put(server.flow_runs.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) + server.flow_runs.cancel(run) + + +def test_wait_for_job_finished(server: TSC.Server) -> None: + # Waiting for an already finished job, directly returns that job's info + response_xml = GET_BY_ID_XML.read_text() + flow_run_id = "cc2e652d-4a9b-4476-8c93-b238c45db968" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.flow_runs.baseurl}/{flow_run_id}", text=response_xml) + flow_run = server.flow_runs.wait_for_job(flow_run_id) + + assert flow_run_id == flow_run.id + assert flow_run.progress == "100" + + +def test_wait_for_job_failed(server: TSC.Server) -> None: + # Waiting for a failed job raises an exception + response_xml = GET_BY_ID_FAILED_XML.read_text() + flow_run_id = "c2b35d5a-e130-471a-aec8-7bc5435fe0e7" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.flow_runs.baseurl}/{flow_run_id}", text=response_xml) + with pytest.raises(FlowRunFailedException): + server.flow_runs.wait_for_job(flow_run_id) + + +def test_wait_for_job_timeout(server: TSC.Server) -> None: + # Waiting for a job which doesn't terminate will throw an exception + response_xml = GET_BY_ID_INPROGRESS_XML.read_text() + flow_run_id = "71afc22c-9c06-40be-8d0f-4c4166d29e6c" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.flow_runs.baseurl}/{flow_run_id}", text=response_xml) + with pytest.raises(TimeoutError): + server.flow_runs.wait_for_job(flow_run_id, timeout=30) + + +def test_queryset(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + error_response = server_response_error_factory( + "400006", "Bad Request", "0xB4EAB088 : The start index '9900' is greater than or equal to the total count.)" + ) + with requests_mock.mock() as m: + m.get(f"{server.flow_runs.baseurl}?pageNumber=1", text=response_xml) + m.get(f"{server.flow_runs.baseurl}?pageNumber=2", text=error_response) + queryset = server.flow_runs.all() + assert len(queryset) == sys.maxsize diff --git a/test/test_flowtask.py b/test/test_flowtask.py index 2d9f7c7bd..601446c0e 100644 --- a/test/test_flowtask.py +++ b/test/test_flowtask.py @@ -1,47 +1,46 @@ -import os -import unittest from datetime import time from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -from tableauserverclient.datetime_helpers import parse_datetime from tableauserverclient.models.task_item import TaskItem TEST_ASSET_DIR = Path(__file__).parent / "assets" -GET_XML_CREATE_FLOW_TASK_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_create_flow_task.xml") +GET_XML_CREATE_FLOW_TASK_RESPONSE = TEST_ASSET_DIR / "tasks_create_flow_task.xml" -class TaskTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.server.version = "3.22" +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) - # Fake Signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.22" - self.baseurl = self.server.flow_tasks.baseurl + return server - def test_create_flow_task(self): - monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), interval_value=15) - monthly_schedule = TSC.ScheduleItem( - "Monthly Schedule", - 50, - TSC.ScheduleItem.Type.Flow, - TSC.ScheduleItem.ExecutionOrder.Parallel, - monthly_interval, - ) - target_item = TSC.Target("flow_id", "flow") - task = TaskItem(None, "RunFlow", None, schedule_item=monthly_schedule, target=target_item) +def test_create_flow_task(server: TSC.Server) -> None: + monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), interval_value=15) + monthly_schedule = TSC.ScheduleItem( + "Monthly Schedule", + 50, + TSC.ScheduleItem.Type.Flow, + TSC.ScheduleItem.ExecutionOrder.Parallel, + monthly_interval, + ) + target_item = TSC.Target("flow_id", "flow") - with open(GET_XML_CREATE_FLOW_TASK_RESPONSE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(f"{self.baseurl}", text=response_xml) - create_response_content = self.server.flow_tasks.create(task).decode("utf-8") + task = TaskItem("", "RunFlow", 0, schedule_item=monthly_schedule, target=target_item) - self.assertTrue("schedule_id" in create_response_content) - self.assertTrue("flow_id" in create_response_content) + response_xml = GET_XML_CREATE_FLOW_TASK_RESPONSE.read_text() + with requests_mock.mock() as m: + m.post(f"{server.flow_tasks.baseurl}", text=response_xml) + create_response_content = server.flow_tasks.create(task).decode("utf-8") + + assert "schedule_id" in create_response_content + assert "flow_id" in create_response_content diff --git a/test/test_group.py b/test/test_group.py index b3de07963..734b5fa38 100644 --- a/test/test_group.py +++ b/test/test_group.py @@ -1,335 +1,342 @@ from pathlib import Path -import unittest -import os import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime -TEST_ASSET_DIR = Path(__file__).absolute().parent / "assets" +import pytest -# TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") +TEST_ASSET_DIR = Path(__file__).absolute().parent / "assets" -GET_XML = os.path.join(TEST_ASSET_DIR, "group_get.xml") +GET_XML = TEST_ASSET_DIR / "group_get.xml" GET_XML_ALL_FIELDS = TEST_ASSET_DIR / "group_get_all_fields.xml" -POPULATE_USERS = os.path.join(TEST_ASSET_DIR, "group_populate_users.xml") -POPULATE_USERS_EMPTY = os.path.join(TEST_ASSET_DIR, "group_populate_users_empty.xml") -ADD_USER = os.path.join(TEST_ASSET_DIR, "group_add_user.xml") +POPULATE_USERS = TEST_ASSET_DIR / "group_populate_users.xml" +POPULATE_USERS_EMPTY = TEST_ASSET_DIR / "group_populate_users_empty.xml" +ADD_USER = TEST_ASSET_DIR / "group_add_user.xml" ADD_USERS = TEST_ASSET_DIR / "group_add_users.xml" -ADD_USER_POPULATE = os.path.join(TEST_ASSET_DIR, "group_users_added.xml") -CREATE_GROUP = os.path.join(TEST_ASSET_DIR, "group_create.xml") -CREATE_GROUP_AD = os.path.join(TEST_ASSET_DIR, "group_create_ad.xml") -CREATE_GROUP_ASYNC = os.path.join(TEST_ASSET_DIR, "group_create_async.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "group_update.xml") +ADD_USER_POPULATE = TEST_ASSET_DIR / "group_users_added.xml" +CREATE_GROUP = TEST_ASSET_DIR / "group_create.xml" +CREATE_GROUP_AD = TEST_ASSET_DIR / "group_create_ad.xml" +CREATE_GROUP_ASYNC = TEST_ASSET_DIR / "group_create_async.xml" +UPDATE_XML = TEST_ASSET_DIR / "group_update.xml" UPDATE_ASYNC_XML = TEST_ASSET_DIR / "group_update_async.xml" -class GroupTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.groups.baseurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_groups, pagination_item = self.server.groups.get() - - self.assertEqual(3, pagination_item.total_available) - self.assertEqual("ef8b19c0-43b6-11e6-af50-63f5805dbe3c", all_groups[0].id) - self.assertEqual("All Users", all_groups[0].name) - self.assertEqual("local", all_groups[0].domain_name) - - self.assertEqual("e7833b48-c6f7-47b5-a2a7-36e7dd232758", all_groups[1].id) - self.assertEqual("Another group", all_groups[1].name) - self.assertEqual("local", all_groups[1].domain_name) - - self.assertEqual("86a66d40-f289-472a-83d0-927b0f954dc8", all_groups[2].id) - self.assertEqual("TableauExample", all_groups[2].name) - self.assertEqual("local", all_groups[2].domain_name) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.groups.get) - - def test_populate_users(self) -> None: - with open(POPULATE_USERS, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get( - self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users?pageNumber=1&pageSize=100", - text=response_xml, - complete_qs=True, - ) - single_group = TSC.GroupItem(name="Test Group") - single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - self.server.groups.populate_users(single_group) - - self.assertEqual(1, len(list(single_group.users))) - user = list(single_group.users).pop() - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", user.id) - self.assertEqual("alice", user.name) - self.assertEqual("Publisher", user.site_role) - self.assertEqual("2016-08-16T23:17:06Z", format_datetime(user.last_login)) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758", status_code=204) - self.server.groups.delete("e7833b48-c6f7-47b5-a2a7-36e7dd232758") - - def test_remove_user(self) -> None: - with open(POPULATE_USERS, "rb") as f: - response_xml_populate = f.read().decode("utf-8") - - with open(POPULATE_USERS_EMPTY, "rb") as f: - response_xml_empty = f.read().decode("utf-8") - - with requests_mock.mock() as m: - url = self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users" "/dd2239f6-ddf1-4107-981a-4cf94e415794" - - m.delete(url, status_code=204) - # We register the get endpoint twice. The first time we have 1 user, the second we have 'removed' them. - m.get(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_populate) - - single_group = TSC.GroupItem("test") - single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - self.server.groups.populate_users(single_group) - self.assertEqual(1, len(list(single_group.users))) - self.server.groups.remove_user(single_group, "dd2239f6-ddf1-4107-981a-4cf94e415794") - - m.get(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_empty) - self.assertEqual(0, len(list(single_group.users))) - - def test_add_user(self) -> None: - with open(ADD_USER, "rb") as f: - response_xml_add = f.read().decode("utf-8") - with open(ADD_USER_POPULATE, "rb") as f: - response_xml_populate = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_add) - m.get(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_populate) - single_group = TSC.GroupItem("test") - single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - - self.server.groups.add_user(single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7") - self.server.groups.populate_users(single_group) - self.assertEqual(1, len(list(single_group.users))) - user = list(single_group.users).pop() - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", user.id) - self.assertEqual("testuser", user.name) - self.assertEqual("ServerAdministrator", user.site_role) - - def test_add_users(self) -> None: - self.server.version = "3.21" - self.baseurl = self.server.groups.baseurl - - def make_user(id: str, name: str, siteRole: str) -> TSC.UserItem: - user = TSC.UserItem(name, siteRole) - user._id = id - return user - - users = [ - make_user(id="5de011f8-4aa9-4d5b-b991-f464c8dd6bb7", name="Alice", siteRole="ServerAdministrator"), - make_user(id="5de011f8-3aa9-4d5b-b991-f467c8dd6bb8", name="Bob", siteRole="Explorer"), - make_user(id="5de011f8-2aa9-4d5b-b991-f466c8dd6bb8", name="Charlie", siteRole="Viewer"), - ] - group = TSC.GroupItem("test") - group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - - with requests_mock.mock() as m: - m.post(f"{self.baseurl}/{group.id}/users", text=ADD_USERS.read_text()) - resp_users = self.server.groups.add_users(group, users) - - for user, resp_user in zip(users, resp_users): - with self.subTest(user=user, resp_user=resp_user): - assert user.id == resp_user.id - assert user.name == resp_user.name - assert user.site_role == resp_user.site_role - - def test_remove_users(self) -> None: - self.server.version = "3.21" - self.baseurl = self.server.groups.baseurl - - def make_user(id: str, name: str, siteRole: str) -> TSC.UserItem: - user = TSC.UserItem(name, siteRole) - user._id = id - return user - - users = [ - make_user(id="5de011f8-4aa9-4d5b-b991-f464c8dd6bb7", name="Alice", siteRole="ServerAdministrator"), - make_user(id="5de011f8-3aa9-4d5b-b991-f467c8dd6bb8", name="Bob", siteRole="Explorer"), - make_user(id="5de011f8-2aa9-4d5b-b991-f466c8dd6bb8", name="Charlie", siteRole="Viewer"), - ] - group = TSC.GroupItem("test") - group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{group.id}/users/remove") - self.server.groups.remove_users(group, users) - - def test_add_user_before_populating(self) -> None: - with open(GET_XML, "rb") as f: - get_xml_response = f.read().decode("utf-8") - with open(ADD_USER, "rb") as f: - add_user_response = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=get_xml_response) - m.post( - self.baseurl + "/ef8b19c0-43b6-11e6-af50-63f5805dbe3c/users", - text=add_user_response, - ) - all_groups, pagination_item = self.server.groups.get() - single_group = all_groups[0] - self.server.groups.add_user(single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7") - - def test_add_user_missing_user_id(self) -> None: - with open(POPULATE_USERS, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml) - single_group = TSC.GroupItem(name="Test Group") - single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - self.server.groups.populate_users(single_group) - - self.assertRaises(ValueError, self.server.groups.add_user, single_group, "") - - def test_add_user_missing_group_id(self) -> None: +@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_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.groups.baseurl, text=response_xml) + all_groups, pagination_item = server.groups.get() + + assert 3 == pagination_item.total_available + assert "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" == all_groups[0].id + assert "All Users" == all_groups[0].name + assert "local" == all_groups[0].domain_name + + assert "e7833b48-c6f7-47b5-a2a7-36e7dd232758" == all_groups[1].id + assert "Another group" == all_groups[1].name + assert "local" == all_groups[1].domain_name + + assert "86a66d40-f289-472a-83d0-927b0f954dc8" == all_groups[2].id + assert "TableauExample" == all_groups[2].name + assert "local" == all_groups[2].domain_name + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.groups.get() + + +def test_populate_users(server: TSC.Server) -> None: + response_xml = POPULATE_USERS.read_text() + with requests_mock.mock() as m: + m.get( + server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users?pageNumber=1&pageSize=100", + text=response_xml, + complete_qs=True, + ) + single_group = TSC.GroupItem(name="Test Group") + single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + server.groups.populate_users(single_group) + + assert 1 == len(list(single_group.users)) + user = list(single_group.users).pop() + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == user.id + assert "alice" == user.name + assert "Publisher" == user.site_role + assert "2016-08-16T23:17:06Z" == format_datetime(user.last_login) + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758", status_code=204) + server.groups.delete("e7833b48-c6f7-47b5-a2a7-36e7dd232758") + + +def test_remove_user(server: TSC.Server) -> None: + response_xml_populate = POPULATE_USERS.read_text() + + response_xml_empty = POPULATE_USERS_EMPTY.read_text() + + with requests_mock.mock() as m: + url = ( + server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users" + "/dd2239f6-ddf1-4107-981a-4cf94e415794" + ) + + m.delete(url, status_code=204) + # We register the get endpoint twice. The first time we have 1 user, the second we have 'removed' them. + m.get(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_populate) + + single_group = TSC.GroupItem("test") + single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + server.groups.populate_users(single_group) + assert 1 == len(list(single_group.users)) + server.groups.remove_user(single_group, "dd2239f6-ddf1-4107-981a-4cf94e415794") + + m.get(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_empty) + assert 0 == len(list(single_group.users)) + + +def test_add_user(server: TSC.Server) -> None: + response_xml_add = ADD_USER.read_text() + response_xml_populate = ADD_USER_POPULATE.read_text() + with requests_mock.mock() as m: + m.post(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_add) + m.get(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml_populate) single_group = TSC.GroupItem("test") - self.assertRaises( - TSC.MissingRequiredFieldError, - self.server.groups.add_user, + single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + + server.groups.add_user(single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7") + server.groups.populate_users(single_group) + assert 1 == len(list(single_group.users)) + user = list(single_group.users).pop() + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == user.id + assert "testuser" == user.name + assert "ServerAdministrator" == user.site_role + + +def test_add_users(server: TSC.Server) -> None: + server.version = "3.21" + + def make_user(id: str, name: str, siteRole: str) -> TSC.UserItem: + user = TSC.UserItem(name, siteRole) + user._id = id + return user + + users = [ + make_user(id="5de011f8-4aa9-4d5b-b991-f464c8dd6bb7", name="Alice", siteRole="ServerAdministrator"), + make_user(id="5de011f8-3aa9-4d5b-b991-f467c8dd6bb8", name="Bob", siteRole="Explorer"), + make_user(id="5de011f8-2aa9-4d5b-b991-f466c8dd6bb8", name="Charlie", siteRole="Viewer"), + ] + group = TSC.GroupItem("test") + group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + + with requests_mock.mock() as m: + m.post(f"{server.groups.baseurl}/{group.id}/users", text=ADD_USERS.read_text()) + resp_users = server.groups.add_users(group, users) + + for user, resp_user in zip(users, resp_users): + assert user.id == resp_user.id + assert user.name == resp_user.name + assert user.site_role == resp_user.site_role + + +def test_remove_users(server: TSC.Server) -> None: + server.version = "3.21" + + def make_user(id: str, name: str, siteRole: str) -> TSC.UserItem: + user = TSC.UserItem(name, siteRole) + user._id = id + return user + + users = [ + make_user(id="5de011f8-4aa9-4d5b-b991-f464c8dd6bb7", name="Alice", siteRole="ServerAdministrator"), + make_user(id="5de011f8-3aa9-4d5b-b991-f467c8dd6bb8", name="Bob", siteRole="Explorer"), + make_user(id="5de011f8-2aa9-4d5b-b991-f466c8dd6bb8", name="Charlie", siteRole="Viewer"), + ] + group = TSC.GroupItem("test") + group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + + with requests_mock.mock() as m: + m.put(f"{server.groups.baseurl}/{group.id}/users/remove") + server.groups.remove_users(group, users) + + +def test_add_user_before_populating(server: TSC.Server) -> None: + get_xml_response = GET_XML.read_text() + add_user_response = ADD_USER.read_text() + with requests_mock.mock() as m: + m.get(server.groups.baseurl, text=get_xml_response) + m.post( + server.groups.baseurl + "/ef8b19c0-43b6-11e6-af50-63f5805dbe3c/users", + text=add_user_response, + ) + all_groups, pagination_item = server.groups.get() + single_group = all_groups[0] + server.groups.add_user(single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7") + + +def test_add_user_missing_user_id(server: TSC.Server) -> None: + response_xml = POPULATE_USERS.read_text() + with requests_mock.mock() as m: + m.get(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml) + single_group = TSC.GroupItem(name="Test Group") + single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + server.groups.populate_users(single_group) + + with pytest.raises(ValueError): + server.groups.add_user(single_group, "") + + +def test_add_user_missing_group_id(server: TSC.Server) -> None: + single_group = TSC.GroupItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.groups.add_user( single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", ) - def test_remove_user_before_populating(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - m.delete( - self.baseurl + "/ef8b19c0-43b6-11e6-af50-63f5805dbe3c/users/5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", - text="ok", - ) - all_groups, pagination_item = self.server.groups.get() - single_group = all_groups[0] - self.server.groups.remove_user(single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7") - - def test_remove_user_missing_user_id(self) -> None: - with open(POPULATE_USERS, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml) - single_group = TSC.GroupItem(name="Test Group") - single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" - self.server.groups.populate_users(single_group) - - self.assertRaises(ValueError, self.server.groups.remove_user, single_group, "") - - def test_remove_user_missing_group_id(self) -> None: - single_group = TSC.GroupItem("test") - self.assertRaises( - TSC.MissingRequiredFieldError, - self.server.groups.remove_user, + +def test_remove_user_before_populating(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.groups.baseurl, text=response_xml) + m.delete( + server.groups.baseurl + "/ef8b19c0-43b6-11e6-af50-63f5805dbe3c/users/5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", + text="ok", + ) + all_groups, pagination_item = server.groups.get() + single_group = all_groups[0] + server.groups.remove_user(single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7") + + +def test_remove_user_missing_user_id(server: TSC.Server) -> None: + response_xml = POPULATE_USERS.read_text() + with requests_mock.mock() as m: + m.get(server.groups.baseurl + "/e7833b48-c6f7-47b5-a2a7-36e7dd232758/users", text=response_xml) + single_group = TSC.GroupItem(name="Test Group") + single_group._id = "e7833b48-c6f7-47b5-a2a7-36e7dd232758" + server.groups.populate_users(single_group) + + with pytest.raises(ValueError): + server.groups.remove_user(single_group, "") + + +def test_remove_user_missing_group_id(server: TSC.Server) -> None: + single_group = TSC.GroupItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.groups.remove_user( single_group, "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", ) - def test_create_group(self) -> None: - with open(CREATE_GROUP, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - group_to_create = TSC.GroupItem("試供品") - group = self.server.groups.create(group_to_create) - self.assertEqual(group.name, "試供品") - self.assertEqual(group.id, "3e4a9ea0-a07a-4fe6-b50f-c345c8c81034") - - def test_create_ad_group(self) -> None: - with open(CREATE_GROUP_AD, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - group_to_create = TSC.GroupItem("試供品") - group_to_create.domain_name = "just-has-to-exist" - group = self.server.groups.create_AD_group(group_to_create, False) - self.assertEqual(group.name, "試供品") - self.assertEqual(group.license_mode, "onLogin") - self.assertEqual(group.minimum_site_role, "Creator") - self.assertEqual(group.domain_name, "active-directory-domain-name") - - def test_create_group_async(self) -> None: - with open(CREATE_GROUP_ASYNC, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - group_to_create = TSC.GroupItem("試供品") - group_to_create.domain_name = "woohoo" - job = self.server.groups.create_AD_group(group_to_create, True) - self.assertEqual(job.mode, "Asynchronous") - self.assertEqual(job.type, "GroupImport") - - def test_update(self) -> None: - with open(UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/ef8b19c0-43b6-11e6-af50-63f5805dbe3c", text=response_xml) - group = TSC.GroupItem(name="Test Group") - group._domain_name = "local" - group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" - group = self.server.groups.update(group) - - self.assertEqual("ef8b19c0-43b6-11e6-af50-63f5805dbe3c", group.id) - self.assertEqual("Group updated name", group.name) - self.assertEqual("ExplorerCanPublish", group.minimum_site_role) - self.assertEqual("onLogin", group.license_mode) - - # async update is not supported for local groups - def test_update_local_async(self) -> None: - group = TSC.GroupItem("myGroup") - group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" - self.assertRaises(ValueError, self.server.groups.update, group, as_job=True) - - # mimic group returned from server where domain name is set to 'local' - group.domain_name = "local" - self.assertRaises(ValueError, self.server.groups.update, group, as_job=True) - def test_update_ad_async(self) -> None: - group = TSC.GroupItem("myGroup", "example.com") +def test_create_group(server: TSC.Server) -> None: + response_xml = CREATE_GROUP.read_text(encoding="utf-8") + with requests_mock.mock() as m: + m.post(server.groups.baseurl, text=response_xml) + group_to_create = TSC.GroupItem("試供品") + group = server.groups.create(group_to_create) + assert group.name == "試供品" + assert group.id == "3e4a9ea0-a07a-4fe6-b50f-c345c8c81034" + + +def test_create_ad_group(server: TSC.Server) -> None: + response_xml = CREATE_GROUP_AD.read_bytes().decode("utf8") + with requests_mock.mock() as m: + m.post(server.groups.baseurl, text=response_xml) + group_to_create = TSC.GroupItem("試供品") + group_to_create.domain_name = "just-has-to-exist" + group = server.groups.create_AD_group(group_to_create, False) + assert group.name == "試供品" + assert group.license_mode == "onLogin" + assert group.minimum_site_role == "Creator" + assert group.domain_name == "active-directory-domain-name" + + +def test_create_group_async(server: TSC.Server) -> None: + response_xml = CREATE_GROUP_ASYNC.read_text() + with requests_mock.mock() as m: + m.post(server.groups.baseurl, text=response_xml) + group_to_create = TSC.GroupItem("試供品") + group_to_create.domain_name = "woohoo" + job = server.groups.create_AD_group(group_to_create, True) + assert job.mode == "Asynchronous" + assert job.type == "GroupImport" + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.groups.baseurl + "/ef8b19c0-43b6-11e6-af50-63f5805dbe3c", text=response_xml) + group = TSC.GroupItem(name="Test Group") + group._domain_name = "local" group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" - group.minimum_site_role = TSC.UserItem.Roles.Viewer - - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{group.id}?asJob=True", text=UPDATE_ASYNC_XML.read_bytes().decode("utf8")) - job = self.server.groups.update(group, as_job=True) - - self.assertEqual(job.id, "c2566efc-0767-4f15-89cb-56acb4349c1b") - self.assertEqual(job.mode, "Asynchronous") - self.assertEqual(job.type, "GroupSync") - - def test_get_all_fields(self) -> None: - ro = TSC.RequestOptions() - ro.all_fields = True - self.server.version = "3.21" - self.baseurl = self.server.groups.baseurl - with requests_mock.mock() as m: - m.get(f"{self.baseurl}?fields=_all_", text=GET_XML_ALL_FIELDS.read_text()) - groups, pages = self.server.groups.get(req_options=ro) - - assert pages.total_available == 3 - assert len(groups) == 3 - assert groups[0].id == "28c5b855-16df-482f-ad0b-428c1df58859" - assert groups[0].name == "All Users" - assert groups[0].user_count == 2 - assert groups[0].domain_name == "local" - assert groups[1].id == "ace1ee2d-e7dd-4d7a-9504-a1ccaa5212ea" - assert groups[1].name == "group1" - assert groups[1].user_count == 1 - assert groups[2].id == "baf0ed9d-c25d-4114-97ed-5232b8a732fd" - assert groups[2].name == "test" - assert groups[2].user_count == 0 + group = server.groups.update(group) + + assert "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" == group.id + assert "Group updated name" == group.name + assert "ExplorerCanPublish" == group.minimum_site_role + assert "onLogin" == group.license_mode + + +# async update is not supported for local groups +def test_update_local_async(server: TSC.Server) -> None: + group = TSC.GroupItem("myGroup") + group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" + with pytest.raises(ValueError): + server.groups.update(group, as_job=True) + + # mimic group returned from server where domain name is set to 'local' + group.domain_name = "local" + with pytest.raises(ValueError): + server.groups.update(group, as_job=True) + + +def test_update_ad_async(server: TSC.Server) -> None: + group = TSC.GroupItem("myGroup", "example.com") + group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" + group.minimum_site_role = TSC.UserItem.Roles.Viewer + + with requests_mock.mock() as m: + m.put(f"{server.groups.baseurl}/{group.id}?asJob=True", text=UPDATE_ASYNC_XML.read_bytes().decode("utf8")) + job = server.groups.update(group, as_job=True) + + assert job.id == "c2566efc-0767-4f15-89cb-56acb4349c1b" + assert job.mode == "Asynchronous" + assert job.type == "GroupSync" + + +def test_get_all_fields(server: TSC.Server) -> None: + ro = TSC.RequestOptions() + ro.all_fields = True + server.version = "3.21" + with requests_mock.mock() as m: + m.get(f"{server.groups.baseurl}?fields=_all_", text=GET_XML_ALL_FIELDS.read_text()) + groups, pages = server.groups.get(req_options=ro) + + assert pages.total_available == 3 + assert len(groups) == 3 + assert groups[0].id == "28c5b855-16df-482f-ad0b-428c1df58859" + assert groups[0].name == "All Users" + assert groups[0].user_count == 2 + assert groups[0].domain_name == "local" + assert groups[1].id == "ace1ee2d-e7dd-4d7a-9504-a1ccaa5212ea" + assert groups[1].name == "group1" + assert groups[1].user_count == 1 + assert groups[2].id == "baf0ed9d-c25d-4114-97ed-5232b8a732fd" + assert groups[2].name == "test" + assert groups[2].user_count == 0 diff --git a/test/test_group_model.py b/test/test_group_model.py index 659a3611f..6ca2f6b25 100644 --- a/test/test_group_model.py +++ b/test/test_group_model.py @@ -1,15 +1,15 @@ -import unittest +import pytest import tableauserverclient as TSC -class GroupModelTests(unittest.TestCase): - def test_invalid_minimum_site_role(self): - group = TSC.GroupItem("grp") - with self.assertRaises(ValueError): - group.minimum_site_role = "Captain" +def test_invalid_minimum_site_role(): + group = TSC.GroupItem("grp") + with pytest.raises(ValueError): + group.minimum_site_role = "Captain" - def test_invalid_license_mode(self): - group = TSC.GroupItem("grp") - with self.assertRaises(ValueError): - group.license_mode = "off" + +def test_invalid_license_mode(): + group = TSC.GroupItem("grp") + with pytest.raises(ValueError): + group.license_mode = "off" diff --git a/test/test_groupsets.py b/test/test_groupsets.py index 5479809d2..e8276d803 100644 --- a/test/test_groupsets.py +++ b/test/test_groupsets.py @@ -1,7 +1,6 @@ from pathlib import Path -import unittest -from defusedxml.ElementTree import fromstring +import pytest import requests_mock import tableauserverclient as TSC @@ -14,126 +13,134 @@ GROUPSET_UPDATE = TEST_ASSET_DIR / "groupsets_get_by_id.xml" -class TestGroupSets(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - self.server.version = "3.22" - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.group_sets.baseurl - - def test_get(self) -> None: - with requests_mock.mock() as m: - m.get(self.baseurl, text=GROUPSETS_GET.read_text()) - groupsets, pagination_item = self.server.group_sets.get() - - assert len(groupsets) == 3 - assert pagination_item.total_available == 3 - assert groupsets[0].id == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - assert groupsets[0].name == "All Users" - assert groupsets[0].group_count == 1 - assert groupsets[0].groups[0].name == "group-one" - assert groupsets[0].groups[0].id == "gs-1" - - assert groupsets[1].id == "9a8a7b6b-5c4c-3d2d-1e0e-9a8a7b6b5b4b" - assert groupsets[1].name == "active-directory-group-import" - assert groupsets[1].group_count == 1 - assert groupsets[1].groups[0].name == "group-two" - assert groupsets[1].groups[0].id == "gs21" - - assert groupsets[2].id == "7b6b59a8-ac3c-4d1d-2e9e-0b5b4ba8a7b6" - assert groupsets[2].name == "local-group-license-on-login" - assert groupsets[2].group_count == 1 - assert groupsets[2].groups[0].name == "group-three" - assert groupsets[2].groups[0].id == "gs-3" - - def test_get_by_id(self) -> None: - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", text=GROUPSET_GET_BY_ID.read_text()) - groupset = self.server.group_sets.get_by_id("1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d") - - assert groupset.id == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - assert groupset.name == "All Users" - assert groupset.group_count == 3 - assert len(groupset.groups) == 3 - - assert groupset.groups[0].name == "group-one" - assert groupset.groups[0].id == "gs-1" - assert groupset.groups[1].name == "group-two" - assert groupset.groups[1].id == "gs21" - assert groupset.groups[2].name == "group-three" - assert groupset.groups[2].id == "gs-3" - - def test_update(self) -> None: - id_ = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - groupset = TSC.GroupSetItem("All Users") - groupset.id = id_ - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{id_}", text=GROUPSET_UPDATE.read_text()) - groupset = self.server.group_sets.update(groupset) - - assert groupset.id == id_ - assert groupset.name == "All Users" - assert groupset.group_count == 3 - assert len(groupset.groups) == 3 - - assert groupset.groups[0].name == "group-one" - assert groupset.groups[0].id == "gs-1" - assert groupset.groups[1].name == "group-two" - assert groupset.groups[1].id == "gs21" - assert groupset.groups[2].name == "group-three" - assert groupset.groups[2].id == "gs-3" - - def test_create(self) -> None: - groupset = TSC.GroupSetItem("All Users") - with requests_mock.mock() as m: - m.post(self.baseurl, text=GROUPSET_CREATE.read_text()) - groupset = self.server.group_sets.create(groupset) - - assert groupset.id == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - assert groupset.name == "All Users" - assert groupset.group_count == 0 - assert len(groupset.groups) == 0 - - def test_add_group(self) -> None: - groupset = TSC.GroupSetItem("All") - groupset.id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - group = TSC.GroupItem("Example") - group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" - - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{groupset.id}/groups/{group._id}") - self.server.group_sets.add_group(groupset, group) - - history = m.request_history - - assert len(history) == 1 - assert history[0].method == "PUT" - assert history[0].url == f"{self.baseurl}/{groupset.id}/groups/{group._id}" - - def test_remove_group(self) -> None: - groupset = TSC.GroupSetItem("All") - groupset.id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - group = TSC.GroupItem("Example") - group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" - - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{groupset.id}/groups/{group._id}") - self.server.group_sets.remove_group(groupset, group) - - history = m.request_history - - assert len(history) == 1 - assert history[0].method == "DELETE" - assert history[0].url == f"{self.baseurl}/{groupset.id}/groups/{group._id}" - - def test_as_reference(self) -> None: - groupset = TSC.GroupSetItem() - groupset.id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" - ref = groupset.as_reference(groupset.id) - assert ref.id == groupset.id - assert ref.tag_name == groupset.tag_name - assert isinstance(ref, ResourceReference) +@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" + server.version = "3.22" + + return server + + +def test_get(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.group_sets.baseurl, text=GROUPSETS_GET.read_text()) + groupsets, pagination_item = server.group_sets.get() + + assert len(groupsets) == 3 + assert pagination_item.total_available == 3 + assert groupsets[0].id == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + assert groupsets[0].name == "All Users" + assert groupsets[0].group_count == 1 + assert groupsets[0].groups[0].name == "group-one" + assert groupsets[0].groups[0].id == "gs-1" + + assert groupsets[1].id == "9a8a7b6b-5c4c-3d2d-1e0e-9a8a7b6b5b4b" + assert groupsets[1].name == "active-directory-group-import" + assert groupsets[1].group_count == 1 + assert groupsets[1].groups[0].name == "group-two" + assert groupsets[1].groups[0].id == "gs21" + + assert groupsets[2].id == "7b6b59a8-ac3c-4d1d-2e9e-0b5b4ba8a7b6" + assert groupsets[2].name == "local-group-license-on-login" + assert groupsets[2].group_count == 1 + assert groupsets[2].groups[0].name == "group-three" + assert groupsets[2].groups[0].id == "gs-3" + + +def test_get_by_id(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(f"{server.group_sets.baseurl}/1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d", text=GROUPSET_GET_BY_ID.read_text()) + groupset = server.group_sets.get_by_id("1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d") + + assert groupset.id == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + assert groupset.name == "All Users" + assert groupset.group_count == 3 + assert len(groupset.groups) == 3 + + assert groupset.groups[0].name == "group-one" + assert groupset.groups[0].id == "gs-1" + assert groupset.groups[1].name == "group-two" + assert groupset.groups[1].id == "gs21" + assert groupset.groups[2].name == "group-three" + assert groupset.groups[2].id == "gs-3" + + +def test_update(server: TSC.Server) -> None: + id_ = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + groupset = TSC.GroupSetItem("All Users") + groupset.id = id_ + with requests_mock.mock() as m: + m.put(f"{server.group_sets.baseurl}/{id_}", text=GROUPSET_UPDATE.read_text()) + groupset = server.group_sets.update(groupset) + + assert groupset.id == id_ + assert groupset.name == "All Users" + assert groupset.group_count == 3 + assert len(groupset.groups) == 3 + + assert groupset.groups[0].name == "group-one" + assert groupset.groups[0].id == "gs-1" + assert groupset.groups[1].name == "group-two" + assert groupset.groups[1].id == "gs21" + assert groupset.groups[2].name == "group-three" + assert groupset.groups[2].id == "gs-3" + + +def test_create(server: TSC.Server) -> None: + groupset = TSC.GroupSetItem("All Users") + with requests_mock.mock() as m: + m.post(server.group_sets.baseurl, text=GROUPSET_CREATE.read_text()) + groupset = server.group_sets.create(groupset) + + assert groupset.id == "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + assert groupset.name == "All Users" + assert groupset.group_count == 0 + assert len(groupset.groups) == 0 + + +def test_add_group(server: TSC.Server) -> None: + groupset = TSC.GroupSetItem("All") + groupset.id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + group = TSC.GroupItem("Example") + group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" + + with requests_mock.mock() as m: + m.put(f"{server.group_sets.baseurl}/{groupset.id}/groups/{group._id}") + server.group_sets.add_group(groupset, group) + + history = m.request_history + + assert len(history) == 1 + assert history[0].method == "PUT" + assert history[0].url == f"{server.group_sets.baseurl}/{groupset.id}/groups/{group._id}" + + +def test_remove_group(server: TSC.Server) -> None: + groupset = TSC.GroupSetItem("All") + groupset.id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + group = TSC.GroupItem("Example") + group._id = "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" + + with requests_mock.mock() as m: + m.delete(f"{server.group_sets.baseurl}/{groupset.id}/groups/{group._id}") + server.group_sets.remove_group(groupset, group) + + history = m.request_history + + assert len(history) == 1 + assert history[0].method == "DELETE" + assert history[0].url == f"{server.group_sets.baseurl}/{groupset.id}/groups/{group._id}" + + +def test_as_reference(server: TSC.Server) -> None: + groupset = TSC.GroupSetItem() + groupset.id = "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d" + ref = groupset.as_reference(groupset.id) + assert ref.id == groupset.id + assert ref.tag_name == groupset.tag_name + assert isinstance(ref, ResourceReference) diff --git a/test/test_job.py b/test/test_job.py index b3d7007aa..fa17b9953 100644 --- a/test/test_job.py +++ b/test/test_job.py @@ -1,158 +1,174 @@ -import os -import unittest from datetime import datetime +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import utc from tableauserverclient.server.endpoint.exceptions import JobFailedException -from ._utils import read_xml_asset, mocked_time - -GET_XML = "job_get.xml" -GET_BY_ID_XML = "job_get_by_id.xml" -GET_BY_ID_COMPLETED_XML = "job_get_by_id_completed.xml" -GET_BY_ID_FAILED_XML = "job_get_by_id_failed.xml" -GET_BY_ID_CANCELLED_XML = "job_get_by_id_cancelled.xml" -GET_BY_ID_INPROGRESS_XML = "job_get_by_id_inprogress.xml" -GET_BY_ID_WORKBOOK = "job_get_by_id_failed_workbook.xml" - - -class JobTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - self.server.version = "3.1" - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.jobs.baseurl - - def test_get(self) -> None: - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_jobs, pagination_item = self.server.jobs.get() - job = all_jobs[0] - created_at = datetime(2018, 5, 22, 13, 0, 29, tzinfo=utc) - started_at = datetime(2018, 5, 22, 13, 0, 37, tzinfo=utc) - ended_at = datetime(2018, 5, 22, 13, 0, 45, tzinfo=utc) - - self.assertEqual(1, pagination_item.total_available) - self.assertEqual("2eef4225-aa0c-41c4-8662-a76d89ed7336", job.id) - self.assertEqual("Success", job.status) - self.assertEqual("50", job.priority) - self.assertEqual("single_subscription_notify", job.type) - self.assertEqual(created_at, job.created_at) - self.assertEqual(started_at, job.started_at) - self.assertEqual(ended_at, job.ended_at) - - def test_get_by_id(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_XML) - job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.get_by_id(job_id) - updated_at = datetime(2020, 5, 13, 20, 25, 18, tzinfo=utc) - - self.assertEqual(job_id, job.id) - self.assertEqual(updated_at, job.updated_at) - self.assertListEqual(job.notes, ["Job detail notes"]) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.jobs.get) - - def test_cancel_id(self) -> None: - with requests_mock.mock() as m: - m.put(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) - self.server.jobs.cancel("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - def test_cancel_item(self) -> None: +from ._utils import mocked_time + + +TEST_ASSET_DIR = Path(__file__).parent / "assets" +GET_XML = TEST_ASSET_DIR / "job_get.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "job_get_by_id.xml" +GET_BY_ID_COMPLETED_XML = TEST_ASSET_DIR / "job_get_by_id_completed.xml" +GET_BY_ID_FAILED_XML = TEST_ASSET_DIR / "job_get_by_id_failed.xml" +GET_BY_ID_CANCELLED_XML = TEST_ASSET_DIR / "job_get_by_id_cancelled.xml" +GET_BY_ID_INPROGRESS_XML = TEST_ASSET_DIR / "job_get_by_id_inprogress.xml" +GET_BY_ID_WORKBOOK = TEST_ASSET_DIR / "job_get_by_id_failed_workbook.xml" + + +@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" + server.version = "3.1" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.jobs.baseurl, text=response_xml) + all_jobs, pagination_item = server.jobs.get() + job = all_jobs[0] created_at = datetime(2018, 5, 22, 13, 0, 29, tzinfo=utc) started_at = datetime(2018, 5, 22, 13, 0, 37, tzinfo=utc) - job = TSC.JobItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "backgroundJob", "0", created_at, started_at, None, 0) - with requests_mock.mock() as m: - m.put(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) - self.server.jobs.cancel(job) - - def test_wait_for_job_finished(self) -> None: - # Waiting for an already finished job, directly returns that job's info - response_xml = read_xml_asset(GET_BY_ID_XML) - job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.wait_for_job(job_id) - - self.assertEqual(job_id, job.id) - self.assertListEqual(job.notes, ["Job detail notes"]) - - def test_wait_for_job_completed(self) -> None: - # Waiting for a bridge (cloud) job completion - response_xml = read_xml_asset(GET_BY_ID_COMPLETED_XML) - job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.wait_for_job(job_id) - - self.assertEqual(job_id, job.id) - self.assertListEqual(job.notes, ["Job detail notes"]) - - def test_wait_for_job_failed(self) -> None: - # Waiting for a failed job raises an exception - response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) - job_id = "77d5e57a-2517-479f-9a3c-a32025f2b64d" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - with self.assertRaises(JobFailedException): - self.server.jobs.wait_for_job(job_id) - - def test_wait_for_job_timeout(self) -> None: - # Waiting for a job which doesn't terminate will throw an exception - response_xml = read_xml_asset(GET_BY_ID_INPROGRESS_XML) - job_id = "77d5e57a-2517-479f-9a3c-a32025f2b64d" - with mocked_time(), requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - with self.assertRaises(TimeoutError): - self.server.jobs.wait_for_job(job_id, timeout=30) - - def test_get_job_datasource_id(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) - job_id = "777bf7c4-421d-4b2c-a518-11b90187c545" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.get_by_id(job_id) - self.assertEqual(job.datasource_id, "03b9fbec-81f6-4160-ae49-5f9f6d412758") - - def test_get_job_workbook_id(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_WORKBOOK) - job_id = "bb1aab79-db54-4e96-9dd3-461d8f081d08" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.get_by_id(job_id) - self.assertEqual(job.workbook_id, "5998aaaf-1abe-4d38-b4d9-bc53e85bdd13") - - def test_get_job_workbook_name(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_WORKBOOK) - job_id = "bb1aab79-db54-4e96-9dd3-461d8f081d08" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.get_by_id(job_id) - self.assertEqual(job.workbook_name, "Superstore") - - def test_get_job_datasource_name(self) -> None: - response_xml = read_xml_asset(GET_BY_ID_FAILED_XML) - job_id = "777bf7c4-421d-4b2c-a518-11b90187c545" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{job_id}", text=response_xml) - job = self.server.jobs.get_by_id(job_id) - self.assertEqual(job.datasource_name, "World Indicators") - - def test_background_job_str(self) -> None: - job = TSC.BackgroundJobItem( - "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", datetime.now(), 1, "extractRefresh", "Failed" - ) - assert not str(job).startswith("< None: + response_xml = GET_BY_ID_XML.read_text() + job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" + with requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.get_by_id(job_id) + updated_at = datetime(2020, 5, 13, 20, 25, 18, tzinfo=utc) + + assert job_id == job.id + assert updated_at == job.updated_at + assert job.notes == ["Job detail notes"] + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.jobs.get() + + +def test_cancel_id(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.put(server.jobs.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) + server.jobs.cancel("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + +def test_cancel_item(server: TSC.Server) -> None: + created_at = datetime(2018, 5, 22, 13, 0, 29, tzinfo=utc) + started_at = datetime(2018, 5, 22, 13, 0, 37, tzinfo=utc) + job = TSC.JobItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", "backgroundJob", "0", created_at, started_at, None, 0) + with requests_mock.mock() as m: + m.put(server.jobs.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) + server.jobs.cancel(job) + + +def test_wait_for_job_finished(server: TSC.Server) -> None: + # Waiting for an already finished job, directly returns that job's info + response_xml = GET_BY_ID_XML.read_text() + job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.wait_for_job(job_id) + + assert job_id == job.id + assert job.notes == ["Job detail notes"] + + +def test_wait_for_job_completed(server: TSC.Server) -> None: + # Waiting for a bridge (cloud) job completion + response_xml = GET_BY_ID_COMPLETED_XML.read_text() + job_id = "2eef4225-aa0c-41c4-8662-a76d89ed7336" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.wait_for_job(job_id) + + assert job_id == job.id + assert job.notes == ["Job detail notes"] + + +def test_wait_for_job_failed(server: TSC.Server) -> None: + # Waiting for a failed job raises an exception + response_xml = GET_BY_ID_FAILED_XML.read_text() + job_id = "77d5e57a-2517-479f-9a3c-a32025f2b64d" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + with pytest.raises(JobFailedException): + server.jobs.wait_for_job(job_id) + + +def test_wait_for_job_timeout(server: TSC.Server) -> None: + # Waiting for a job which doesn't terminate will throw an exception + response_xml = GET_BY_ID_INPROGRESS_XML.read_text() + job_id = "77d5e57a-2517-479f-9a3c-a32025f2b64d" + with mocked_time(), requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + with pytest.raises(TimeoutError): + server.jobs.wait_for_job(job_id, timeout=30) + + +def test_get_job_datasource_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_FAILED_XML.read_text() + job_id = "777bf7c4-421d-4b2c-a518-11b90187c545" + with requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.get_by_id(job_id) + assert job.datasource_id == "03b9fbec-81f6-4160-ae49-5f9f6d412758" + + +def test_get_job_workbook_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_WORKBOOK.read_text() + job_id = "bb1aab79-db54-4e96-9dd3-461d8f081d08" + with requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.get_by_id(job_id) + assert job.workbook_id == "5998aaaf-1abe-4d38-b4d9-bc53e85bdd13" + + +def test_get_job_workbook_name(server: TSC.Server) -> None: + response_xml = GET_BY_ID_WORKBOOK.read_text() + job_id = "bb1aab79-db54-4e96-9dd3-461d8f081d08" + with requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.get_by_id(job_id) + assert job.workbook_name == "Superstore" + + +def test_get_job_datasource_name(server: TSC.Server) -> None: + response_xml = GET_BY_ID_FAILED_XML.read_text() + job_id = "777bf7c4-421d-4b2c-a518-11b90187c545" + with requests_mock.mock() as m: + m.get(f"{server.jobs.baseurl}/{job_id}", text=response_xml) + job = server.jobs.get_by_id(job_id) + assert job.datasource_name == "World Indicators" + + +def test_background_job_str() -> None: + job = TSC.BackgroundJobItem("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", datetime.now(), 1, "extractRefresh", "Failed") + assert not str(job).startswith("< None: - self.server = TSC.Server("http://test", False) - self.server.version = "3.15" - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.linked_tasks.baseurl - - def test_parse_linked_task_flow_run(self): - xml = fromstring(GET_LINKED_TASKS.read_bytes()) - task_runs = LinkedTaskFlowRunItem._parse_element(xml, self.server.namespace) - assert 1 == len(task_runs) - task = task_runs[0] - assert task.flow_run_id == "e3d1fc25-5644-4e32-af35-58dcbd1dbd73" - assert task.flow_run_priority == 1 - assert task.flow_run_consecutive_failed_count == 3 - assert task.flow_run_task_type == "runFlow" - assert task.flow_id == "ab1231eb-b8ca-461e-a131-83f3c2b6a673" - assert task.flow_name == "flow-name" - - def test_parse_linked_task_step(self): - xml = fromstring(GET_LINKED_TASKS.read_bytes()) - steps = LinkedTaskStepItem.from_task_xml(xml, self.server.namespace) - assert 1 == len(steps) - step = steps[0] - assert step.id == "f554a4df-bb6f-4294-94ee-9a709ef9bda0" - assert step.stop_downstream_on_failure - assert step.step_number == 1 - assert 1 == len(step.task_details) - task = step.task_details[0] - assert task.flow_run_id == "e3d1fc25-5644-4e32-af35-58dcbd1dbd73" - assert task.flow_run_priority == 1 - assert task.flow_run_consecutive_failed_count == 3 - assert task.flow_run_task_type == "runFlow" - assert task.flow_id == "ab1231eb-b8ca-461e-a131-83f3c2b6a673" - assert task.flow_name == "flow-name" - - def test_parse_linked_task(self): - tasks = LinkedTaskItem.from_response(GET_LINKED_TASKS.read_bytes(), self.server.namespace) - assert 1 == len(tasks) - task = tasks[0] - assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" - assert task.num_steps == 1 - assert task.schedule is not None - assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" - - def test_get_linked_tasks(self): - with requests_mock.mock() as m: - m.get(self.baseurl, text=GET_LINKED_TASKS.read_text()) - tasks, pagination_item = self.server.linked_tasks.get() - - assert 1 == len(tasks) - task = tasks[0] - assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" - assert task.num_steps == 1 - assert task.schedule is not None - assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" - - def test_get_by_id_str_linked_task(self): - id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" - - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{id_}", text=GET_LINKED_TASKS.read_text()) - task = self.server.linked_tasks.get_by_id(id_) - - assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" - assert task.num_steps == 1 - assert task.schedule is not None - assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" - - def test_get_by_id_obj_linked_task(self): - id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" - in_task = LinkedTaskItem() - in_task.id = id_ - - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{id_}", text=GET_LINKED_TASKS.read_text()) - task = self.server.linked_tasks.get_by_id(in_task) - - assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" - assert task.num_steps == 1 - assert task.schedule is not None - assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" - - def test_run_now_str_linked_task(self): - id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" - - with requests_mock.mock() as m: - m.post(f"{self.baseurl}/{id_}/runNow", text=RUN_LINKED_TASK_NOW.read_text()) - job = self.server.linked_tasks.run_now(id_) - - assert job.id == "269a1e5a-1220-4a13-ac01-704982693dd8" - assert job.status == "InProgress" - assert job.created_at == parse_datetime("2022-02-15T00:22:22Z") - assert job.linked_task_id == id_ - - def test_run_now_obj_linked_task(self): - id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" - in_task = LinkedTaskItem() - in_task.id = id_ - - with requests_mock.mock() as m: - m.post(f"{self.baseurl}/{id_}/runNow", text=RUN_LINKED_TASK_NOW.read_text()) - job = self.server.linked_tasks.run_now(in_task) - - assert job.id == "269a1e5a-1220-4a13-ac01-704982693dd8" - assert job.status == "InProgress" - assert job.created_at == parse_datetime("2022-02-15T00:22:22Z") - assert job.linked_task_id == id_ +@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" + server.version = "3.15" + + return server + + +def test_parse_linked_task_flow_run(server: TSC.Server) -> None: + xml = fromstring(GET_LINKED_TASKS.read_bytes()) + task_runs = LinkedTaskFlowRunItem._parse_element(xml, server.namespace) + assert 1 == len(task_runs) + task = task_runs[0] + assert task.flow_run_id == "e3d1fc25-5644-4e32-af35-58dcbd1dbd73" + assert task.flow_run_priority == 1 + assert task.flow_run_consecutive_failed_count == 3 + assert task.flow_run_task_type == "runFlow" + assert task.flow_id == "ab1231eb-b8ca-461e-a131-83f3c2b6a673" + assert task.flow_name == "flow-name" + + +def test_parse_linked_task_step(server: TSC.Server) -> None: + xml = fromstring(GET_LINKED_TASKS.read_bytes()) + steps = LinkedTaskStepItem.from_task_xml(xml, server.namespace) + assert 1 == len(steps) + step = steps[0] + assert step.id == "f554a4df-bb6f-4294-94ee-9a709ef9bda0" + assert step.stop_downstream_on_failure + assert step.step_number == 1 + assert 1 == len(step.task_details) + task = step.task_details[0] + assert task.flow_run_id == "e3d1fc25-5644-4e32-af35-58dcbd1dbd73" + assert task.flow_run_priority == 1 + assert task.flow_run_consecutive_failed_count == 3 + assert task.flow_run_task_type == "runFlow" + assert task.flow_id == "ab1231eb-b8ca-461e-a131-83f3c2b6a673" + assert task.flow_name == "flow-name" + + +def test_parse_linked_task(server: TSC.Server) -> None: + tasks = LinkedTaskItem.from_response(GET_LINKED_TASKS.read_bytes(), server.namespace) + assert 1 == len(tasks) + task = tasks[0] + assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" + assert task.num_steps == 1 + assert task.schedule is not None + assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" + + +def test_get_linked_tasks(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.linked_tasks.baseurl, text=GET_LINKED_TASKS.read_text()) + tasks, pagination_item = server.linked_tasks.get() + + assert 1 == len(tasks) + task = tasks[0] + assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" + assert task.num_steps == 1 + assert task.schedule is not None + assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" + + +def test_get_by_id_str_linked_task(server: TSC.Server) -> None: + id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" + + with requests_mock.mock() as m: + m.get(f"{server.linked_tasks.baseurl}/{id_}", text=GET_LINKED_TASKS.read_text()) + task = server.linked_tasks.get_by_id(id_) + + assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" + assert task.num_steps == 1 + assert task.schedule is not None + assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" + + +def test_get_by_id_obj_linked_task(server: TSC.Server) -> None: + id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" + in_task = LinkedTaskItem() + in_task.id = id_ + + with requests_mock.mock() as m: + m.get(f"{server.linked_tasks.baseurl}/{id_}", text=GET_LINKED_TASKS.read_text()) + task = server.linked_tasks.get_by_id(in_task) + + assert task.id == "1b8211dc-51a8-45ce-a831-b5921708e03e" + assert task.num_steps == 1 + assert task.schedule is not None + assert task.schedule.id == "be077332-d01d-481b-b2f3-917e463d4dca" + + +def test_run_now_str_linked_task(server: TSC.Server) -> None: + id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" + + with requests_mock.mock() as m: + m.post(f"{server.linked_tasks.baseurl}/{id_}/runNow", text=RUN_LINKED_TASK_NOW.read_text()) + job = server.linked_tasks.run_now(id_) + + assert job.id == "269a1e5a-1220-4a13-ac01-704982693dd8" + assert job.status == "InProgress" + assert job.created_at == parse_datetime("2022-02-15T00:22:22Z") + assert job.linked_task_id == id_ + + +def test_run_now_obj_linked_task(server: TSC.Server) -> None: + id_ = "1b8211dc-51a8-45ce-a831-b5921708e03e" + in_task = LinkedTaskItem() + in_task.id = id_ + + with requests_mock.mock() as m: + m.post(f"{server.linked_tasks.baseurl}/{id_}/runNow", text=RUN_LINKED_TASK_NOW.read_text()) + job = server.linked_tasks.run_now(in_task) + + assert job.id == "269a1e5a-1220-4a13-ac01-704982693dd8" + assert job.status == "InProgress" + assert job.created_at == parse_datetime("2022-02-15T00:22:22Z") + assert job.linked_task_id == id_ diff --git a/test/test_metadata.py b/test/test_metadata.py index 1dc9cf1c6..8b8b25151 100644 --- a/test/test_metadata.py +++ b/test/test_metadata.py @@ -1,21 +1,21 @@ import json -import os.path -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.server.endpoint.exceptions import GraphQLError -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") +TEST_ASSET_DIR = Path(__file__).parent / "assets" -METADATA_QUERY_SUCCESS = os.path.join(TEST_ASSET_DIR, "metadata_query_success.json") -METADATA_QUERY_ERROR = os.path.join(TEST_ASSET_DIR, "metadata_query_error.json") -EXPECTED_PAGED_DICT = os.path.join(TEST_ASSET_DIR, "metadata_query_expected_dict.dict") +METADATA_QUERY_SUCCESS = TEST_ASSET_DIR / "metadata_query_success.json" +METADATA_QUERY_ERROR = TEST_ASSET_DIR / "metadata_query_error.json" +EXPECTED_PAGED_DICT = TEST_ASSET_DIR / "metadata_query_expected_dict.dict" -METADATA_PAGE_1 = os.path.join(TEST_ASSET_DIR, "metadata_paged_1.json") -METADATA_PAGE_2 = os.path.join(TEST_ASSET_DIR, "metadata_paged_2.json") -METADATA_PAGE_3 = os.path.join(TEST_ASSET_DIR, "metadata_paged_3.json") +METADATA_PAGE_1 = TEST_ASSET_DIR / "metadata_paged_1.json" +METADATA_PAGE_2 = TEST_ASSET_DIR / "metadata_paged_2.json" +METADATA_PAGE_3 = TEST_ASSET_DIR / "metadata_paged_3.json" EXPECTED_DICT = { "publishedDatasources": [ @@ -29,74 +29,81 @@ EXPECTED_DICT_ERROR = [{"message": "Reached time limit of PT5S for query execution.", "path": None, "extensions": None}] -class MetadataTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.baseurl = self.server.metadata.baseurl - self.server.version = "3.5" - - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - def test_metadata_query(self): - with open(METADATA_QUERY_SUCCESS, "rb") as f: - response_json = json.loads(f.read().decode()) - with requests_mock.mock() as m: - m.post(self.baseurl, json=response_json) - actual = self.server.metadata.query("fake query") - - datasources = actual["data"] - - self.assertDictEqual(EXPECTED_DICT, datasources) - - def test_paged_metadata_query(self): - with open(EXPECTED_PAGED_DICT, "rb") as f: - expected = eval(f.read()) - - # prepare the 3 pages of results - with open(METADATA_PAGE_1, "rb") as f: - result_1 = f.read().decode() - with open(METADATA_PAGE_2, "rb") as f: - result_2 = f.read().decode() - with open(METADATA_PAGE_3, "rb") as f: - result_3 = f.read().decode() - - with requests_mock.mock() as m: - m.post( - self.baseurl, - [ - {"text": result_1, "status_code": 200}, - {"text": result_2, "status_code": 200}, - {"text": result_3, "status_code": 200}, - ], - ) - - # validation checks for endCursor and hasNextPage, - # but the query text doesn't matter for the test - actual = self.server.metadata.paginated_query( - "fake query endCursor hasNextPage", variables={"first": 1, "afterToken": None} - ) - - self.assertDictEqual(expected, actual) - - def test_metadata_query_ignore_error(self): - with open(METADATA_QUERY_ERROR, "rb") as f: - response_json = json.loads(f.read().decode()) - with requests_mock.mock() as m: - m.post(self.baseurl, json=response_json) - actual = self.server.metadata.query("fake query") - datasources = actual["data"] - - self.assertNotEqual(actual.get("errors", None), None) - self.assertListEqual(EXPECTED_DICT_ERROR, actual["errors"]) - self.assertDictEqual(EXPECTED_DICT, datasources) - - def test_metadata_query_abort_on_error(self): - with open(METADATA_QUERY_ERROR, "rb") as f: - response_json = json.loads(f.read().decode()) - with requests_mock.mock() as m: - m.post(self.baseurl, json=response_json) - - with self.assertRaises(GraphQLError) as e: - self.server.metadata.query("fake query", abort_on_error=True) - self.assertListEqual(e.error, EXPECTED_DICT_ERROR) +@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" + server.version = "3.5" + + return server + + +def test_metadata_query(server: TSC.Server) -> None: + with open(METADATA_QUERY_SUCCESS, "rb") as f: + response_json = json.loads(f.read().decode()) + with requests_mock.mock() as m: + m.post(server.metadata.baseurl, json=response_json) + actual = server.metadata.query("fake query") + + datasources = actual["data"] + + assert EXPECTED_DICT == datasources + + +def test_paged_metadata_query(server: TSC.Server) -> None: + with open(EXPECTED_PAGED_DICT, "rb") as f: + expected = eval(f.read()) + + # prepare the 3 pages of results + with open(METADATA_PAGE_1, "rb") as f: + result_1 = f.read().decode() + with open(METADATA_PAGE_2, "rb") as f: + result_2 = f.read().decode() + with open(METADATA_PAGE_3, "rb") as f: + result_3 = f.read().decode() + + with requests_mock.mock() as m: + m.post( + server.metadata.baseurl, + [ + {"text": result_1, "status_code": 200}, + {"text": result_2, "status_code": 200}, + {"text": result_3, "status_code": 200}, + ], + ) + + # validation checks for endCursor and hasNextPage, + # but the query text doesn't matter for the test + actual = server.metadata.paginated_query( + "fake query endCursor hasNextPage", variables={"first": 1, "afterToken": None} + ) + + assert expected == actual + + +def test_metadata_query_ignore_error(server: TSC.Server) -> None: + with open(METADATA_QUERY_ERROR, "rb") as f: + response_json = json.loads(f.read().decode()) + with requests_mock.mock() as m: + m.post(server.metadata.baseurl, json=response_json) + actual = server.metadata.query("fake query") + datasources = actual["data"] + + assert actual.get("errors", None) is not None + assert EXPECTED_DICT_ERROR == actual["errors"] + assert EXPECTED_DICT == datasources + + +def test_metadata_query_abort_on_error(server: TSC.Server) -> None: + with open(METADATA_QUERY_ERROR, "rb") as f: + response_json = json.loads(f.read().decode()) + with requests_mock.mock() as m: + m.post(server.metadata.baseurl, json=response_json) + + with pytest.raises(GraphQLError) as e: + server.metadata.query("fake query", abort_on_error=True) + assert e.error == EXPECTED_DICT_ERROR # type: ignore[attr-defined] diff --git a/test/test_metrics.py b/test/test_metrics.py index 7628abb1a..fdb21f8f0 100644 --- a/test/test_metrics.py +++ b/test/test_metrics.py @@ -1,7 +1,8 @@ -import unittest import requests_mock from pathlib import Path +import pytest + import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime @@ -11,95 +12,100 @@ METRICS_UPDATE = assets / "metrics_update.xml" -class TestMetrics(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.9" - - self.baseurl = self.server.metrics.baseurl - - def test_metrics_get(self) -> None: - with requests_mock.mock() as m: - m.get(self.baseurl, text=METRICS_GET.read_text()) - all_metrics, pagination_item = self.server.metrics.get() - - self.assertEqual(len(all_metrics), 2) - self.assertEqual(pagination_item.total_available, 27) - self.assertEqual(all_metrics[0].id, "6561daa3-20e8-407f-ba09-709b178c0b4a") - self.assertEqual(all_metrics[0].name, "Example metric") - self.assertEqual(all_metrics[0].description, "Description of my metric.") - self.assertEqual(all_metrics[0].webpage_url, "https://test/#/site/site-name/metrics/3") - self.assertEqual(format_datetime(all_metrics[0].created_at), "2020-01-02T01:02:03Z") - self.assertEqual(format_datetime(all_metrics[0].updated_at), "2020-01-02T01:02:03Z") - self.assertEqual(all_metrics[0].suspended, True) - self.assertEqual(all_metrics[0].project_id, "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33") - self.assertEqual(all_metrics[0].project_name, "Default") - self.assertEqual(all_metrics[0].owner_id, "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33") - self.assertEqual(all_metrics[0].view_id, "29dae0cd-1862-4a20-a638-e2c2dfa682d4") - self.assertEqual(len(all_metrics[0].tags), 0) - - self.assertEqual(all_metrics[1].id, "721760d9-0aa4-4029-87ae-371c956cea07") - self.assertEqual(all_metrics[1].name, "Another Example metric") - self.assertEqual(all_metrics[1].description, "Description of another metric.") - self.assertEqual(all_metrics[1].webpage_url, "https://test/#/site/site-name/metrics/4") - self.assertEqual(format_datetime(all_metrics[1].created_at), "2020-01-03T01:02:03Z") - self.assertEqual(format_datetime(all_metrics[1].updated_at), "2020-01-04T01:02:03Z") - self.assertEqual(all_metrics[1].suspended, False) - self.assertEqual(all_metrics[1].project_id, "486e0de0-2258-45bd-99cf-b62013e19f4e") - self.assertEqual(all_metrics[1].project_name, "Assets") - self.assertEqual(all_metrics[1].owner_id, "1bbbc2b9-847d-443c-9a1f-dbcf112b8814") - self.assertEqual(all_metrics[1].view_id, "7dbfdb63-a6ca-4723-93ee-4fefc71992d3") - self.assertEqual(len(all_metrics[1].tags), 2) - self.assertIn("Test", all_metrics[1].tags) - self.assertIn("Asset", all_metrics[1].tags) - - def test_metrics_get_by_id(self) -> None: - luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{luid}", text=METRICS_GET_BY_ID.read_text()) - metric = self.server.metrics.get_by_id(luid) - - self.assertEqual(metric.id, "6561daa3-20e8-407f-ba09-709b178c0b4a") - self.assertEqual(metric.name, "Example metric") - self.assertEqual(metric.description, "Description of my metric.") - self.assertEqual(metric.webpage_url, "https://test/#/site/site-name/metrics/3") - self.assertEqual(format_datetime(metric.created_at), "2020-01-02T01:02:03Z") - self.assertEqual(format_datetime(metric.updated_at), "2020-01-02T01:02:03Z") - self.assertEqual(metric.suspended, True) - self.assertEqual(metric.project_id, "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33") - self.assertEqual(metric.project_name, "Default") - self.assertEqual(metric.owner_id, "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33") - self.assertEqual(metric.view_id, "29dae0cd-1862-4a20-a638-e2c2dfa682d4") - self.assertEqual(len(metric.tags), 0) - - def test_metrics_delete(self) -> None: - luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{luid}") - self.server.metrics.delete(luid) - - def test_metrics_update(self) -> None: - luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" - metric = TSC.MetricItem() - metric._id = luid - - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{luid}", text=METRICS_UPDATE.read_text()) - metric = self.server.metrics.update(metric) - - self.assertEqual(metric.id, "6561daa3-20e8-407f-ba09-709b178c0b4a") - self.assertEqual(metric.name, "Example metric") - self.assertEqual(metric.description, "Description of my metric.") - self.assertEqual(metric.webpage_url, "https://test/#/site/site-name/metrics/3") - self.assertEqual(format_datetime(metric.created_at), "2020-01-02T01:02:03Z") - self.assertEqual(format_datetime(metric.updated_at), "2020-01-02T01:02:03Z") - self.assertEqual(metric.suspended, True) - self.assertEqual(metric.project_id, "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33") - self.assertEqual(metric.project_name, "Default") - self.assertEqual(metric.owner_id, "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33") - self.assertEqual(metric.view_id, "29dae0cd-1862-4a20-a638-e2c2dfa682d4") - self.assertEqual(len(metric.tags), 0) +@pytest.fixture(scope="function") +def server() -> TSC.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" + server.version = "3.9" + + return server + + +def test_metrics_get(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.metrics.baseurl, text=METRICS_GET.read_text()) + all_metrics, pagination_item = server.metrics.get() + + assert len(all_metrics) == 2 + assert pagination_item.total_available == 27 + assert all_metrics[0].id == "6561daa3-20e8-407f-ba09-709b178c0b4a" + assert all_metrics[0].name == "Example metric" + assert all_metrics[0].description == "Description of my metric." + assert all_metrics[0].webpage_url == "https://test/#/site/site-name/metrics/3" + assert format_datetime(all_metrics[0].created_at) == "2020-01-02T01:02:03Z" + assert format_datetime(all_metrics[0].updated_at) == "2020-01-02T01:02:03Z" + assert all_metrics[0].suspended + assert all_metrics[0].project_id == "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33" + assert all_metrics[0].project_name == "Default" + assert all_metrics[0].owner_id == "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33" + assert all_metrics[0].view_id == "29dae0cd-1862-4a20-a638-e2c2dfa682d4" + assert len(all_metrics[0].tags) == 0 + + assert all_metrics[1].id == "721760d9-0aa4-4029-87ae-371c956cea07" + assert all_metrics[1].name == "Another Example metric" + assert all_metrics[1].description == "Description of another metric." + assert all_metrics[1].webpage_url == "https://test/#/site/site-name/metrics/4" + assert format_datetime(all_metrics[1].created_at) == "2020-01-03T01:02:03Z" + assert format_datetime(all_metrics[1].updated_at) == "2020-01-04T01:02:03Z" + assert all_metrics[1].suspended is False + assert all_metrics[1].project_id == "486e0de0-2258-45bd-99cf-b62013e19f4e" + assert all_metrics[1].project_name == "Assets" + assert all_metrics[1].owner_id == "1bbbc2b9-847d-443c-9a1f-dbcf112b8814" + assert all_metrics[1].view_id == "7dbfdb63-a6ca-4723-93ee-4fefc71992d3" + assert len(all_metrics[1].tags) == 2 + assert "Test" in all_metrics[1].tags + assert "Asset" in all_metrics[1].tags + + +def test_metrics_get_by_id(server: TSC.Server) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + with requests_mock.mock() as m: + m.get(f"{server.metrics.baseurl}/{luid}", text=METRICS_GET_BY_ID.read_text()) + metric = server.metrics.get_by_id(luid) + + assert metric.id == "6561daa3-20e8-407f-ba09-709b178c0b4a" + assert metric.name == "Example metric" + assert metric.description == "Description of my metric." + assert metric.webpage_url == "https://test/#/site/site-name/metrics/3" + assert format_datetime(metric.created_at) == "2020-01-02T01:02:03Z" + assert format_datetime(metric.updated_at) == "2020-01-02T01:02:03Z" + assert metric.suspended + assert metric.project_id == "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33" + assert metric.project_name == "Default" + assert metric.owner_id == "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33" + assert metric.view_id == "29dae0cd-1862-4a20-a638-e2c2dfa682d4" + assert len(metric.tags) == 0 + + +def test_metrics_delete(server: TSC.Server) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + with requests_mock.mock() as m: + m.delete(f"{server.metrics.baseurl}/{luid}") + server.metrics.delete(luid) + + +def test_metrics_update(server: TSC.Server) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + metric = TSC.MetricItem() + metric._id = luid + + with requests_mock.mock() as m: + m.put(f"{server.metrics.baseurl}/{luid}", text=METRICS_UPDATE.read_text()) + metric = server.metrics.update(metric) + + assert metric.id == "6561daa3-20e8-407f-ba09-709b178c0b4a" + assert metric.name == "Example metric" + assert metric.description == "Description of my metric." + assert metric.webpage_url == "https://test/#/site/site-name/metrics/3" + assert format_datetime(metric.created_at) == "2020-01-02T01:02:03Z" + assert format_datetime(metric.updated_at) == "2020-01-02T01:02:03Z" + assert metric.suspended + assert metric.project_id == "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33" + assert metric.project_name == "Default" + assert metric.owner_id == "32e79edb-6cfd-47dc-ad79-e8ec2fbb1d33" + assert metric.view_id == "29dae0cd-1862-4a20-a638-e2c2dfa682d4" + assert len(metric.tags) == 0 diff --git a/test/test_oidc.py b/test/test_oidc.py new file mode 100644 index 000000000..476d902a1 --- /dev/null +++ b/test/test_oidc.py @@ -0,0 +1,159 @@ +import requests_mock +from pathlib import Path + +import pytest + +import tableauserverclient as TSC + +assets = Path(__file__).parent / "assets" +OIDC_GET = assets / "oidc_get.xml" +OIDC_GET_BY_ID = assets / "oidc_get_by_id.xml" +OIDC_UPDATE = assets / "oidc_update.xml" +OIDC_CREATE = assets / "oidc_create.xml" + + +@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" + server.version = "3.24" + + return server + + +def test_oidc_get_by_id(server: TSC.Server) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + with requests_mock.mock() as m: + m.get(f"{server.oidc.baseurl}/{luid}", text=OIDC_GET.read_text()) + oidc = server.oidc.get_by_id(luid) + + assert oidc.enabled is True + assert ( + oidc.test_login_url + == "https://sso.online.tableau.com/public/testLogin?alias=8a04d825-e5d4-408f-bbc2-1042b8bb4818&authSetting=OIDC&idpConfigurationId=78c985b4-5494-4436-bcee-f595e287ba4a" + ) + assert oidc.known_provider_alias == "Google" + assert oidc.allow_embedded_authentication is False + assert oidc.use_full_name is False + assert oidc.idp_configuration_name == "GoogleOIDC" + assert oidc.idp_configuration_id == "78c985b4-5494-4436-bcee-f595e287ba4a" + assert oidc.client_id == "ICcGeDt3XHwzZ1D0nCZt" + assert oidc.client_secret == "omit" + assert oidc.authorization_endpoint == "https://myidp.com/oauth2/v1/authorize" + assert oidc.token_endpoint == "https://myidp.com/oauth2/v1/token" + assert oidc.userinfo_endpoint == "https://myidp.com/oauth2/v1/userinfo" + assert oidc.jwks_uri == "https://myidp.com/oauth2/v1/keys" + assert oidc.end_session_endpoint == "https://myidp.com/oauth2/v1/logout" + assert oidc.custom_scope == "openid, email, profile" + assert oidc.prompt == "login,consent" + assert oidc.client_authentication == "client_secret_basic" + assert oidc.essential_acr_values == "phr" + assert oidc.email_mapping == "email" + assert oidc.first_name_mapping == "given_name" + assert oidc.last_name_mapping == "family_name" + assert oidc.full_name_mapping == "name" + + +def test_oidc_delete(server: TSC.Server) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + with requests_mock.mock() as m: + m.put(f"{server.baseurl}/sites/{server.site_id}/disable-site-oidc-configuration") + server.oidc.delete_configuration(luid) + history = m.request_history[0] + + assert "idpconfigurationid" in history.qs + assert history.qs["idpconfigurationid"][0] == luid + + +def test_oidc_update(server: TSC.Server) -> None: + luid = "6561daa3-20e8-407f-ba09-709b178c0b4a" + oidc = TSC.SiteOIDCConfiguration() + oidc.idp_configuration_id = luid + + # Only include the required fields for updates + oidc.enabled = True + oidc.idp_configuration_name = "GoogleOIDC" + oidc.client_id = "ICcGeDt3XHwzZ1D0nCZt" + oidc.client_secret = "omit" + oidc.authorization_endpoint = "https://myidp.com/oauth2/v1/authorize" + oidc.token_endpoint = "https://myidp.com/oauth2/v1/token" + oidc.userinfo_endpoint = "https://myidp.com/oauth2/v1/userinfo" + oidc.jwks_uri = "https://myidp.com/oauth2/v1/keys" + + with requests_mock.mock() as m: + m.put(f"{server.oidc.baseurl}/{luid}", text=OIDC_UPDATE.read_text()) + oidc = server.oidc.update(oidc) + + assert oidc.enabled is True + assert ( + oidc.test_login_url + == "https://sso.online.tableau.com/public/testLogin?alias=8a04d825-e5d4-408f-bbc2-1042b8bb4818&authSetting=OIDC&idpConfigurationId=78c985b4-5494-4436-bcee-f595e287ba4a" + ) + assert oidc.known_provider_alias == "Google" + assert oidc.allow_embedded_authentication is False + assert oidc.use_full_name is False + assert oidc.idp_configuration_name == "GoogleOIDC" + assert oidc.idp_configuration_id == "78c985b4-5494-4436-bcee-f595e287ba4a" + assert oidc.client_id == "ICcGeDt3XHwzZ1D0nCZt" + assert oidc.client_secret == "omit" + assert oidc.authorization_endpoint == "https://myidp.com/oauth2/v1/authorize" + assert oidc.token_endpoint == "https://myidp.com/oauth2/v1/token" + assert oidc.userinfo_endpoint == "https://myidp.com/oauth2/v1/userinfo" + assert oidc.jwks_uri == "https://myidp.com/oauth2/v1/keys" + assert oidc.end_session_endpoint == "https://myidp.com/oauth2/v1/logout" + assert oidc.custom_scope == "openid, email, profile" + assert oidc.prompt == "login,consent" + assert oidc.client_authentication == "client_secret_basic" + assert oidc.essential_acr_values == "phr" + assert oidc.email_mapping == "email" + assert oidc.first_name_mapping == "given_name" + assert oidc.last_name_mapping == "family_name" + assert oidc.full_name_mapping == "name" + + +def test_oidc_create(server: TSC.Server) -> None: + oidc = TSC.SiteOIDCConfiguration() + + # Only include the required fields for creation + oidc.enabled = True + oidc.idp_configuration_name = "GoogleOIDC" + oidc.client_id = "ICcGeDt3XHwzZ1D0nCZt" + oidc.client_secret = "omit" + oidc.authorization_endpoint = "https://myidp.com/oauth2/v1/authorize" + oidc.token_endpoint = "https://myidp.com/oauth2/v1/token" + oidc.userinfo_endpoint = "https://myidp.com/oauth2/v1/userinfo" + oidc.jwks_uri = "https://myidp.com/oauth2/v1/keys" + + with requests_mock.mock() as m: + m.put(server.oidc.baseurl, text=OIDC_CREATE.read_text()) + oidc = server.oidc.create(oidc) + + assert oidc.enabled is True + assert ( + oidc.test_login_url + == "https://sso.online.tableau.com/public/testLogin?alias=8a04d825-e5d4-408f-bbc2-1042b8bb4818&authSetting=OIDC&idpConfigurationId=78c985b4-5494-4436-bcee-f595e287ba4a" + ) + assert oidc.known_provider_alias == "Google" + assert oidc.allow_embedded_authentication is False + assert oidc.use_full_name is False + assert oidc.idp_configuration_name == "GoogleOIDC" + assert oidc.idp_configuration_id == "78c985b4-5494-4436-bcee-f595e287ba4a" + assert oidc.client_id == "ICcGeDt3XHwzZ1D0nCZt" + assert oidc.client_secret == "omit" + assert oidc.authorization_endpoint == "https://myidp.com/oauth2/v1/authorize" + assert oidc.token_endpoint == "https://myidp.com/oauth2/v1/token" + assert oidc.userinfo_endpoint == "https://myidp.com/oauth2/v1/userinfo" + assert oidc.jwks_uri == "https://myidp.com/oauth2/v1/keys" + assert oidc.end_session_endpoint == "https://myidp.com/oauth2/v1/logout" + assert oidc.custom_scope == "openid, email, profile" + assert oidc.prompt == "login,consent" + assert oidc.client_authentication == "client_secret_basic" + assert oidc.essential_acr_values == "phr" + assert oidc.email_mapping == "email" + assert oidc.first_name_mapping == "given_name" + assert oidc.last_name_mapping == "family_name" + assert oidc.full_name_mapping == "name" diff --git a/test/test_pager.py b/test/test_pager.py index 1836095bb..0a7ccf00a 100644 --- a/test/test_pager.py +++ b/test/test_pager.py @@ -1,19 +1,32 @@ import contextlib import os -import unittest +from pathlib import Path import xml.etree.ElementTree as ET +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.config import config -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") +TEST_ASSET_DIR = Path(__file__).parent / "assets" -GET_VIEW_XML = os.path.join(TEST_ASSET_DIR, "view_get.xml") -GET_XML_PAGE1 = os.path.join(TEST_ASSET_DIR, "workbook_get_page_1.xml") -GET_XML_PAGE2 = os.path.join(TEST_ASSET_DIR, "workbook_get_page_2.xml") -GET_XML_PAGE3 = os.path.join(TEST_ASSET_DIR, "workbook_get_page_3.xml") +GET_VIEW_XML = TEST_ASSET_DIR / "view_get.xml" +GET_XML_PAGE1 = TEST_ASSET_DIR / "workbook_get_page_1.xml" +GET_XML_PAGE2 = TEST_ASSET_DIR / "workbook_get_page_2.xml" +GET_XML_PAGE3 = TEST_ASSET_DIR / "workbook_get_page_3.xml" + + +@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 @contextlib.contextmanager @@ -27,110 +40,99 @@ def set_env(**environ): os.environ.update(old_environ) -class PagerTests(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_pager_with_no_options(self) -> None: - with open(GET_XML_PAGE1, "rb") as f: - page_1 = f.read().decode("utf-8") - with open(GET_XML_PAGE2, "rb") as f: - page_2 = f.read().decode("utf-8") - with open(GET_XML_PAGE3, "rb") as f: - page_3 = f.read().decode("utf-8") - with requests_mock.mock() as m: - # Register Pager with default request options - m.get(self.baseurl, text=page_1) - - # Register Pager with some pages - m.get(self.baseurl + "?pageNumber=1&pageSize=1", text=page_1) - m.get(self.baseurl + "?pageNumber=2&pageSize=1", text=page_2) - m.get(self.baseurl + "?pageNumber=3&pageSize=1", text=page_3) - - # No options should get all 3 - workbooks = list(TSC.Pager(self.server.workbooks)) - self.assertTrue(len(workbooks) == 3) - - # Let's check that workbook items aren't duplicates - wb1, wb2, wb3 = workbooks - self.assertEqual(wb1.name, "Page1Workbook") - self.assertEqual(wb2.name, "Page2Workbook") - self.assertEqual(wb3.name, "Page3Workbook") - - def test_pager_with_options(self) -> None: - with open(GET_XML_PAGE1, "rb") as f: - page_1 = f.read().decode("utf-8") - with open(GET_XML_PAGE2, "rb") as f: - page_2 = f.read().decode("utf-8") - with open(GET_XML_PAGE3, "rb") as f: - page_3 = f.read().decode("utf-8") - with requests_mock.mock() as m: - # Register Pager with some pages - m.get(self.baseurl + "?pageNumber=1&pageSize=1", complete_qs=True, text=page_1) - m.get(self.baseurl + "?pageNumber=2&pageSize=1", complete_qs=True, text=page_2) - m.get(self.baseurl + "?pageNumber=3&pageSize=1", complete_qs=True, text=page_3) - m.get(self.baseurl + "?pageNumber=1&pageSize=3", complete_qs=True, text=page_1) - - # Starting on page 2 should get 2 out of 3 - opts = TSC.RequestOptions(2, 1) - workbooks = list(TSC.Pager(self.server.workbooks, opts)) - self.assertTrue(len(workbooks) == 2) - - # Check that the workbooks are the 2 we think they should be - wb2, wb3 = workbooks - self.assertEqual(wb2.name, "Page2Workbook") - self.assertEqual(wb3.name, "Page3Workbook") - - # Starting on 1 with pagesize of 3 should get all 3 - opts = TSC.RequestOptions(1, 3) - workbooks = list(TSC.Pager(self.server.workbooks, opts)) - self.assertTrue(len(workbooks) == 3) - wb1, wb2, wb3 = workbooks - self.assertEqual(wb1.name, "Page1Workbook") - self.assertEqual(wb2.name, "Page2Workbook") - self.assertEqual(wb3.name, "Page3Workbook") - - # Starting on 3 with pagesize of 1 should get the last item - opts = TSC.RequestOptions(3, 1) - workbooks = list(TSC.Pager(self.server.workbooks, opts)) - self.assertTrue(len(workbooks) == 1) - # Should have the last workbook - wb3 = workbooks.pop() - self.assertEqual(wb3.name, "Page3Workbook") - - def test_pager_with_env_var(self) -> None: - with set_env(TSC_PAGE_SIZE="1000"): - assert config.PAGE_SIZE == 1000 - loop = TSC.Pager(self.server.workbooks) - assert loop._options.pagesize == 1000 - - def test_queryset_with_env_var(self) -> None: - with set_env(TSC_PAGE_SIZE="1000"): - assert config.PAGE_SIZE == 1000 - loop = self.server.workbooks.all() - assert loop.request_options.pagesize == 1000 - - def test_pager_view(self) -> None: - with open(GET_VIEW_XML, "rb") as f: - view_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.views.baseurl, text=view_xml) - for view in TSC.Pager(self.server.views): - assert view.name is not None - - def test_queryset_no_matches(self) -> None: - elem = ET.Element("tsResponse", xmlns="http://tableau.com/api") - ET.SubElement(elem, "pagination", totalAvailable="0") - ET.SubElement(elem, "groups") - xml = ET.tostring(elem).decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.groups.baseurl, text=xml) - all_groups = self.server.groups.all() - groups = list(all_groups) - assert len(groups) == 0 +def test_pager_with_no_options(server: TSC.Server) -> None: + page_1 = GET_XML_PAGE1.read_text() + page_2 = GET_XML_PAGE2.read_text() + page_3 = GET_XML_PAGE3.read_text() + with requests_mock.mock() as m: + # Register Pager with default request options + m.get(server.workbooks.baseurl, text=page_1) + + # Register Pager with some pages + m.get(server.workbooks.baseurl + "?pageNumber=1&pageSize=1", text=page_1) + m.get(server.workbooks.baseurl + "?pageNumber=2&pageSize=1", text=page_2) + m.get(server.workbooks.baseurl + "?pageNumber=3&pageSize=1", text=page_3) + + # No options should get all 3 + workbooks = list(TSC.Pager(server.workbooks)) + assert len(workbooks) == 3 + + # Let's check that workbook items aren't duplicates + wb1, wb2, wb3 = workbooks + assert wb1.name == "Page1Workbook" + assert wb2.name == "Page2Workbook" + assert wb3.name == "Page3Workbook" + + +def test_pager_with_options(server: TSC.Server) -> None: + page_1 = GET_XML_PAGE1.read_text() + page_2 = GET_XML_PAGE2.read_text() + page_3 = GET_XML_PAGE3.read_text() + with requests_mock.mock() as m: + # Register Pager with some pages + m.get(server.workbooks.baseurl + "?pageNumber=1&pageSize=1", complete_qs=True, text=page_1) + m.get(server.workbooks.baseurl + "?pageNumber=2&pageSize=1", complete_qs=True, text=page_2) + m.get(server.workbooks.baseurl + "?pageNumber=3&pageSize=1", complete_qs=True, text=page_3) + m.get(server.workbooks.baseurl + "?pageNumber=1&pageSize=3", complete_qs=True, text=page_1) + + # Starting on page 2 should get 2 out of 3 + opts = TSC.RequestOptions(2, 1) + workbooks = list(TSC.Pager(server.workbooks, opts)) + assert len(workbooks) == 2 + + # Check that the workbooks are the 2 we think they should be + wb2, wb3 = workbooks + assert wb2.name == "Page2Workbook" + assert wb3.name == "Page3Workbook" + + # Starting on 1 with pagesize of 3 should get all 3 + opts = TSC.RequestOptions(1, 3) + workbooks = list(TSC.Pager(server.workbooks, opts)) + assert len(workbooks) == 3 + wb1, wb2, wb3 = workbooks + assert wb1.name == "Page1Workbook" + assert wb2.name == "Page2Workbook" + assert wb3.name == "Page3Workbook" + + # Starting on 3 with pagesize of 1 should get the last item + opts = TSC.RequestOptions(3, 1) + workbooks = list(TSC.Pager(server.workbooks, opts)) + assert len(workbooks) == 1 + # Should have the last workbook + wb3 = workbooks.pop() + assert wb3.name == "Page3Workbook" + + +def test_pager_with_env_var(server: TSC.Server) -> None: + with set_env(TSC_PAGE_SIZE="1000"): + assert config.PAGE_SIZE == 1000 + loop = TSC.Pager(server.workbooks) + assert loop._options.pagesize == 1000 + + +def test_queryset_with_env_var(server: TSC.Server) -> None: + with set_env(TSC_PAGE_SIZE="1000"): + assert config.PAGE_SIZE == 1000 + loop = server.workbooks.all() + assert loop.request_options.pagesize == 1000 + + +def test_pager_view(server: TSC.Server) -> None: + with open(GET_VIEW_XML, "rb") as f: + view_xml = f.read().decode("utf-8") + with requests_mock.mock() as m: + m.get(server.views.baseurl, text=view_xml) + for view in TSC.Pager(server.views): + assert view.name is not None + + +def test_queryset_no_matches(server: TSC.Server) -> None: + elem = ET.Element("tsResponse", xmlns="http://tableau.com/api") + ET.SubElement(elem, "pagination", totalAvailable="0") + ET.SubElement(elem, "groups") + xml = ET.tostring(elem).decode("utf-8") + with requests_mock.mock() as m: + m.get(server.groups.baseurl, text=xml) + all_groups = server.groups.all() + groups = list(all_groups) + assert len(groups) == 0 diff --git a/test/test_permissionsrule.py b/test/test_permissionsrule.py index d7bceb258..3d016057e 100644 --- a/test/test_permissionsrule.py +++ b/test/test_permissionsrule.py @@ -1,104 +1,104 @@ -import unittest - import tableauserverclient as TSC from tableauserverclient.models.reference_item import ResourceReference -class TestPermissionsRules(unittest.TestCase): - def test_and(self): - grantee = ResourceReference("a", "user") - rule1 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) - rule2 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) +def test_and() -> None: + grantee = ResourceReference("a", "user") + rule1 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) + rule2 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) + + composite = rule1 & rule2 + + assert composite.capabilities.get(TSC.Permission.Capability.ExportData) == TSC.Permission.Mode.Allow + assert composite.capabilities.get(TSC.Permission.Capability.Delete) == TSC.Permission.Mode.Deny + assert composite.capabilities.get(TSC.Permission.Capability.ViewComments) == None + assert composite.capabilities.get(TSC.Permission.Capability.ExportXml) == TSC.Permission.Mode.Deny + - composite = rule1 & rule2 +def test_or() -> None: + grantee = ResourceReference("a", "user") + rule1 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) + rule2 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.ExportData), TSC.Permission.Mode.Allow) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.Delete), TSC.Permission.Mode.Deny) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.ViewComments), None) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.ExportXml), TSC.Permission.Mode.Deny) + composite = rule1 | rule2 - def test_or(self): - grantee = ResourceReference("a", "user") - rule1 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) - rule2 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) + assert composite.capabilities.get(TSC.Permission.Capability.ExportData) == TSC.Permission.Mode.Allow + assert composite.capabilities.get(TSC.Permission.Capability.Delete) == TSC.Permission.Mode.Allow + assert composite.capabilities.get(TSC.Permission.Capability.ViewComments) == TSC.Permission.Mode.Allow + assert composite.capabilities.get(TSC.Permission.Capability.ExportXml) == TSC.Permission.Mode.Deny - composite = rule1 | rule2 - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.ExportData), TSC.Permission.Mode.Allow) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.Delete), TSC.Permission.Mode.Allow) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.ViewComments), TSC.Permission.Mode.Allow) - self.assertEqual(composite.capabilities.get(TSC.Permission.Capability.ExportXml), TSC.Permission.Mode.Deny) +def test_eq_false() -> None: + grantee = ResourceReference("a", "user") + rule1 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) + rule2 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) - def test_eq_false(self): - grantee = ResourceReference("a", "user") - rule1 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) - rule2 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) + assert rule1 != rule2 - self.assertNotEqual(rule1, rule2) - def test_eq_true(self): - grantee = ResourceReference("a", "user") - rule1 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) - rule2 = TSC.PermissionsRule( - grantee, - { - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, - }, - ) - self.assertEqual(rule1, rule2) +def test_eq_true() -> None: + grantee = ResourceReference("a", "user") + rule1 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) + rule2 = TSC.PermissionsRule( + grantee, + { + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny, + }, + ) + assert rule1 == rule2 diff --git a/test/test_project.py b/test/test_project.py index c51f2e1e6..eb33f6732 100644 --- a/test/test_project.py +++ b/test/test_project.py @@ -1,438 +1,466 @@ -import os -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient import GroupItem -from ._utils import read_xml_asset, asset - -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -GET_XML = asset("project_get.xml") -GET_XML_ALL_FIELDS = asset("project_get_all_fields.xml") -UPDATE_XML = asset("project_update.xml") -SET_CONTENT_PERMISSIONS_XML = asset("project_content_permission.xml") -CREATE_XML = asset("project_create.xml") -POPULATE_PERMISSIONS_XML = "project_populate_permissions.xml" -POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML = "project_populate_workbook_default_permissions.xml" -UPDATE_DATASOURCE_DEFAULT_PERMISSIONS_XML = "project_update_datasource_default_permissions.xml" -POPULATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML = "project_populate_virtualconnection_default_permissions.xml" -UPDATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML = "project_update_virtualconnection_default_permissions.xml" - - -class ProjectTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.projects.baseurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_projects, pagination_item = self.server.projects.get() - - self.assertEqual(3, pagination_item.total_available) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_projects[0].id) - self.assertEqual("default", all_projects[0].name) - self.assertEqual("The default project that was automatically created by Tableau.", all_projects[0].description) - self.assertEqual("ManagedByOwner", all_projects[0].content_permissions) - self.assertEqual(None, all_projects[0].parent_id) - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[0].owner_id) - - self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[1].id) - self.assertEqual("Tableau", all_projects[1].name) - self.assertEqual("ManagedByOwner", all_projects[1].content_permissions) - self.assertEqual(None, all_projects[1].parent_id) - self.assertEqual("2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3", all_projects[1].owner_id) - - self.assertEqual("4cc52973-5e3a-4d1f-a4fb-5b5f73796edf", all_projects[2].id) - self.assertEqual("Tableau > Child 1", all_projects[2].name) - self.assertEqual("ManagedByOwner", all_projects[2].content_permissions) - self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", all_projects[2].parent_id) - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", all_projects[2].owner_id) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.projects.get) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) - self.server.projects.delete("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - def test_delete_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.projects.delete, "") - - def test_update(self) -> None: - with open(UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1d0304cd-3796-429f-b815-7258370b9b74", text=response_xml) - single_project = TSC.ProjectItem( - name="Test Project", - content_permissions="LockedToProject", - description="Project created for testing", - parent_id="9a8f2265-70f3-4494-96c5-e5949d7a1120", - ) - single_project._id = "1d0304cd-3796-429f-b815-7258370b9b74" - single_project.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_project = self.server.projects.update(single_project) - - self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", single_project.id) - self.assertEqual("Test Project", single_project.name) - self.assertEqual("Project created for testing", single_project.description) - self.assertEqual("LockedToProject", single_project.content_permissions) - self.assertEqual("9a8f2265-70f3-4494-96c5-e5949d7a1120", single_project.parent_id) - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", single_project.owner_id) - - def test_content_permission_locked_to_project_without_nested(self) -> None: - with open(SET_CONTENT_PERMISSIONS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/cb3759e5-da4a-4ade-b916-7e2b4ea7ec86", text=response_xml) - project_item = TSC.ProjectItem( - name="Test Project Permissions", - content_permissions="LockedToProjectWithoutNested", - description="Project created for testing", - parent_id="7687bc43-a543-42f3-b86f-80caed03a813", - ) - project_item._id = "cb3759e5-da4a-4ade-b916-7e2b4ea7ec86" - project_item = self.server.projects.update(project_item) - self.assertEqual("cb3759e5-da4a-4ade-b916-7e2b4ea7ec86", project_item.id) - self.assertEqual("Test Project Permissions", project_item.name) - self.assertEqual("Project created for testing", project_item.description) - self.assertEqual("LockedToProjectWithoutNested", project_item.content_permissions) - self.assertEqual("7687bc43-a543-42f3-b86f-80caed03a813", project_item.parent_id) - - def test_update_datasource_default_permission(self) -> None: - response_xml = read_xml_asset(UPDATE_DATASOURCE_DEFAULT_PERMISSIONS_XML) - with requests_mock.mock() as m: - m.put( - self.baseurl + "/b4065286-80f0-11ea-af1b-cb7191f48e45/default-permissions/datasources", - text=response_xml, - ) - project = TSC.ProjectItem("test-project") - project._id = "b4065286-80f0-11ea-af1b-cb7191f48e45" - - group = TSC.GroupItem("test-group") - group._id = "b4488bce-80f0-11ea-af1c-976d0c1dab39" - - capabilities = {TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny} - - rules = [TSC.PermissionsRule(grantee=GroupItem.as_reference(group._id), capabilities=capabilities)] - - new_rules = self.server.projects.update_datasource_default_permissions(project, rules) - - self.assertEqual("b4488bce-80f0-11ea-af1c-976d0c1dab39", new_rules[0].grantee.id) - - updated_capabilities = new_rules[0].capabilities - self.assertEqual(4, len(updated_capabilities)) - self.assertEqual("Deny", updated_capabilities["ExportXml"]) - self.assertEqual("Allow", updated_capabilities["Read"]) - self.assertEqual("Allow", updated_capabilities["Write"]) - self.assertEqual("Allow", updated_capabilities["Connect"]) - - def test_update_missing_id(self) -> None: - single_project = TSC.ProjectItem("test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.projects.update, single_project) - - def test_create(self) -> None: - with open(CREATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - new_project = TSC.ProjectItem(name="Test Project", description="Project created for testing") - new_project.content_permissions = "ManagedByOwner" - new_project.parent_id = "9a8f2265-70f3-4494-96c5-e5949d7a1120" - new_project = self.server.projects.create(new_project) - - self.assertEqual("ccbea03f-77c4-4209-8774-f67bc59c3cef", new_project.id) - self.assertEqual("Test Project", new_project.name) - self.assertEqual("Project created for testing", new_project.description) - self.assertEqual("ManagedByOwner", new_project.content_permissions) - self.assertEqual("9a8f2265-70f3-4494-96c5-e5949d7a1120", new_project.parent_id) - - def test_create_missing_name(self) -> None: - TSC.ProjectItem() - - def test_populate_permissions(self) -> None: - with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) - single_project = TSC.ProjectItem("Project3") - single_project._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" - - self.server.projects.populate_permissions(single_project) - permissions = single_project.permissions - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "c8f2773a-c83a-11e8-8c8f-33e6d787b506") - self.assertDictEqual( - permissions[0].capabilities, - { - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - }, - ) - - def test_populate_workbooks(self) -> None: - response_xml = read_xml_asset(POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML) - with requests_mock.mock() as m: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks", text=response_xml - ) - single_project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") - single_project.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - - self.server.projects.populate_workbook_default_permissions(single_project) - permissions = single_project.default_workbook_permissions - - rule1 = permissions.pop() - - self.assertEqual("c8f2773a-c83a-11e8-8c8f-33e6d787b506", rule1.grantee.id) - self.assertEqual("group", rule1.grantee.tag_name) - self.assertDictEqual( - rule1.capabilities, - { - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ViewUnderlyingData: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, - }, + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "project_get.xml" +GET_XML_ALL_FIELDS = TEST_ASSET_DIR / "project_get_all_fields.xml" +UPDATE_XML = TEST_ASSET_DIR / "project_update.xml" +SET_CONTENT_PERMISSIONS_XML = TEST_ASSET_DIR / "project_content_permission.xml" +CREATE_XML = TEST_ASSET_DIR / "project_create.xml" +POPULATE_PERMISSIONS_XML = TEST_ASSET_DIR / "project_populate_permissions.xml" +POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML = TEST_ASSET_DIR / "project_populate_workbook_default_permissions.xml" +UPDATE_DATASOURCE_DEFAULT_PERMISSIONS_XML = TEST_ASSET_DIR / "project_update_datasource_default_permissions.xml" +POPULATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML = ( + TEST_ASSET_DIR / "project_populate_virtualconnection_default_permissions.xml" +) +UPDATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML = ( + TEST_ASSET_DIR / "project_update_virtualconnection_default_permissions.xml" +) + + +@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_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.projects.baseurl, text=response_xml) + all_projects, pagination_item = server.projects.get() + + assert 3 == pagination_item.total_available + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == all_projects[0].id + assert "default" == all_projects[0].name + assert "The default project that was automatically created by Tableau." == all_projects[0].description + assert "ManagedByOwner" == all_projects[0].content_permissions + assert None == all_projects[0].parent_id + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == all_projects[0].owner_id + + assert "1d0304cd-3796-429f-b815-7258370b9b74" == all_projects[1].id + assert "Tableau" == all_projects[1].name + assert "ManagedByOwner" == all_projects[1].content_permissions + assert None == all_projects[1].parent_id + assert "2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3" == all_projects[1].owner_id + + assert "4cc52973-5e3a-4d1f-a4fb-5b5f73796edf" == all_projects[2].id + assert "Tableau > Child 1" == all_projects[2].name + assert "ManagedByOwner" == all_projects[2].content_permissions + assert "1d0304cd-3796-429f-b815-7258370b9b74" == all_projects[2].parent_id + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == all_projects[2].owner_id + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.projects.get() + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.projects.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) + server.projects.delete("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + +def test_delete_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.projects.delete("") + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.projects.baseurl + "/1d0304cd-3796-429f-b815-7258370b9b74", text=response_xml) + project = server.projects.get_by_id("1d0304cd-3796-429f-b815-7258370b9b74") + assert "1d0304cd-3796-429f-b815-7258370b9b74" == project.id + assert "Test Project" == project.name + assert "Project created for testing" == project.description + assert "LockedToProject" == project.content_permissions + assert "9a8f2265-70f3-4494-96c5-e5949d7a1120" == project.parent_id + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == project.owner_id + assert "LockedToProject" == project.content_permissions + + +def test_get_by_id_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.projects.get_by_id("") + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.projects.baseurl + "/1d0304cd-3796-429f-b815-7258370b9b74", text=response_xml) + single_project = TSC.ProjectItem( + name="Test Project", + content_permissions="LockedToProject", + description="Project created for testing", + parent_id="9a8f2265-70f3-4494-96c5-e5949d7a1120", + ) + single_project._id = "1d0304cd-3796-429f-b815-7258370b9b74" + single_project.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_project = server.projects.update(single_project) + + assert "1d0304cd-3796-429f-b815-7258370b9b74" == single_project.id + assert "Test Project" == single_project.name + assert "Project created for testing" == single_project.description + assert "LockedToProject" == single_project.content_permissions + assert "9a8f2265-70f3-4494-96c5-e5949d7a1120" == single_project.parent_id + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == single_project.owner_id + + +def test_content_permission_locked_to_project_without_nested(server: TSC.Server) -> None: + response_xml = SET_CONTENT_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.put(server.projects.baseurl + "/cb3759e5-da4a-4ade-b916-7e2b4ea7ec86", text=response_xml) + project_item = TSC.ProjectItem( + name="Test Project Permissions", + content_permissions="LockedToProjectWithoutNested", + description="Project created for testing", + parent_id="7687bc43-a543-42f3-b86f-80caed03a813", + ) + project_item._id = "cb3759e5-da4a-4ade-b916-7e2b4ea7ec86" + project_item = server.projects.update(project_item) + assert "cb3759e5-da4a-4ade-b916-7e2b4ea7ec86" == project_item.id + assert "Test Project Permissions" == project_item.name + assert "Project created for testing" == project_item.description + assert "LockedToProjectWithoutNested" == project_item.content_permissions + assert "7687bc43-a543-42f3-b86f-80caed03a813" == project_item.parent_id + + +def test_update_datasource_default_permission(server: TSC.Server) -> None: + response_xml = UPDATE_DATASOURCE_DEFAULT_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.put( + server.projects.baseurl + "/b4065286-80f0-11ea-af1b-cb7191f48e45/default-permissions/datasources", + text=response_xml, + ) + project = TSC.ProjectItem("test-project") + project._id = "b4065286-80f0-11ea-af1b-cb7191f48e45" + + group = TSC.GroupItem("test-group") + group._id = "b4488bce-80f0-11ea-af1c-976d0c1dab39" + + capabilities = {TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Deny} + + rules = [TSC.PermissionsRule(grantee=GroupItem.as_reference(group._id), capabilities=capabilities)] + + new_rules = server.projects.update_datasource_default_permissions(project, rules) + + assert "b4488bce-80f0-11ea-af1c-976d0c1dab39" == new_rules[0].grantee.id + + updated_capabilities = new_rules[0].capabilities + assert 4 == len(updated_capabilities) + assert "Deny" == updated_capabilities["ExportXml"] + assert "Allow" == updated_capabilities["Read"] + assert "Allow" == updated_capabilities["Write"] + assert "Allow" == updated_capabilities["Connect"] + + +def test_update_missing_id(server: TSC.Server) -> None: + single_project = TSC.ProjectItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.projects.update(single_project) + + +def test_create(server: TSC.Server) -> None: + response_xml = CREATE_XML.read_text() + with requests_mock.mock() as m: + m.post(server.projects.baseurl, text=response_xml) + new_project = TSC.ProjectItem(name="Test Project", description="Project created for testing") + new_project.content_permissions = "ManagedByOwner" + new_project.parent_id = "9a8f2265-70f3-4494-96c5-e5949d7a1120" + new_project = server.projects.create(new_project) + + assert "ccbea03f-77c4-4209-8774-f67bc59c3cef" == new_project.id + assert "Test Project" == new_project.name + assert "Project created for testing" == new_project.description + assert "ManagedByOwner" == new_project.content_permissions + assert "9a8f2265-70f3-4494-96c5-e5949d7a1120" == new_project.parent_id + + +def test_create_missing_name() -> None: + TSC.ProjectItem() + + +def test_populate_permissions(server: TSC.Server) -> None: + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.projects.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) + single_project = TSC.ProjectItem("Project3") + single_project._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + + server.projects.populate_permissions(single_project) + permissions = single_project.permissions + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "c8f2773a-c83a-11e8-8c8f-33e6d787b506" + assert permissions[0].capabilities == { + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + } + + +def test_populate_workbooks(server: TSC.Server) -> None: + response_xml = POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get( + server.projects.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks", + text=response_xml, + ) + single_project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") + single_project.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + + server.projects.populate_workbook_default_permissions(single_project) + permissions = single_project.default_workbook_permissions + + rule1 = permissions.pop() + + assert "c8f2773a-c83a-11e8-8c8f-33e6d787b506" == rule1.grantee.id + assert "group" == rule1.grantee.tag_name + assert rule1.capabilities == { + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ViewUnderlyingData: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, + } + + +def test_delete_permission(server: TSC.Server) -> None: + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.projects.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) + + single_group = TSC.GroupItem("Group1") + single_group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" + + single_project = TSC.ProjectItem("Project3") + single_project._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" + + server.projects.populate_permissions(single_project) + permissions = single_project.permissions + + capabilities = {} + + for permission in permissions: + if permission.grantee.tag_name == "group": + if permission.grantee.id == single_group._id: + capabilities = permission.capabilities + + rules = TSC.PermissionsRule(grantee=GroupItem.as_reference(single_group._id), capabilities=capabilities) + + endpoint = f"{single_project._id}/permissions/groups/{single_group._id}" + m.delete(f"{server.projects.baseurl}/{endpoint}/Read/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/Write/Allow", status_code=204) + server.projects.delete_permission(item=single_project, rules=rules) + + +def test_delete_workbook_default_permission(server: TSC.Server) -> None: + response_xml = POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML.read_text() + + with requests_mock.mock() as m: + m.get( + server.projects.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks", + text=response_xml, ) - def test_delete_permission(self) -> None: - with open(asset(POPULATE_PERMISSIONS_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5/permissions", text=response_xml) - - single_group = TSC.GroupItem("Group1") - single_group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" - - single_project = TSC.ProjectItem("Project3") - single_project._id = "0448d2ed-590d-4fa0-b272-a2a8a24555b5" - - self.server.projects.populate_permissions(single_project) - permissions = single_project.permissions - - capabilities = {} - - for permission in permissions: - if permission.grantee.tag_name == "group": - if permission.grantee.id == single_group._id: - capabilities = permission.capabilities - - rules = TSC.PermissionsRule(grantee=GroupItem.as_reference(single_group._id), capabilities=capabilities) - - endpoint = f"{single_project._id}/permissions/groups/{single_group._id}" - m.delete(f"{self.baseurl}/{endpoint}/Read/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/Write/Allow", status_code=204) - self.server.projects.delete_permission(item=single_project, rules=rules) - - def test_delete_workbook_default_permission(self) -> None: - with open(asset(POPULATE_WORKBOOK_DEFAULT_PERMISSIONS_XML), "rb") as f: - response_xml = f.read().decode("utf-8") - - with requests_mock.mock() as m: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/workbooks", text=response_xml - ) - - single_group = TSC.GroupItem("Group1") - single_group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" - - single_project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") - single_project._owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - - self.server.projects.populate_workbook_default_permissions(single_project) - permissions = single_project.default_workbook_permissions - - capabilities = { - # View - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, - # Interact/Edit - TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ViewUnderlyingData: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow, - # Edit - TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Allow, - } - - rules = TSC.PermissionsRule(grantee=GroupItem.as_reference(single_group._id), capabilities=capabilities) - - endpoint = f"{single_project._id}/default-permissions/workbooks/groups/{single_group._id}" - m.delete(f"{self.baseurl}/{endpoint}/Read/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ExportImage/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ExportData/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ViewComments/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/AddComment/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/Filter/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ViewUnderlyingData/Deny", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ShareView/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/WebAuthoring/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/Write/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ExportXml/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ChangeHierarchy/Allow", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/Delete/Deny", status_code=204) - m.delete(f"{self.baseurl}/{endpoint}/ChangePermissions/Allow", status_code=204) - self.server.projects.delete_workbook_default_permissions(item=single_project, rule=rules) - - def test_populate_virtualconnection_default_permissions(self): - response_xml = read_xml_asset(POPULATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML) - - self.server.version = "3.23" - base_url = self.server.projects.baseurl - - with requests_mock.mock() as m: - m.get( - base_url + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/virtualConnections", - text=response_xml, - ) - project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") - project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - - self.server.projects.populate_virtualconnection_default_permissions(project) - permissions = project.default_virtualconnection_permissions - - rule = permissions.pop() - - self.assertEqual("c8f2773a-c83a-11e8-8c8f-33e6d787b506", rule.grantee.id) - self.assertEqual("group", rule.grantee.tag_name) - self.assertDictEqual( - rule.capabilities, - { - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny, - }, + single_group = TSC.GroupItem("Group1") + single_group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" + + single_project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") + single_project._owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + + server.projects.populate_workbook_default_permissions(single_project) + permissions = single_project.default_workbook_permissions + + capabilities = { + # View + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, + # Interact/Edit + TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ViewUnderlyingData: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow, + # Edit + TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportXml: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Allow, + } + + rules = TSC.PermissionsRule(grantee=GroupItem.as_reference(single_group._id), capabilities=capabilities) + + endpoint = f"{single_project._id}/default-permissions/workbooks/groups/{single_group._id}" + m.delete(f"{server.projects.baseurl}/{endpoint}/Read/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ExportImage/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ExportData/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ViewComments/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/AddComment/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/Filter/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ViewUnderlyingData/Deny", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ShareView/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/WebAuthoring/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/Write/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ExportXml/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ChangeHierarchy/Allow", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/Delete/Deny", status_code=204) + m.delete(f"{server.projects.baseurl}/{endpoint}/ChangePermissions/Allow", status_code=204) + server.projects.delete_workbook_default_permissions(item=single_project, rule=rules) + + +def test_populate_virtualconnection_default_permissions(server: TSC.Server) -> None: + response_xml = POPULATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML.read_text() + + server.version = "3.23" + base_url = server.projects.baseurl + + with requests_mock.mock() as m: + m.get( + base_url + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/virtualConnections", + text=response_xml, ) + project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") + project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + + server.projects.populate_virtualconnection_default_permissions(project) + permissions = project.default_virtualconnection_permissions + + rule = permissions.pop() + + assert "c8f2773a-c83a-11e8-8c8f-33e6d787b506" == rule.grantee.id + assert "group" == rule.grantee.tag_name + assert rule.capabilities == { + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ChangePermissions: TSC.Permission.Mode.Deny, + } + + +def test_update_virtualconnection_default_permissions(server: TSC.Server) -> None: + response_xml = UPDATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML.read_text() - def test_update_virtualconnection_default_permissions(self): - response_xml = read_xml_asset(UPDATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML) - - self.server.version = "3.23" - base_url = self.server.projects.baseurl - - with requests_mock.mock() as m: - m.put( - base_url + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/virtualConnections", - text=response_xml, - ) - project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") - project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - - group = TSC.GroupItem("test-group") - group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" - - capabilities = { - TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny, - } - - rules = [TSC.PermissionsRule(GroupItem.as_reference(group.id), capabilities)] - new_rules = self.server.projects.update_virtualconnection_default_permissions(project, rules) - - rule = new_rules.pop() - - self.assertEqual(group.id, rule.grantee.id) - self.assertEqual("group", rule.grantee.tag_name) - self.assertDictEqual( - rule.capabilities, - { - TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny, - }, + server.version = "3.23" + base_url = server.projects.baseurl + + with requests_mock.mock() as m: + m.put( + base_url + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/virtualConnections", + text=response_xml, ) + project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") + project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + + group = TSC.GroupItem("test-group") + group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" + + capabilities = { + TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny, + } + + assert group.id is not None + rules = [TSC.PermissionsRule(GroupItem.as_reference(group.id), capabilities)] + new_rules = server.projects.update_virtualconnection_default_permissions(project, rules) + + rule = new_rules.pop() - def test_delete_virtualconnection_default_permimssions(self): - response_xml = read_xml_asset(POPULATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML) + assert group.id == rule.grantee.id + assert "group" == rule.grantee.tag_name + assert rule.capabilities == { + TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Delete: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny, + } - self.server.version = "3.23" - base_url = self.server.projects.baseurl - with requests_mock.mock() as m: - m.get( - base_url + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/virtualConnections", - text=response_xml, - ) +def test_delete_virtualconnection_default_permimssions(server: TSC.Server) -> None: + response_xml = POPULATE_VIRTUALCONNECTION_DEFAULT_PERMISSIONS_XML.read_text() + + server.version = "3.23" + base_url = server.projects.baseurl + + with requests_mock.mock() as m: + m.get( + base_url + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/default-permissions/virtualConnections", + text=response_xml, + ) + + project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") + project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" - project = TSC.ProjectItem("test", "1d0304cd-3796-429f-b815-7258370b9b74") - project._id = "9dbd2263-16b5-46e1-9c43-a76bb8ab65fb" + group = TSC.GroupItem("test-group") + group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" - group = TSC.GroupItem("test-group") - group._id = "c8f2773a-c83a-11e8-8c8f-33e6d787b506" + server.projects.populate_virtualconnection_default_permissions(project) + permissions = project.default_virtualconnection_permissions - self.server.projects.populate_virtualconnection_default_permissions(project) - permissions = project.default_virtualconnection_permissions + del_caps = { + TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow, + } - del_caps = { - TSC.Permission.Capability.ChangeHierarchy: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.Connect: TSC.Permission.Mode.Allow, - } + assert group.id is not None + rule = TSC.PermissionsRule(GroupItem.as_reference(group.id), del_caps) - rule = TSC.PermissionsRule(GroupItem.as_reference(group.id), del_caps) + endpoint = f"{project.id}/default-permissions/virtualConnections/groups/{group.id}" + m.delete(f"{base_url}/{endpoint}/ChangeHierarchy/Deny", status_code=204) + m.delete(f"{base_url}/{endpoint}/Connect/Allow", status_code=204) - endpoint = f"{project.id}/default-permissions/virtualConnections/groups/{group.id}" - m.delete(f"{base_url}/{endpoint}/ChangeHierarchy/Deny", status_code=204) - m.delete(f"{base_url}/{endpoint}/Connect/Allow", status_code=204) + server.projects.delete_virtualconnection_default_permissions(project, rule) - self.server.projects.delete_virtualconnection_default_permissions(project, rule) - def test_get_all_fields(self) -> None: - self.server.version = "3.23" - base_url = self.server.projects.baseurl - with open(GET_XML_ALL_FIELDS, "rb") as f: - response_xml = f.read().decode("utf-8") +def test_get_all_fields(server: TSC.Server) -> None: + server.version = "3.23" + base_url = server.projects.baseurl + response_xml = GET_XML_ALL_FIELDS.read_text() - ro = TSC.RequestOptions() - ro.all_fields = True + ro = TSC.RequestOptions() + ro.all_fields = True - with requests_mock.mock() as m: - m.get(f"{base_url}?fields=_all_", text=response_xml) - all_projects, pagination_item = self.server.projects.get(req_options=ro) + with requests_mock.mock() as m: + m.get(f"{base_url}?fields=_all_", text=response_xml) + all_projects, pagination_item = server.projects.get(req_options=ro) - assert pagination_item.total_available == 3 - assert len(all_projects) == 1 - project: TSC.ProjectItem = all_projects[0] - assert isinstance(project, TSC.ProjectItem) - assert project.id == "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - assert project.name == "Samples" - assert project.description == "This project includes automatically uploaded samples." - assert project.top_level_project is True - assert project.content_permissions == "ManagedByOwner" - assert project.parent_id is None - assert project.writeable is True + assert pagination_item.total_available == 3 + assert len(all_projects) == 1 + project: TSC.ProjectItem = all_projects[0] + assert isinstance(project, TSC.ProjectItem) + assert project.id == "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + assert project.name == "Samples" + assert project.description == "This project includes automatically uploaded samples." + assert project.top_level_project is True + assert project.content_permissions == "ManagedByOwner" + assert project.parent_id is None + assert project.writeable is True diff --git a/test/test_project_model.py b/test/test_project_model.py index ecfe1bd14..b51a218ec 100644 --- a/test/test_project_model.py +++ b/test/test_project_model.py @@ -1,21 +1,22 @@ -import unittest +import pytest import tableauserverclient as TSC -class ProjectModelTests(unittest.TestCase): - def test_nullable_name(self): - TSC.ProjectItem(None) - TSC.ProjectItem("") - project = TSC.ProjectItem("proj") - project.name = None +def test_nullable_name(): + TSC.ProjectItem(None) + TSC.ProjectItem("") + project = TSC.ProjectItem("proj") + project.name = None - def test_invalid_content_permissions(self): - project = TSC.ProjectItem("proj") - with self.assertRaises(ValueError): - project.content_permissions = "Hello" - def test_parent_id(self): - project = TSC.ProjectItem("proj") - project.parent_id = "foo" - self.assertEqual(project.parent_id, "foo") +def test_invalid_content_permissions(): + project = TSC.ProjectItem("proj") + with pytest.raises(ValueError): + project.content_permissions = "Hello" + + +def test_parent_id(): + project = TSC.ProjectItem("proj") + project.parent_id = "foo" + assert project.parent_id == "foo" diff --git a/test/test_regression_tests.py b/test/test_regression_tests.py index 62e301591..21fbf3848 100644 --- a/test/test_regression_tests.py +++ b/test/test_regression_tests.py @@ -1,4 +1,3 @@ -import unittest from unittest import mock import tableauserverclient.server.request_factory as factory @@ -6,78 +5,80 @@ from tableauserverclient.filesys_helpers import to_filename, make_download_path -class BugFix257(unittest.TestCase): - def test_empty_request_works(self): - result = factory.EmptyRequest().empty_req() - self.assertEqual(b"", result) - - -class FileSysHelpers(unittest.TestCase): - def test_to_filename(self): - invalid = [ - "23brhafbjrjhkbbea.txt", - "a_b_C.txt", - "windows space.txt", - "abc#def.txt", - "t@bL3A()", - ] - - valid = [ - "23brhafbjrjhkbbea.txt", - "a_b_C.txt", - "windows space.txt", - "abcdef.txt", - "tbL3A", - ] - - self.assertTrue(all([(to_filename(i) == v) for i, v in zip(invalid, valid)])) - - def test_make_download_path(self): - no_file_path = (None, "file.ext") - has_file_path_folder = ("/root/folder/", "file.ext") - has_file_path_file = ("outx", "file.ext") - - self.assertEqual("file.ext", make_download_path(*no_file_path)) - self.assertEqual("outx.ext", make_download_path(*has_file_path_file)) - - 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 +def test_empty_request_works(): + result = factory.EmptyRequest().empty_req() + assert b"" == result + + +def test_to_filename(): + invalid = [ + "23brhafbjrjhkbbea.txt", + "a_b_C.txt", + "windows space.txt", + "abc#def.txt", + "t@bL3A()", + ] + + valid = [ + "23brhafbjrjhkbbea.txt", + "a_b_C.txt", + "windows space.txt", + "abcdef.txt", + "tbL3A", + ] + + assert all([(to_filename(i) == v) for i, v in zip(invalid, valid)]) + + +def test_make_download_path(): + no_file_path = (None, "file.ext") + has_file_path_folder = ("/root/folder/", "file.ext") + has_file_path_file = ("outx", "file.ext") + + assert "file.ext" == make_download_path(*no_file_path) + assert "outx.ext" == make_download_path(*has_file_path_file) + + with mock.patch("os.path.isdir") as mocked_isdir: + mocked_isdir.return_value = True + assert "/root/folder/file.ext" == make_download_path(*has_file_path_folder) + + +def test_redact_password_string(): + 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(): + redacted = redact_xml( + b"" + ) + assert redacted.find(b"value") == -1 + assert redacted.find(b"secret") == -1 + + +def test_redact_password_with_special_char(): + 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(): + redacted = redact_xml( + " " + ) + assert redacted.find("my_s per_secre>_passphrase_which_nobody_should_ever_see") == -1 + + +def test_redact_password_really_not_xml(): + 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 diff --git a/test/test_request_option.py b/test/test_request_option.py index 57dfdc2a0..2c5354b2a 100644 --- a/test/test_request_option.py +++ b/test/test_request_option.py @@ -1,380 +1,443 @@ -import os from pathlib import Path -import re -import unittest from urllib.parse import parse_qs +import pytest import requests_mock import tableauserverclient as TSC TEST_ASSET_DIR = Path(__file__).parent / "assets" -PAGINATION_XML = os.path.join(TEST_ASSET_DIR, "request_option_pagination.xml") -PAGE_NUMBER_XML = os.path.join(TEST_ASSET_DIR, "request_option_page_number.xml") -PAGE_SIZE_XML = os.path.join(TEST_ASSET_DIR, "request_option_page_size.xml") -FILTER_EQUALS = os.path.join(TEST_ASSET_DIR, "request_option_filter_equals.xml") -FILTER_NAME_IN = os.path.join(TEST_ASSET_DIR, "request_option_filter_name_in.xml") -FILTER_TAGS_IN = os.path.join(TEST_ASSET_DIR, "request_option_filter_tags_in.xml") -FILTER_MULTIPLE = os.path.join(TEST_ASSET_DIR, "request_option_filter_tags_in.xml") -SLICING_QUERYSET = os.path.join(TEST_ASSET_DIR, "request_option_slicing_queryset.xml") +PAGINATION_XML = TEST_ASSET_DIR / "request_option_pagination.xml" +PAGE_NUMBER_XML = TEST_ASSET_DIR / "request_option_page_number.xml" +PAGE_SIZE_XML = TEST_ASSET_DIR / "request_option_page_size.xml" +FILTER_EQUALS = TEST_ASSET_DIR / "request_option_filter_equals.xml" +FILTER_NAME_IN = TEST_ASSET_DIR / "request_option_filter_name_in.xml" +FILTER_TAGS_IN = TEST_ASSET_DIR / "request_option_filter_tags_in.xml" +FILTER_MULTIPLE = TEST_ASSET_DIR / "request_option_filter_tags_in.xml" +SLICING_QUERYSET = TEST_ASSET_DIR / "request_option_slicing_queryset.xml" SLICING_QUERYSET_PAGE_1 = TEST_ASSET_DIR / "queryset_slicing_page_1.xml" SLICING_QUERYSET_PAGE_2 = TEST_ASSET_DIR / "queryset_slicing_page_2.xml" -class RequestOptionTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False, http_options={"timeout": 5}) - - # Fake signin - self.server.version = "3.10" - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = f"{self.server.sites.baseurl}/{self.server._site_id}" - - def test_pagination(self) -> None: - with open(PAGINATION_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/views?pageNumber=1&pageSize=10", text=response_xml) - req_option = TSC.RequestOptions().page_size(10) - all_views, pagination_item = self.server.views.get(req_option) - - self.assertEqual(1, pagination_item.page_number) - self.assertEqual(10, pagination_item.page_size) - self.assertEqual(33, pagination_item.total_available) - self.assertEqual(10, len(all_views)) - - def test_page_number(self) -> None: - with open(PAGE_NUMBER_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/views?pageNumber=3", text=response_xml) - req_option = TSC.RequestOptions().page_number(3) - all_views, pagination_item = self.server.views.get(req_option) - - self.assertEqual(3, pagination_item.page_number) - self.assertEqual(100, pagination_item.page_size) - self.assertEqual(210, pagination_item.total_available) - self.assertEqual(10, len(all_views)) - - def test_page_size(self) -> None: - with open(PAGE_SIZE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/views?pageSize=5", text=response_xml) - req_option = TSC.RequestOptions().page_size(5) - all_views, pagination_item = self.server.views.get(req_option) - - self.assertEqual(1, pagination_item.page_number) - self.assertEqual(5, pagination_item.page_size) - self.assertEqual(33, pagination_item.total_available) - self.assertEqual(5, len(all_views)) - - def test_filter_equals(self) -> None: - with open(FILTER_EQUALS, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/workbooks?filter=name:eq:RESTAPISample", text=response_xml) - req_option = TSC.RequestOptions() - req_option.filter.add( - TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "RESTAPISample") +@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_pagination(server: TSC.Server) -> None: + response_xml = PAGINATION_XML.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?pageNumber=1&pageSize=10", text=response_xml) + req_option = TSC.RequestOptions().page_size(10) + all_views, pagination_item = server.views.get(req_option) + + assert 1 == pagination_item.page_number + assert 10 == pagination_item.page_size + assert 33 == pagination_item.total_available + assert 10 == len(all_views) + + +def test_page_number(server: TSC.Server) -> None: + response_xml = PAGE_NUMBER_XML.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?pageNumber=3", text=response_xml) + req_option = TSC.RequestOptions().page_number(3) + all_views, pagination_item = server.views.get(req_option) + + assert 3 == pagination_item.page_number + assert 100 == pagination_item.page_size + assert 210 == pagination_item.total_available + assert 10 == len(all_views) + + +def test_page_size(server: TSC.Server) -> None: + response_xml = PAGE_SIZE_XML.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?pageSize=5", text=response_xml) + req_option = TSC.RequestOptions().page_size(5) + all_views, pagination_item = server.views.get(req_option) + + assert 1 == pagination_item.page_number + assert 5 == pagination_item.page_size + assert 33 == pagination_item.total_available + assert 5 == len(all_views) + + +def test_filter_equals(server: TSC.Server) -> None: + response_xml = FILTER_EQUALS.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "?filter=name:eq:RESTAPISample", text=response_xml) + req_option = TSC.RequestOptions() + req_option.filter.add( + TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "RESTAPISample") + ) + matching_workbooks, pagination_item = server.workbooks.get(req_option) + + assert 2 == pagination_item.total_available + assert "RESTAPISample" == matching_workbooks[0].name + assert "RESTAPISample" == matching_workbooks[1].name + + +def test_filter_equals_shorthand(server: TSC.Server) -> None: + response_xml = FILTER_EQUALS.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "?filter=name:eq:RESTAPISample", text=response_xml) + matching_workbooks = server.workbooks.filter(name="RESTAPISample").order_by("name") + + assert 2 == matching_workbooks.total_available + assert "RESTAPISample" == matching_workbooks[0].name + assert "RESTAPISample" == matching_workbooks[1].name + + +def test_filter_tags_in(server: TSC.Server) -> None: + response_xml = FILTER_TAGS_IN.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "?filter=tags:in:[sample,safari,weather]", text=response_xml) + req_option = TSC.RequestOptions() + req_option.filter.add( + TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["sample", "safari", "weather"]) + ) + matching_workbooks, pagination_item = server.workbooks.get(req_option) + + assert 3 == pagination_item.total_available + assert {"weather"} == matching_workbooks[0].tags + assert {"safari"} == matching_workbooks[1].tags + assert {"sample"} == matching_workbooks[2].tags + + +# check if filtered projects with spaces & special characters +# get correctly returned +def test_filter_name_in(server: TSC.Server) -> None: + response_xml = FILTER_NAME_IN.read_text("utf8") + with requests_mock.mock() as m: + m.get( + server.projects.baseurl + "?filter=name%3Ain%3A%5Bdefault%2CSalesforce+Sales+Proje%C5%9Bt%5D", + text=response_xml, + ) + req_option = TSC.RequestOptions() + req_option.filter.add( + TSC.Filter( + TSC.RequestOptions.Field.Name, + TSC.RequestOptions.Operator.In, + ["default", "Salesforce Sales Projeśt"], ) - matching_workbooks, pagination_item = self.server.workbooks.get(req_option) - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("RESTAPISample", matching_workbooks[0].name) - self.assertEqual("RESTAPISample", matching_workbooks[1].name) - - def test_filter_equals_shorthand(self) -> None: - with open(FILTER_EQUALS, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/workbooks?filter=name:eq:RESTAPISample", text=response_xml) - matching_workbooks = self.server.workbooks.filter(name="RESTAPISample").order_by("name") - - self.assertEqual(2, matching_workbooks.total_available) - self.assertEqual("RESTAPISample", matching_workbooks[0].name) - self.assertEqual("RESTAPISample", matching_workbooks[1].name) - - def test_filter_tags_in(self) -> None: - with open(FILTER_TAGS_IN, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/workbooks?filter=tags:in:[sample,safari,weather]", text=response_xml) - req_option = TSC.RequestOptions() - req_option.filter.add( - TSC.Filter( - TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["sample", "safari", "weather"] - ) - ) - matching_workbooks, pagination_item = self.server.workbooks.get(req_option) - - self.assertEqual(3, pagination_item.total_available) - self.assertEqual({"weather"}, matching_workbooks[0].tags) - self.assertEqual({"safari"}, matching_workbooks[1].tags) - self.assertEqual({"sample"}, matching_workbooks[2].tags) - - # check if filtered projects with spaces & special characters - # get correctly returned - def test_filter_name_in(self) -> None: - with open(FILTER_NAME_IN, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get( - self.baseurl + "/projects?filter=name%3Ain%3A%5Bdefault%2CSalesforce+Sales+Proje%C5%9Bt%5D", - text=response_xml, - ) - req_option = TSC.RequestOptions() - req_option.filter.add( - TSC.Filter( - TSC.RequestOptions.Field.Name, - TSC.RequestOptions.Operator.In, - ["default", "Salesforce Sales Projeśt"], - ) - ) - matching_projects, pagination_item = self.server.projects.get(req_option) - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("default", matching_projects[0].name) - self.assertEqual("Salesforce Sales Projeśt", matching_projects[1].name) - - def test_filter_tags_in_shorthand(self) -> None: - with open(FILTER_TAGS_IN, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/workbooks?filter=tags:in:[sample,safari,weather]", text=response_xml) - matching_workbooks = self.server.workbooks.filter(tags__in=["sample", "safari", "weather"]) - - self.assertEqual(3, matching_workbooks.total_available) - self.assertEqual({"weather"}, matching_workbooks[0].tags) - self.assertEqual({"safari"}, matching_workbooks[1].tags) - self.assertEqual({"sample"}, matching_workbooks[2].tags) - - def test_invalid_shorthand_option(self) -> None: - with self.assertRaises(ValueError): - self.server.workbooks.filter(nonexistant__in=["sample", "safari"]) - - def test_multiple_filter_options(self) -> None: - with open(FILTER_MULTIPLE, "rb") as f: - response_xml = f.read().decode("utf-8") - # To ensure that this is deterministic, run this a few times - with requests_mock.mock() as m: - # Sometimes pep8 requires you to do things you might not otherwise do - url = "".join( - ( - self.baseurl, - "/workbooks?pageNumber=1&pageSize=100&", - "filter=name:eq:foo,tags:in:[sample,safari,weather]", - ) - ) - m.get(url, text=response_xml) - req_option = TSC.RequestOptions() - req_option.filter.add( - TSC.Filter( - TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["sample", "safari", "weather"] - ) - ) - req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "foo")) - for _ in range(5): - matching_workbooks, pagination_item = self.server.workbooks.get(req_option) - self.assertEqual(3, pagination_item.total_available) - - # Test req_options if url already has query params - def test_double_query_params(self) -> None: - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views?queryParamExists=true" - opts = TSC.RequestOptions() - - opts.filter.add( - TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["stocks", "market"]) - ) - opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) - - resp = self.server.workbooks.get_request(url, request_object=opts) - self.assertTrue(re.search("queryparamexists=true", resp.request.query)) - self.assertTrue(re.search("filter=tags%3ain%3a%5bstocks%2cmarket%5d", resp.request.query)) - self.assertTrue(re.search("sort=name%3aasc", resp.request.query)) - - # Test req_options for versions below 3.7 - def test_filter_sort_legacy(self) -> None: - self.server.version = "3.6" - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views?queryParamExists=true" - opts = TSC.RequestOptions() - - opts.filter.add( - TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["stocks", "market"]) + ) + matching_projects, pagination_item = server.projects.get(req_option) + + assert 2 == pagination_item.total_available + assert "default" == matching_projects[0].name + assert "Salesforce Sales Projeśt" == matching_projects[1].name + + +def test_filter_tags_in_shorthand(server: TSC.Server) -> None: + response_xml = FILTER_TAGS_IN.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "?filter=tags:in:[sample,safari,weather]", text=response_xml) + matching_workbooks = server.workbooks.filter(tags__in=["sample", "safari", "weather"]) + + assert 3 == matching_workbooks.total_available + assert {"weather"} == matching_workbooks[0].tags + assert {"safari"} == matching_workbooks[1].tags + assert {"sample"} == matching_workbooks[2].tags + + +def test_invalid_shorthand_option(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.workbooks.filter(nonexistant__in=["sample", "safari"]) + + +def test_multiple_filter_options(server: TSC.Server) -> None: + response_xml = FILTER_MULTIPLE.read_text() + # To ensure that this is deterministic, run this a few times + with requests_mock.mock() as m: + # Sometimes pep8 requires you to do things you might not otherwise do + url = "".join( + ( + server.workbooks.baseurl, + "?pageNumber=1&pageSize=100&", + "filter=name:eq:foo,tags:in:[sample,safari,weather]", ) - opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) - - resp = self.server.workbooks.get_request(url, request_object=opts) - self.assertTrue(re.search("queryparamexists=true", resp.request.query)) - self.assertTrue(re.search("filter=tags:in:%5bstocks,market%5d", resp.request.query)) - self.assertTrue(re.search("sort=name:asc", resp.request.query)) - - def test_vf(self) -> None: - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views/456/data" - opts = TSC.PDFRequestOptions() - opts.vf("name1#", "value1") - opts.vf("name2$", "value2") - opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid - - resp = self.server.workbooks.get_request(url, request_object=opts) - self.assertTrue(re.search("vf_name1%23=value1", resp.request.query)) - self.assertTrue(re.search("vf_name2%24=value2", resp.request.query)) - self.assertTrue(re.search("type=tabloid", resp.request.query)) - - # Test req_options for versions beloe 3.7 - def test_vf_legacy(self) -> None: - self.server.version = "3.6" - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views/456/data" - opts = TSC.PDFRequestOptions() - opts.vf("name1@", "value1") - opts.vf("name2$", "value2") - opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid - - resp = self.server.workbooks.get_request(url, request_object=opts) - self.assertTrue(re.search("vf_name1@=value1", resp.request.query)) - self.assertTrue(re.search("vf_name2\\$=value2", resp.request.query)) - self.assertTrue(re.search("type=tabloid", resp.request.query)) - - def test_all_fields(self) -> None: - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views/456/data" - opts = TSC.RequestOptions() - opts.all_fields = True - - resp = self.server.users.get_request(url, request_object=opts) - self.assertTrue(re.search("fields=_all_", resp.request.query)) - - def test_multiple_filter_options_shorthand(self) -> None: - with open(FILTER_MULTIPLE, "rb") as f: - response_xml = f.read().decode("utf-8") - # To ensure that this is deterministic, run this a few times - with requests_mock.mock() as m: - # Sometimes pep8 requires you to do things you might not otherwise do - url = "".join( - ( - self.baseurl, - "/workbooks?pageNumber=1&pageSize=100&", - "filter=name:eq:foo,tags:in:[sample,safari,weather]", - ) + ) + m.get(url, text=response_xml) + req_option = TSC.RequestOptions() + req_option.filter.add( + TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["sample", "safari", "weather"]) + ) + req_option.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "foo")) + for _ in range(5): + matching_workbooks, pagination_item = server.workbooks.get(req_option) + assert 3 == pagination_item.total_available + + +# Test req_options if url already has query params +def test_double_query_params(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.workbooks.baseurl + "?queryParamExists=true" + opts = TSC.RequestOptions() + + opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["stocks", "market"])) + opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) + + resp = server.workbooks.get_request(url, request_object=opts) + query_string = parse_qs(resp.request.query) + assert "queryparamexists" in query_string + assert "true" in query_string["queryparamexists"] + assert "filter" in query_string + assert "tags:in:[stocks,market]" in query_string["filter"] + assert "sort" in query_string + assert "name:asc" in query_string["sort"] + + +# Test req_options for versions below 3.7 +def test_filter_sort_legacy(server: TSC.Server) -> None: + server.version = "3.6" + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.workbooks.baseurl + "?queryParamExists=true" + opts = TSC.RequestOptions() + + opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["stocks", "market"])) + opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) + + resp = server.workbooks.get_request(url, request_object=opts) + query_string = parse_qs(resp.request.query) + assert "queryparamexists" in query_string + assert "true" in query_string["queryparamexists"] + assert "filter" in query_string + assert "tags:in:[stocks,market]" in query_string["filter"] + assert "sort" in query_string + assert "name:asc" in query_string["sort"] + + +def test_vf(server: TSC.Server) -> None: + server.version = "3.10" + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.workbooks.baseurl + "/456/data" + opts = TSC.PDFRequestOptions() + opts.vf("name1#", "value1") + opts.vf("name2$", "value2") + opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid + + resp = server.workbooks.get_request(url, request_object=opts) + query_string = parse_qs(resp.request.query) + assert "vf_name1#" in query_string + assert "value1" in query_string["vf_name1#"] + assert "vf_name2$" in query_string + assert "value2" in query_string["vf_name2$"] + assert "type" in query_string + assert "tabloid" in query_string["type"] + + +# Test req_options for versions below 3.7 +def test_vf_legacy(server: TSC.Server) -> None: + server.version = "3.6" + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.workbooks.baseurl + opts = TSC.PDFRequestOptions() + opts.vf("name1@", "value1") + opts.vf("name2$", "value2") + opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid + + resp = server.workbooks.get_request(url, request_object=opts) + query_string = parse_qs(resp.request.query) + assert "vf_name1@" in query_string + assert "value1" in query_string["vf_name1@"] + assert "vf_name2$" in query_string + assert "value2" in query_string["vf_name2$"] + assert "type" in query_string + assert "tabloid" in query_string["type"] + + +def test_all_fields(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.views.baseurl + "/456/data" + opts = TSC.RequestOptions() + opts.all_fields = True + + resp = server.users.get_request(url, request_object=opts) + query_string = parse_qs(resp.request.query) + assert "fields" in query_string + assert ["_all_"] == query_string["fields"] + + +def test_multiple_filter_options_shorthand(server: TSC.Server) -> None: + response_xml = FILTER_MULTIPLE.read_text() + # To ensure that this is deterministic, run this a few times + with requests_mock.mock() as m: + # Sometimes pep8 requires you to do things you might not otherwise do + url = "".join( + ( + server.workbooks.baseurl, + "?pageNumber=1&pageSize=100&", + "filter=name:eq:foo,tags:in:[sample,safari,weather]", ) - m.get(url, text=response_xml) - - for _ in range(5): - matching_workbooks = self.server.workbooks.filter(tags__in=["sample", "safari", "weather"], name="foo") - self.assertEqual(3, matching_workbooks.total_available) - - def test_slicing_queryset(self) -> None: - with open(SLICING_QUERYSET, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/views?pageNumber=1", text=response_xml) - all_views = self.server.views.all() - - self.assertEqual(10, len(all_views[::])) - self.assertEqual(5, len(all_views[::2])) - self.assertEqual(8, len(all_views[2:])) - self.assertEqual(2, len(all_views[:2])) - self.assertEqual(3, len(all_views[2:5])) - self.assertEqual(3, len(all_views[-3:])) - self.assertEqual(3, len(all_views[-6:-3])) - self.assertEqual(3, len(all_views[3:6:-1])) - self.assertEqual(3, len(all_views[6:3:-1])) - self.assertEqual(10, len(all_views[::-1])) - self.assertEqual(all_views[3:6], list(reversed(all_views[3:6:-1]))) - - self.assertEqual(all_views[-3].id, "2df55de2-3a2d-4e34-b515-6d4e70b830e9") - - with self.assertRaises(IndexError): - all_views[100] - - def test_slicing_queryset_multi_page(self) -> None: - with requests_mock.mock() as m: - m.get(self.baseurl + "/views?pageNumber=1", text=SLICING_QUERYSET_PAGE_1.read_text()) - m.get(self.baseurl + "/views?pageNumber=2", text=SLICING_QUERYSET_PAGE_2.read_text()) - sliced_views = self.server.views.all()[9:12] - - self.assertEqual(sliced_views[0].id, "2e6d6c81-da71-4b41-892c-ba80d4e7a6d0") - self.assertEqual(sliced_views[1].id, "47ffcb8e-3f7a-4ecf-8ab3-605da9febe20") - self.assertEqual(sliced_views[2].id, "6757fea8-0aa9-4160-a87c-9be27b1d1c8c") - - def test_queryset_filter_args_error(self) -> None: - with self.assertRaises(RuntimeError): - workbooks = self.server.workbooks.filter("argument") - - def test_filtering_parameters(self) -> None: - self.server.version = "3.6" - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views/456/data" - opts = TSC.PDFRequestOptions() - opts.parameter("name1@", "value1") - opts.parameter("name2$", "value2") - opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid - - resp = self.server.workbooks.get_request(url, request_object=opts) - query_params = parse_qs(resp.request.query) - self.assertIn("name1@", query_params) - self.assertIn("value1", query_params["name1@"]) - self.assertIn("name2$", query_params) - self.assertIn("value2", query_params["name2$"]) - self.assertIn("type", query_params) - self.assertIn("tabloid", query_params["type"]) - - def test_queryset_endpoint_pagesize_all(self) -> None: - for page_size in (1, 10, 100, 1000): - with self.subTest(page_size): - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/views?pageSize={page_size}", text=SLICING_QUERYSET_PAGE_1.read_text()) - queryset = self.server.views.all(page_size=page_size) - assert queryset.request_options.pagesize == page_size - _ = list(queryset) - - def test_queryset_endpoint_pagesize_filter(self) -> None: - for page_size in (1, 10, 100, 1000): - with self.subTest(page_size): - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/views?pageSize={page_size}", text=SLICING_QUERYSET_PAGE_1.read_text()) - queryset = self.server.views.filter(page_size=page_size) - assert queryset.request_options.pagesize == page_size - _ = list(queryset) - - def test_queryset_pagesize_filter(self) -> None: - for page_size in (1, 10, 100, 1000): - with self.subTest(page_size): - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/views?pageSize={page_size}", text=SLICING_QUERYSET_PAGE_1.read_text()) - queryset = self.server.views.all().filter(page_size=page_size) - assert queryset.request_options.pagesize == page_size - _ = list(queryset) - - def test_language_export(self) -> None: - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = self.baseurl + "/views/456/data" - opts = TSC.PDFRequestOptions() - opts.language = "en-US" - - resp = self.server.users.get_request(url, request_object=opts) - self.assertTrue(re.search("language=en-us", resp.request.query)) - - def test_queryset_fields(self) -> None: - loop = self.server.users.fields("id") - assert "id" in loop.request_options.fields - assert "_default_" in loop.request_options.fields - - def test_queryset_only_fields(self) -> None: - loop = self.server.users.only_fields("id") - assert "id" in loop.request_options.fields - assert "_default_" not in loop.request_options.fields + ) + m.get(url, text=response_xml) + + for _ in range(5): + matching_workbooks = server.workbooks.filter(tags__in=["sample", "safari", "weather"], name="foo") + assert 3 == matching_workbooks.total_available + + +def test_slicing_queryset(server: TSC.Server) -> None: + response_xml = SLICING_QUERYSET.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?pageNumber=1", text=response_xml) + all_views = server.views.all() + + assert 10 == len(all_views[::]) + assert 5 == len(all_views[::2]) + assert 8 == len(all_views[2:]) + assert 2 == len(all_views[:2]) + assert 3 == len(all_views[2:5]) + assert 3 == len(all_views[-3:]) + assert 3 == len(all_views[-6:-3]) + assert 3 == len(all_views[3:6:-1]) + assert 3 == len(all_views[6:3:-1]) + assert 10 == len(all_views[::-1]) + assert all_views[3:6] == list(reversed(all_views[3:6:-1])) + + assert all_views[-3].id == "2df55de2-3a2d-4e34-b515-6d4e70b830e9" + + with pytest.raises(IndexError): + all_views[100] + + +def test_slicing_queryset_multi_page(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?pageNumber=1", text=SLICING_QUERYSET_PAGE_1.read_text()) + m.get(server.views.baseurl + "?pageNumber=2", text=SLICING_QUERYSET_PAGE_2.read_text()) + sliced_views = server.views.all()[9:12] + + assert sliced_views[0].id == "2e6d6c81-da71-4b41-892c-ba80d4e7a6d0" + assert sliced_views[1].id == "47ffcb8e-3f7a-4ecf-8ab3-605da9febe20" + assert sliced_views[2].id == "6757fea8-0aa9-4160-a87c-9be27b1d1c8c" + + +def test_queryset_filter_args_error(server: TSC.Server) -> None: + with pytest.raises(RuntimeError): + workbooks = server.workbooks.filter("argument") + + +def test_filtering_parameters(server: TSC.Server) -> None: + server.version = "3.6" + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.workbooks.baseurl + "/456/data" + opts = TSC.PDFRequestOptions() + opts.parameter("name1@", "value1") + opts.parameter("name2$", "value2") + opts.parameter("Parameters.name3", "value3") + opts.parameter("vf_Parameters.name4", "value4") + opts.page_type = TSC.PDFRequestOptions.PageType.Tabloid + + # While Tableau Server side IS case sensitive with the query string, + # requiring the prefix to be "vf_Parameters", requests does not end + # up preserving the case sensitivity with the Response.Request + # object. It also shows up lowercased in the requests_mock request + # history. + resp = server.workbooks.get_request(url, request_object=opts) + query_params = parse_qs(resp.request.query) + assert "vf_parameters.name1@" in query_params + assert "value1" in query_params["vf_parameters.name1@"] + assert "vf_parameters.name2$" in query_params + assert "value2" in query_params["vf_parameters.name2$"] + assert "vf_parameters.name3" in query_params + assert "value3" in query_params["vf_parameters.name3"] + assert "vf_parameters.name4" in query_params + assert "value4" in query_params["vf_parameters.name4"] + assert "type" in query_params + assert "tabloid" in query_params["type"] + + +@pytest.mark.parametrize("page_size", [1, 10, 100, 1_000]) +def test_queryset_endpoint_pagesize_all(server: TSC.Server, page_size: int) -> None: + with requests_mock.mock() as m: + m.get(f"{server.views.baseurl}?pageSize={page_size}", text=SLICING_QUERYSET_PAGE_1.read_text()) + queryset = server.views.all(page_size=page_size) + assert queryset.request_options.pagesize == page_size + _ = list(queryset) + + +@pytest.mark.parametrize("page_size", [1, 10, 100, 1_000]) +def test_queryset_endpoint_pagesize_filter(server: TSC.Server, page_size: int) -> None: + with requests_mock.mock() as m: + m.get(f"{server.views.baseurl}?pageSize={page_size}", text=SLICING_QUERYSET_PAGE_1.read_text()) + queryset = server.views.filter(page_size=page_size) + assert queryset.request_options.pagesize == page_size + _ = list(queryset) + + +44 + + +@pytest.mark.parametrize("page_size", [1, 10, 100, 1_000]) +def test_queryset_pagesize_filter(server: TSC.Server, page_size: int) -> None: + with requests_mock.mock() as m: + m.get(f"{server.views.baseurl}?pageSize={page_size}", text=SLICING_QUERYSET_PAGE_1.read_text()) + queryset = server.views.all().filter(page_size=page_size) + assert queryset.request_options.pagesize == page_size + _ = list(queryset) + + +def test_language_export(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(requests_mock.ANY) + url = server.views.baseurl + "/456/data" + opts = TSC.PDFRequestOptions() + opts.language = "en-US" + + resp = server.users.get_request(url, request_object=opts) + query_string = parse_qs(resp.request.query) + assert "language" in query_string + assert "en-us" in query_string["language"] + + +def test_queryset_fields(server: TSC.Server) -> None: + loop = server.users.fields("id") + assert "id" in loop.request_options.fields + assert "_default_" in loop.request_options.fields + + +def test_queryset_only_fields(server: TSC.Server) -> None: + loop = server.users.only_fields("id") + assert "id" in loop.request_options.fields + assert "_default_" not in loop.request_options.fields + + +def test_queryset_field_order(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.views.baseurl, text=SLICING_QUERYSET_PAGE_1.read_text()) + loop = server.views.fields("id", "name") + list(loop) + history = m.request_history[0] + + fields = history.qs.get("fields", [""])[0].split(",") + + assert fields[0] == "_default_" + assert "id" in fields + assert "name" in fields + + +def test_queryset_field_all(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.views.baseurl, text=SLICING_QUERYSET_PAGE_1.read_text()) + loop = server.views.fields("id", "name", "_all_") + list(loop) + history = m.request_history[0] + + fields = history.qs.get("fields", [""])[0] + + assert fields == "_all_" diff --git a/test/test_requests.py b/test/test_requests.py index 5c0d090ba..5ee68b020 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,62 @@ 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() diff --git a/test/test_schedule.py b/test/test_schedule.py index 4fcc85e18..823a87607 100644 --- a/test/test_schedule.py +++ b/test/test_schedule.py @@ -1,425 +1,494 @@ -import os -import unittest +from pathlib import Path from datetime import time +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -GET_XML = os.path.join(TEST_ASSET_DIR, "schedule_get.xml") -GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_by_id.xml") -GET_HOURLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_hourly_id.xml") -GET_DAILY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_daily_id.xml") -GET_MONTHLY_ID_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id.xml") -GET_MONTHLY_ID_2_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_monthly_id_2.xml") -GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_empty.xml") -CREATE_HOURLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_hourly.xml") -CREATE_DAILY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_daily.xml") -CREATE_WEEKLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_weekly.xml") -CREATE_MONTHLY_XML = os.path.join(TEST_ASSET_DIR, "schedule_create_monthly.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "schedule_update.xml") -ADD_WORKBOOK_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_workbook.xml") -ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS = os.path.join(TEST_ASSET_DIR, "schedule_add_workbook_with_warnings.xml") -ADD_DATASOURCE_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_datasource.xml") -ADD_FLOW_TO_SCHEDULE = os.path.join(TEST_ASSET_DIR, "schedule_add_flow.xml") -GET_EXTRACT_TASKS_XML = os.path.join(TEST_ASSET_DIR, "schedule_get_extract_refresh_tasks.xml") - -WORKBOOK_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_by_id.xml") -DATASOURCE_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "datasource_get_by_id.xml") -FLOW_GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "flow_get_by_id.xml") - - -class ScheduleTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake Signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.schedules.baseurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_schedules, pagination_item = self.server.schedules.get() - - extract = all_schedules[0] - subscription = all_schedules[1] - flow = all_schedules[2] - system = all_schedules[3] - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("c9cff7f9-309c-4361-99ff-d4ba8c9f5467", extract.id) - self.assertEqual("Weekday early mornings", extract.name) - self.assertEqual("Active", extract.state) - self.assertEqual(50, extract.priority) - self.assertEqual("2016-07-06T20:19:00Z", format_datetime(extract.created_at)) - self.assertEqual("2016-09-13T11:00:32Z", format_datetime(extract.updated_at)) - self.assertEqual("Extract", extract.schedule_type) - self.assertEqual("2016-09-14T11:00:00Z", format_datetime(extract.next_run_at)) - - self.assertEqual("bcb79d07-6e47-472f-8a65-d7f51f40c36c", subscription.id) - self.assertEqual("Saturday night", subscription.name) - self.assertEqual("Active", subscription.state) - self.assertEqual(80, subscription.priority) - self.assertEqual("2016-07-07T20:19:00Z", format_datetime(subscription.created_at)) - self.assertEqual("2016-09-12T16:39:38Z", format_datetime(subscription.updated_at)) - self.assertEqual("Subscription", subscription.schedule_type) - self.assertEqual("2016-09-18T06:00:00Z", format_datetime(subscription.next_run_at)) - - self.assertEqual("f456e8f2-aeb2-4a8e-b823-00b6f08640f0", flow.id) - self.assertEqual("First of the month 1:00AM", flow.name) - self.assertEqual("Active", flow.state) - self.assertEqual(50, flow.priority) - self.assertEqual("2019-02-19T18:52:19Z", format_datetime(flow.created_at)) - self.assertEqual("2019-02-19T18:55:51Z", format_datetime(flow.updated_at)) - self.assertEqual("Flow", flow.schedule_type) - self.assertEqual("2019-03-01T09:00:00Z", format_datetime(flow.next_run_at)) - - self.assertEqual("3cfa4713-ce7c-4fa7-aa2e-f752bfc8dd04", system.id) - self.assertEqual("First of the month 2:00AM", system.name) - self.assertEqual("Active", system.state) - self.assertEqual(30, system.priority) - self.assertEqual("2019-02-19T18:52:19Z", format_datetime(system.created_at)) - self.assertEqual("2019-02-19T18:55:51Z", format_datetime(system.updated_at)) - self.assertEqual("System", system.schedule_type) - self.assertEqual("2019-03-01T09:00:00Z", format_datetime(system.next_run_at)) - - def test_get_empty(self) -> None: - with open(GET_EMPTY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_schedules, pagination_item = self.server.schedules.get() - - self.assertEqual(0, pagination_item.total_available) - self.assertEqual([], all_schedules) - - def test_get_by_id(self) -> None: - self.server.version = "3.8" - with open(GET_BY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" - m.get(baseurl, text=response_xml) - schedule = self.server.schedules.get_by_id(schedule_id) - self.assertIsNotNone(schedule) - self.assertEqual(schedule_id, schedule.id) - self.assertEqual("Weekday early mornings", schedule.name) - self.assertEqual("Active", schedule.state) - - def test_get_hourly_by_id(self) -> None: - self.server.version = "3.8" - with open(GET_HOURLY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" - m.get(baseurl, text=response_xml) - schedule = self.server.schedules.get_by_id(schedule_id) - self.assertIsNotNone(schedule) - self.assertEqual(schedule_id, schedule.id) - self.assertEqual("Hourly schedule", schedule.name) - self.assertEqual("Active", schedule.state) - self.assertEqual(("Monday", 0.5), schedule.interval_item.interval) - - def test_get_daily_by_id(self) -> None: - self.server.version = "3.8" - with open(GET_DAILY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" - m.get(baseurl, text=response_xml) - schedule = self.server.schedules.get_by_id(schedule_id) - self.assertIsNotNone(schedule) - self.assertEqual(schedule_id, schedule.id) - self.assertEqual("Daily schedule", schedule.name) - self.assertEqual("Active", schedule.state) - self.assertEqual(("Monday", 2.0), schedule.interval_item.interval) - - def test_get_monthly_by_id(self) -> None: - self.server.version = "3.8" - with open(GET_MONTHLY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" - m.get(baseurl, text=response_xml) - schedule = self.server.schedules.get_by_id(schedule_id) - self.assertIsNotNone(schedule) - self.assertEqual(schedule_id, schedule.id) - self.assertEqual("Monthly multiple days", schedule.name) - self.assertEqual("Active", schedule.state) - self.assertEqual(("1", "2"), schedule.interval_item.interval) - - def test_get_monthly_by_id_2(self) -> None: - self.server.version = "3.15" - with open(GET_MONTHLY_ID_2_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - schedule_id = "8c5caf33-6223-4724-83c3-ccdc1e730a07" - baseurl = f"{self.server.baseurl}/schedules/{schedule_id}" - m.get(baseurl, text=response_xml) - schedule = self.server.schedules.get_by_id(schedule_id) - self.assertIsNotNone(schedule) - self.assertEqual(schedule_id, schedule.id) - self.assertEqual("Monthly First Monday!", schedule.name) - self.assertEqual("Active", schedule.state) - self.assertEqual(("Monday", "First"), schedule.interval_item.interval) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204) - self.server.schedules.delete("c9cff7f9-309c-4361-99ff-d4ba8c9f5467") - - def test_create_hourly(self) -> None: - with open(CREATE_HOURLY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - hourly_interval = TSC.HourlyInterval(start_time=time(2, 30), end_time=time(23, 0), interval_value=2) - new_schedule = TSC.ScheduleItem( - "hourly-schedule-1", - 50, - TSC.ScheduleItem.Type.Extract, - TSC.ScheduleItem.ExecutionOrder.Parallel, - hourly_interval, - ) - new_schedule = self.server.schedules.create(new_schedule) - - self.assertEqual("5f42be25-8a43-47ba-971a-63f2d4e7029c", new_schedule.id) - self.assertEqual("hourly-schedule-1", new_schedule.name) - self.assertEqual("Active", new_schedule.state) - self.assertEqual(50, new_schedule.priority) - self.assertEqual("2016-09-15T20:47:33Z", format_datetime(new_schedule.created_at)) - self.assertEqual("2016-09-15T20:47:33Z", format_datetime(new_schedule.updated_at)) - self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type) - self.assertEqual("2016-09-16T01:30:00Z", format_datetime(new_schedule.next_run_at)) - self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order) - self.assertEqual(time(2, 30), new_schedule.interval_item.start_time) - self.assertEqual(time(23), new_schedule.interval_item.end_time) # type: ignore[union-attr] - self.assertEqual(("8",), new_schedule.interval_item.interval) # type: ignore[union-attr] - - def test_create_daily(self) -> None: - with open(CREATE_DAILY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - daily_interval = TSC.DailyInterval(time(4, 50)) - new_schedule = TSC.ScheduleItem( - "daily-schedule-1", - 90, - TSC.ScheduleItem.Type.Subscription, - TSC.ScheduleItem.ExecutionOrder.Serial, - daily_interval, - ) - new_schedule = self.server.schedules.create(new_schedule) - - self.assertEqual("907cae38-72fd-417c-892a-95540c4664cd", new_schedule.id) - self.assertEqual("daily-schedule-1", new_schedule.name) - self.assertEqual("Active", new_schedule.state) - self.assertEqual(90, new_schedule.priority) - self.assertEqual("2016-09-15T21:01:09Z", format_datetime(new_schedule.created_at)) - self.assertEqual("2016-09-15T21:01:09Z", format_datetime(new_schedule.updated_at)) - self.assertEqual(TSC.ScheduleItem.Type.Subscription, new_schedule.schedule_type) - self.assertEqual("2016-09-16T11:45:00Z", format_datetime(new_schedule.next_run_at)) - self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order) - self.assertEqual(time(4, 45), new_schedule.interval_item.start_time) - - def test_create_weekly(self) -> None: - with open(CREATE_WEEKLY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - weekly_interval = TSC.WeeklyInterval( - time(9, 15), TSC.IntervalItem.Day.Monday, TSC.IntervalItem.Day.Wednesday, TSC.IntervalItem.Day.Friday - ) - new_schedule = TSC.ScheduleItem( - "weekly-schedule-1", - 80, - TSC.ScheduleItem.Type.Extract, - TSC.ScheduleItem.ExecutionOrder.Parallel, - weekly_interval, - ) - new_schedule = self.server.schedules.create(new_schedule) - - self.assertEqual("1adff386-6be0-4958-9f81-a35e676932bf", new_schedule.id) - self.assertEqual("weekly-schedule-1", new_schedule.name) - self.assertEqual("Active", new_schedule.state) - self.assertEqual(80, new_schedule.priority) - self.assertEqual("2016-09-15T21:12:50Z", format_datetime(new_schedule.created_at)) - self.assertEqual("2016-09-15T21:12:50Z", format_datetime(new_schedule.updated_at)) - self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type) - self.assertEqual("2016-09-16T16:15:00Z", format_datetime(new_schedule.next_run_at)) - self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, new_schedule.execution_order) - self.assertEqual(time(9, 15), new_schedule.interval_item.start_time) - self.assertEqual(("Monday", "Wednesday", "Friday"), new_schedule.interval_item.interval) - self.assertEqual(2, len(new_schedule.warnings)) - self.assertEqual("warning 1", new_schedule.warnings[0]) - self.assertEqual("warning 2", new_schedule.warnings[1]) - - def test_create_monthly(self) -> None: - with open(CREATE_MONTHLY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - monthly_interval = TSC.MonthlyInterval(time(7), 12) - new_schedule = TSC.ScheduleItem( - "monthly-schedule-1", - 20, - TSC.ScheduleItem.Type.Extract, - TSC.ScheduleItem.ExecutionOrder.Serial, - monthly_interval, - ) - new_schedule = self.server.schedules.create(new_schedule) - - self.assertEqual("e06a7c75-5576-4f68-882d-8909d0219326", new_schedule.id) - self.assertEqual("monthly-schedule-1", new_schedule.name) - self.assertEqual("Active", new_schedule.state) - self.assertEqual(20, new_schedule.priority) - self.assertEqual("2016-09-15T21:16:56Z", format_datetime(new_schedule.created_at)) - self.assertEqual("2016-09-15T21:16:56Z", format_datetime(new_schedule.updated_at)) - self.assertEqual(TSC.ScheduleItem.Type.Extract, new_schedule.schedule_type) - self.assertEqual("2016-10-12T14:00:00Z", format_datetime(new_schedule.next_run_at)) - self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Serial, new_schedule.execution_order) - self.assertEqual(time(7), new_schedule.interval_item.start_time) - self.assertEqual(("12",), new_schedule.interval_item.interval) # type: ignore[union-attr] - - def test_update(self) -> None: - with open(UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/7bea1766-1543-4052-9753-9d224bc069b5", text=response_xml) - new_interval = TSC.WeeklyInterval(time(7), TSC.IntervalItem.Day.Monday, TSC.IntervalItem.Day.Friday) - single_schedule = TSC.ScheduleItem( - "weekly-schedule-1", - 90, - TSC.ScheduleItem.Type.Extract, - TSC.ScheduleItem.ExecutionOrder.Parallel, - new_interval, - ) - single_schedule._id = "7bea1766-1543-4052-9753-9d224bc069b5" - single_schedule.state = TSC.ScheduleItem.State.Suspended - single_schedule = self.server.schedules.update(single_schedule) - - self.assertEqual("7bea1766-1543-4052-9753-9d224bc069b5", single_schedule.id) - self.assertEqual("weekly-schedule-1", single_schedule.name) - self.assertEqual(90, single_schedule.priority) - self.assertEqual("2016-09-15T23:50:02Z", format_datetime(single_schedule.updated_at)) - self.assertEqual(TSC.ScheduleItem.Type.Extract, single_schedule.schedule_type) - self.assertEqual("2016-09-16T14:00:00Z", format_datetime(single_schedule.next_run_at)) - self.assertEqual(TSC.ScheduleItem.ExecutionOrder.Parallel, single_schedule.execution_order) - self.assertEqual(time(7), single_schedule.interval_item.start_time) - self.assertEqual(("Monday", "Friday"), single_schedule.interval_item.interval) # type: ignore[union-attr] - self.assertEqual(TSC.ScheduleItem.State.Suspended, single_schedule.state) - - # Tests calling update with a schedule item returned from the server - def test_update_after_get(self) -> None: - with open(GET_XML, "rb") as f: - get_response_xml = f.read().decode("utf-8") - with open(UPDATE_XML, "rb") as f: - update_response_xml = f.read().decode("utf-8") - - # Get a schedule - with requests_mock.mock() as m: - m.get(self.baseurl, text=get_response_xml) - all_schedules, pagination_item = self.server.schedules.get() - schedule_item = all_schedules[0] - self.assertEqual(TSC.ScheduleItem.State.Active, schedule_item.state) - self.assertEqual("Weekday early mornings", schedule_item.name) - - # Update the schedule - with requests_mock.mock() as m: - m.put(self.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", text=update_response_xml) - schedule_item.state = TSC.ScheduleItem.State.Suspended - schedule_item.name = "newName" - schedule_item = self.server.schedules.update(schedule_item) - - self.assertEqual(TSC.ScheduleItem.State.Suspended, schedule_item.state) - self.assertEqual("weekly-schedule-1", schedule_item.name) - - def test_add_workbook(self) -> None: - self.server.version = "2.8" - baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" - - with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: - workbook_response = f.read().decode("utf-8") - with open(ADD_WORKBOOK_TO_SCHEDULE, "rb") as f: - add_workbook_response = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.workbooks.baseurl + "/bar", text=workbook_response) - m.put(baseurl + "/foo/workbooks", text=add_workbook_response) - workbook = self.server.workbooks.get_by_id("bar") - result = self.server.schedules.add_to_schedule("foo", workbook=workbook) - self.assertEqual(0, len(result), "Added properly") - - def test_add_workbook_with_warnings(self) -> None: - self.server.version = "2.8" - baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" - - with open(WORKBOOK_GET_BY_ID_XML, "rb") as f: - workbook_response = f.read().decode("utf-8") - with open(ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS, "rb") as f: - add_workbook_response = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.workbooks.baseurl + "/bar", text=workbook_response) - m.put(baseurl + "/foo/workbooks", text=add_workbook_response) - workbook = self.server.workbooks.get_by_id("bar") - result = self.server.schedules.add_to_schedule("foo", workbook=workbook) - self.assertEqual(1, len(result), "Not added properly") - self.assertEqual(2, len(result[0].warnings)) - - def test_add_datasource(self) -> None: - self.server.version = "2.8" - baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" - - with open(DATASOURCE_GET_BY_ID_XML, "rb") as f: - datasource_response = f.read().decode("utf-8") - with open(ADD_DATASOURCE_TO_SCHEDULE, "rb") as f: - add_datasource_response = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.datasources.baseurl + "/bar", text=datasource_response) - m.put(baseurl + "/foo/datasources", text=add_datasource_response) - datasource = self.server.datasources.get_by_id("bar") - result = self.server.schedules.add_to_schedule("foo", datasource=datasource) - self.assertEqual(0, len(result), "Added properly") - - def test_add_flow(self) -> None: - self.server.version = "3.3" - baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules" - - with open(FLOW_GET_BY_ID_XML, "rb") as f: - flow_response = f.read().decode("utf-8") - with open(ADD_FLOW_TO_SCHEDULE, "rb") as f: - add_flow_response = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.flows.baseurl + "/bar", text=flow_response) - m.put(baseurl + "/foo/flows", text=flow_response) - flow = self.server.flows.get_by_id("bar") - result = self.server.schedules.add_to_schedule("foo", flow=flow) - self.assertEqual(0, len(result), "Added properly") - - def test_get_extract_refresh_tasks(self) -> None: - self.server.version = "2.3" - - with open(GET_EXTRACT_TASKS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/schedules/{schedule_id}/extracts" - m.get(baseurl, text=response_xml) - - extracts = self.server.schedules.get_extract_refresh_tasks(schedule_id) - - self.assertIsNotNone(extracts) - self.assertIsInstance(extracts[0], list) - self.assertEqual(2, len(extracts[0])) - self.assertEqual("task1", extracts[0][0].id) +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "schedule_get.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "schedule_get_by_id.xml" +GET_HOURLY_ID_XML = TEST_ASSET_DIR / "schedule_get_hourly_id.xml" +GET_DAILY_ID_XML = TEST_ASSET_DIR / "schedule_get_daily_id.xml" +GET_MONTHLY_ID_XML = TEST_ASSET_DIR / "schedule_get_monthly_id.xml" +GET_MONTHLY_ID_2_XML = TEST_ASSET_DIR / "schedule_get_monthly_id_2.xml" +GET_CUSTOMIZED_MONTHLY_ID_XML = TEST_ASSET_DIR / "schedule_get_customized_monthly_id.xml" +GET_EMPTY_XML = TEST_ASSET_DIR / "schedule_get_empty.xml" +CREATE_HOURLY_XML = TEST_ASSET_DIR / "schedule_create_hourly.xml" +CREATE_DAILY_XML = TEST_ASSET_DIR / "schedule_create_daily.xml" +CREATE_WEEKLY_XML = TEST_ASSET_DIR / "schedule_create_weekly.xml" +CREATE_MONTHLY_XML = TEST_ASSET_DIR / "schedule_create_monthly.xml" +UPDATE_XML = TEST_ASSET_DIR / "schedule_update.xml" +ADD_WORKBOOK_TO_SCHEDULE = TEST_ASSET_DIR / "schedule_add_workbook.xml" +ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS = TEST_ASSET_DIR / "schedule_add_workbook_with_warnings.xml" +ADD_DATASOURCE_TO_SCHEDULE = TEST_ASSET_DIR / "schedule_add_datasource.xml" +ADD_FLOW_TO_SCHEDULE = TEST_ASSET_DIR / "schedule_add_flow.xml" +GET_EXTRACT_TASKS_XML = TEST_ASSET_DIR / "schedule_get_extract_refresh_tasks.xml" +BATCH_UPDATE_STATE = TEST_ASSET_DIR / "schedule_batch_update_state.xml" + +WORKBOOK_GET_BY_ID_XML = TEST_ASSET_DIR / "workbook_get_by_id.xml" +DATASOURCE_GET_BY_ID_XML = TEST_ASSET_DIR / "datasource_get_by_id.xml" +FLOW_GET_BY_ID_XML = TEST_ASSET_DIR / "flow_get_by_id.xml" + + +@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_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.schedules.baseurl, text=response_xml) + all_schedules, pagination_item = server.schedules.get() + + extract = all_schedules[0] + subscription = all_schedules[1] + flow = all_schedules[2] + system = all_schedules[3] + + assert 2 == pagination_item.total_available + assert "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" == extract.id + assert "Weekday early mornings" == extract.name + assert "Active" == extract.state + assert 50 == extract.priority + assert "2016-07-06T20:19:00Z" == format_datetime(extract.created_at) + assert "2016-09-13T11:00:32Z" == format_datetime(extract.updated_at) + assert "Extract" == extract.schedule_type + assert "2016-09-14T11:00:00Z" == format_datetime(extract.next_run_at) + + assert "bcb79d07-6e47-472f-8a65-d7f51f40c36c" == subscription.id + assert "Saturday night" == subscription.name + assert "Active" == subscription.state + assert 80 == subscription.priority + assert "2016-07-07T20:19:00Z" == format_datetime(subscription.created_at) + assert "2016-09-12T16:39:38Z" == format_datetime(subscription.updated_at) + assert "Subscription" == subscription.schedule_type + assert "2016-09-18T06:00:00Z" == format_datetime(subscription.next_run_at) + + assert "f456e8f2-aeb2-4a8e-b823-00b6f08640f0" == flow.id + assert "First of the month 1:00AM" == flow.name + assert "Active" == flow.state + assert 50 == flow.priority + assert "2019-02-19T18:52:19Z" == format_datetime(flow.created_at) + assert "2019-02-19T18:55:51Z" == format_datetime(flow.updated_at) + assert "Flow" == flow.schedule_type + assert "2019-03-01T09:00:00Z" == format_datetime(flow.next_run_at) + + assert "3cfa4713-ce7c-4fa7-aa2e-f752bfc8dd04" == system.id + assert "First of the month 2:00AM" == system.name + assert "Active" == system.state + assert 30 == system.priority + assert "2019-02-19T18:52:19Z" == format_datetime(system.created_at) + assert "2019-02-19T18:55:51Z" == format_datetime(system.updated_at) + assert "System" == system.schedule_type + assert "2019-03-01T09:00:00Z" == format_datetime(system.next_run_at) + + +def test_get_empty(server: TSC.Server) -> None: + response_xml = GET_EMPTY_XML.read_text() + with requests_mock.mock() as m: + m.get(server.schedules.baseurl, text=response_xml) + all_schedules, pagination_item = server.schedules.get() + + assert 0 == pagination_item.total_available + assert [] == all_schedules + + +def test_get_by_id(server: TSC.Server) -> None: + server.version = "3.8" + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + baseurl = f"{server.baseurl}/schedules/{schedule_id}" + m.get(baseurl, text=response_xml) + schedule = server.schedules.get_by_id(schedule_id) + assert schedule is not None + assert schedule_id == schedule.id + assert "Weekday early mornings" == schedule.name + assert "Active" == schedule.state + + +def test_get_hourly_by_id(server: TSC.Server) -> None: + server.version = "3.8" + response_xml = GET_HOURLY_ID_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + baseurl = f"{server.baseurl}/schedules/{schedule_id}" + m.get(baseurl, text=response_xml) + schedule = server.schedules.get_by_id(schedule_id) + assert schedule is not None + assert schedule_id == schedule.id + assert "Hourly schedule" == schedule.name + assert "Active" == schedule.state + assert ("Monday", 0.5) == schedule.interval_item.interval + + +def test_get_daily_by_id(server: TSC.Server) -> None: + server.version = "3.8" + response_xml = GET_DAILY_ID_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + baseurl = f"{server.baseurl}/schedules/{schedule_id}" + m.get(baseurl, text=response_xml) + schedule = server.schedules.get_by_id(schedule_id) + assert schedule is not None + assert schedule_id == schedule.id + assert "Daily schedule" == schedule.name + assert "Active" == schedule.state + assert ("Monday", 2.0) == schedule.interval_item.interval + + +def test_get_monthly_by_id(server: TSC.Server) -> None: + server.version = "3.8" + response_xml = GET_MONTHLY_ID_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + baseurl = f"{server.baseurl}/schedules/{schedule_id}" + m.get(baseurl, text=response_xml) + schedule = server.schedules.get_by_id(schedule_id) + assert schedule is not None + assert schedule_id == schedule.id + assert "Monthly multiple days" == schedule.name + assert "Active" == schedule.state + assert ("1", "2") == schedule.interval_item.interval + + +def test_get_monthly_by_id_2(server: TSC.Server) -> None: + server.version = "3.15" + response_xml = GET_MONTHLY_ID_2_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "8c5caf33-6223-4724-83c3-ccdc1e730a07" + baseurl = f"{server.baseurl}/schedules/{schedule_id}" + m.get(baseurl, text=response_xml) + schedule = server.schedules.get_by_id(schedule_id) + assert schedule is not None + assert schedule_id == schedule.id + assert "Monthly First Monday!" == schedule.name + assert "Active" == schedule.state + assert ("Monday", "First") == schedule.interval_item.interval + + +def test_get_customized_monthly_by_id(server: TSC.Server) -> None: + server.version = "3.15" + response_xml = GET_CUSTOMIZED_MONTHLY_ID_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "f048d794-90dc-40b0-bfad-2ca78e437369" + baseurl = f"{server.baseurl}/schedules/{schedule_id}" + m.get(baseurl, text=response_xml) + schedule = server.schedules.get_by_id(schedule_id) + assert schedule is not None + assert schedule_id == schedule.id + assert "Monthly customized" == schedule.name + assert "Active" == schedule.state + assert ("Customized Monthly",) == schedule.interval_item.interval + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.schedules.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", status_code=204) + server.schedules.delete("c9cff7f9-309c-4361-99ff-d4ba8c9f5467") + + +def test_create_hourly(server: TSC.Server) -> None: + response_xml = CREATE_HOURLY_XML.read_text() + with requests_mock.mock() as m: + m.post(server.schedules.baseurl, text=response_xml) + hourly_interval = TSC.HourlyInterval(start_time=time(2, 30), end_time=time(23, 0), interval_value=2) + new_schedule = TSC.ScheduleItem( + "hourly-schedule-1", + 50, + TSC.ScheduleItem.Type.Extract, + TSC.ScheduleItem.ExecutionOrder.Parallel, + hourly_interval, + ) + new_schedule = server.schedules.create(new_schedule) + + assert "5f42be25-8a43-47ba-971a-63f2d4e7029c" == new_schedule.id + assert "hourly-schedule-1" == new_schedule.name + assert "Active" == new_schedule.state + assert 50 == new_schedule.priority + assert "2016-09-15T20:47:33Z" == format_datetime(new_schedule.created_at) + assert "2016-09-15T20:47:33Z" == format_datetime(new_schedule.updated_at) + assert TSC.ScheduleItem.Type.Extract == new_schedule.schedule_type + assert "2016-09-16T01:30:00Z" == format_datetime(new_schedule.next_run_at) + assert TSC.ScheduleItem.ExecutionOrder.Parallel == new_schedule.execution_order + assert time(2, 30) == new_schedule.interval_item.start_time + assert time(23) == new_schedule.interval_item.end_time # type: ignore[union-attr] + assert ("8",) == new_schedule.interval_item.interval # type: ignore[union-attr] + + +def test_create_daily(server: TSC.Server) -> None: + response_xml = CREATE_DAILY_XML.read_text() + with requests_mock.mock() as m: + m.post(server.schedules.baseurl, text=response_xml) + daily_interval = TSC.DailyInterval(time(4, 50)) + new_schedule = TSC.ScheduleItem( + "daily-schedule-1", + 90, + TSC.ScheduleItem.Type.Subscription, + TSC.ScheduleItem.ExecutionOrder.Serial, + daily_interval, + ) + new_schedule = server.schedules.create(new_schedule) + + assert "907cae38-72fd-417c-892a-95540c4664cd" == new_schedule.id + assert "daily-schedule-1" == new_schedule.name + assert "Active" == new_schedule.state + assert 90 == new_schedule.priority + assert "2016-09-15T21:01:09Z" == format_datetime(new_schedule.created_at) + assert "2016-09-15T21:01:09Z" == format_datetime(new_schedule.updated_at) + assert TSC.ScheduleItem.Type.Subscription == new_schedule.schedule_type + assert "2016-09-16T11:45:00Z" == format_datetime(new_schedule.next_run_at) + assert TSC.ScheduleItem.ExecutionOrder.Serial == new_schedule.execution_order + assert time(4, 45) == new_schedule.interval_item.start_time + + +def test_create_weekly(server: TSC.Server) -> None: + response_xml = CREATE_WEEKLY_XML.read_text() + with requests_mock.mock() as m: + m.post(server.schedules.baseurl, text=response_xml) + weekly_interval = TSC.WeeklyInterval( + time(9, 15), TSC.IntervalItem.Day.Monday, TSC.IntervalItem.Day.Wednesday, TSC.IntervalItem.Day.Friday + ) + new_schedule = TSC.ScheduleItem( + "weekly-schedule-1", + 80, + TSC.ScheduleItem.Type.Extract, + TSC.ScheduleItem.ExecutionOrder.Parallel, + weekly_interval, + ) + new_schedule = server.schedules.create(new_schedule) + + assert "1adff386-6be0-4958-9f81-a35e676932bf" == new_schedule.id + assert "weekly-schedule-1" == new_schedule.name + assert "Active" == new_schedule.state + assert 80 == new_schedule.priority + assert "2016-09-15T21:12:50Z" == format_datetime(new_schedule.created_at) + assert "2016-09-15T21:12:50Z" == format_datetime(new_schedule.updated_at) + assert TSC.ScheduleItem.Type.Extract == new_schedule.schedule_type + assert "2016-09-16T16:15:00Z" == format_datetime(new_schedule.next_run_at) + assert TSC.ScheduleItem.ExecutionOrder.Parallel == new_schedule.execution_order + assert time(9, 15) == new_schedule.interval_item.start_time + assert ("Monday", "Wednesday", "Friday") == new_schedule.interval_item.interval + assert 2 == len(new_schedule.warnings) + assert "warning 1" == new_schedule.warnings[0] + assert "warning 2" == new_schedule.warnings[1] + + +def test_create_monthly(server: TSC.Server) -> None: + response_xml = CREATE_MONTHLY_XML.read_text() + with requests_mock.mock() as m: + m.post(server.schedules.baseurl, text=response_xml) + monthly_interval = TSC.MonthlyInterval(time(7), 12) + new_schedule = TSC.ScheduleItem( + "monthly-schedule-1", + 20, + TSC.ScheduleItem.Type.Extract, + TSC.ScheduleItem.ExecutionOrder.Serial, + monthly_interval, + ) + new_schedule = server.schedules.create(new_schedule) + + assert "e06a7c75-5576-4f68-882d-8909d0219326" == new_schedule.id + assert "monthly-schedule-1" == new_schedule.name + assert "Active" == new_schedule.state + assert 20 == new_schedule.priority + assert "2016-09-15T21:16:56Z" == format_datetime(new_schedule.created_at) + assert "2016-09-15T21:16:56Z" == format_datetime(new_schedule.updated_at) + assert TSC.ScheduleItem.Type.Extract == new_schedule.schedule_type + assert "2016-10-12T14:00:00Z" == format_datetime(new_schedule.next_run_at) + assert TSC.ScheduleItem.ExecutionOrder.Serial == new_schedule.execution_order + assert time(7) == new_schedule.interval_item.start_time + assert ("12",) == new_schedule.interval_item.interval # type: ignore[union-attr] + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.schedules.baseurl + "/7bea1766-1543-4052-9753-9d224bc069b5", text=response_xml) + new_interval = TSC.WeeklyInterval(time(7), TSC.IntervalItem.Day.Monday, TSC.IntervalItem.Day.Friday) + single_schedule = TSC.ScheduleItem( + "weekly-schedule-1", + 90, + TSC.ScheduleItem.Type.Extract, + TSC.ScheduleItem.ExecutionOrder.Parallel, + new_interval, + ) + single_schedule._id = "7bea1766-1543-4052-9753-9d224bc069b5" + single_schedule.state = TSC.ScheduleItem.State.Suspended + single_schedule = server.schedules.update(single_schedule) + + assert "7bea1766-1543-4052-9753-9d224bc069b5" == single_schedule.id + assert "weekly-schedule-1" == single_schedule.name + assert 90 == single_schedule.priority + assert "2016-09-15T23:50:02Z" == format_datetime(single_schedule.updated_at) + assert TSC.ScheduleItem.Type.Extract == single_schedule.schedule_type + assert "2016-09-16T14:00:00Z" == format_datetime(single_schedule.next_run_at) + assert TSC.ScheduleItem.ExecutionOrder.Parallel == single_schedule.execution_order + assert time(7) == single_schedule.interval_item.start_time + assert ("Monday", "Friday") == single_schedule.interval_item.interval # type: ignore[union-attr] + assert TSC.ScheduleItem.State.Suspended == single_schedule.state + + +# Tests calling update with a schedule item returned from the server +def test_update_after_get(server: TSC.Server) -> None: + get_response_xml = GET_XML.read_text() + update_response_xml = UPDATE_XML.read_text() + + # Get a schedule + with requests_mock.mock() as m: + m.get(server.schedules.baseurl, text=get_response_xml) + all_schedules, pagination_item = server.schedules.get() + schedule_item = all_schedules[0] + assert TSC.ScheduleItem.State.Active == schedule_item.state + assert "Weekday early mornings" == schedule_item.name + + # Update the schedule + with requests_mock.mock() as m: + m.put(server.schedules.baseurl + "/c9cff7f9-309c-4361-99ff-d4ba8c9f5467", text=update_response_xml) + schedule_item.state = TSC.ScheduleItem.State.Suspended + schedule_item.name = "newName" + schedule_item = server.schedules.update(schedule_item) + + assert TSC.ScheduleItem.State.Suspended == schedule_item.state + assert "weekly-schedule-1" == schedule_item.name + + +def test_add_workbook(server: TSC.Server) -> None: + server.version = "2.8" + baseurl = f"{server.baseurl}/sites/{server.site_id}/schedules" + + workbook_response = WORKBOOK_GET_BY_ID_XML.read_text() + add_workbook_response = ADD_WORKBOOK_TO_SCHEDULE.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/bar", text=workbook_response) + m.put(baseurl + "/foo/workbooks", text=add_workbook_response) + workbook = server.workbooks.get_by_id("bar") + result = server.schedules.add_to_schedule("foo", workbook=workbook) + assert 0 == len(result), "Added properly" + + +def test_add_workbook_with_warnings(server: TSC.Server) -> None: + server.version = "2.8" + baseurl = f"{server.baseurl}/sites/{server.site_id}/schedules" + + workbook_response = WORKBOOK_GET_BY_ID_XML.read_text() + add_workbook_response = ADD_WORKBOOK_TO_SCHEDULE_WITH_WARNINGS.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/bar", text=workbook_response) + m.put(baseurl + "/foo/workbooks", text=add_workbook_response) + workbook = server.workbooks.get_by_id("bar") + result = server.schedules.add_to_schedule("foo", workbook=workbook) + assert 1 == len(result), "Not added properly" + assert 2 == len(result[0].warnings) + + +def test_add_datasource(server: TSC.Server) -> None: + server.version = "2.8" + baseurl = f"{server.baseurl}/sites/{server.site_id}/schedules" + + datasource_response = DATASOURCE_GET_BY_ID_XML.read_text() + add_datasource_response = ADD_DATASOURCE_TO_SCHEDULE.read_text() + with requests_mock.mock() as m: + m.get(server.datasources.baseurl + "/bar", text=datasource_response) + m.put(baseurl + "/foo/datasources", text=add_datasource_response) + datasource = server.datasources.get_by_id("bar") + result = server.schedules.add_to_schedule("foo", datasource=datasource) + assert 0 == len(result), "Added properly" + + +def test_add_flow(server: TSC.Server) -> None: + server.version = "3.3" + baseurl = f"{server.baseurl}/sites/{server.site_id}/schedules" + + flow_response = FLOW_GET_BY_ID_XML.read_text() + add_flow_response = ADD_FLOW_TO_SCHEDULE.read_text() + with requests_mock.mock() as m: + m.get(server.flows.baseurl + "/bar", text=flow_response) + m.put(baseurl + "/foo/flows", text=flow_response) + flow = server.flows.get_by_id("bar") + result = server.schedules.add_to_schedule("foo", flow=flow) + assert 0 == len(result), "Added properly" + + +def test_get_extract_refresh_tasks(server: TSC.Server) -> None: + server.version = "2.3" + + response_xml = GET_EXTRACT_TASKS_XML.read_text() + with requests_mock.mock() as m: + schedule_id = "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + baseurl = f"{server.baseurl}/sites/{server.site_id}/schedules/{schedule_id}/extracts" + m.get(baseurl, text=response_xml) + + extracts = server.schedules.get_extract_refresh_tasks(schedule_id) + + assert extracts is not None + assert isinstance(extracts[0], list) + assert 2 == len(extracts[0]) + assert "task1" == extracts[0][0].id + + +def test_batch_update_state_items(server: TSC.Server) -> None: + server.version = "3.27" + hourly_interval = TSC.HourlyInterval(start_time=time(2, 30), end_time=time(23, 0), interval_value=2) + args = ("hourly", 50, TSC.ScheduleItem.Type.Extract, TSC.ScheduleItem.ExecutionOrder.Parallel, hourly_interval) + new_schedules = [TSC.ScheduleItem(*args), TSC.ScheduleItem(*args), TSC.ScheduleItem(*args)] + new_schedules[0]._id = "593d2ebf-0d18-4deb-9d21-b113a4902583" + new_schedules[1]._id = "cecbb71e-def0-4030-8068-5ae50f51db1c" + new_schedules[2]._id = "f39a6e7d-405e-4c07-8c18-95845f9da80e" + + state = "active" + with requests_mock.mock() as m: + m.put(f"{server.schedules.baseurl}?state={state}", text=BATCH_UPDATE_STATE.read_text()) + resp = server.schedules.batch_update_state(new_schedules, state) + + assert len(resp) == 3 + for sch, r in zip(new_schedules, resp): + assert sch.id == r + + +def test_batch_update_state_str(server: TSC.Server) -> None: + server.version = "3.27" + new_schedules = [ + "593d2ebf-0d18-4deb-9d21-b113a4902583", + "cecbb71e-def0-4030-8068-5ae50f51db1c", + "f39a6e7d-405e-4c07-8c18-95845f9da80e", + ] + + state = "suspended" + with requests_mock.mock() as m: + m.put(f"{server.schedules.baseurl}?state={state}", text=BATCH_UPDATE_STATE.read_text()) + resp = server.schedules.batch_update_state(new_schedules, state) + + assert len(resp) == 3 + for sch, r in zip(new_schedules, resp): + assert sch == r + + +def test_batch_update_state_all(server: TSC.Server) -> None: + server.version = "3.27" + new_schedules = [ + "593d2ebf-0d18-4deb-9d21-b113a4902583", + "cecbb71e-def0-4030-8068-5ae50f51db1c", + "f39a6e7d-405e-4c07-8c18-95845f9da80e", + ] + + state = "suspended" + with requests_mock.mock() as m: + m.put(f"{server.schedules.baseurl}?state={state}&updateAll=true", text=BATCH_UPDATE_STATE.read_text()) + _ = server.schedules.batch_update_state(new_schedules, state, True) + + history = m.request_history[0] + + assert history.text == "" diff --git a/test/test_server_info.py b/test/test_server_info.py index fa1472c9a..bc1a1bcb3 100644 --- a/test/test_server_info.py +++ b/test/test_server_info.py @@ -1,75 +1,81 @@ -import os.path -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.server.endpoint.exceptions import NonXMLResponseError -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -SERVER_INFO_GET_XML = os.path.join(TEST_ASSET_DIR, "server_info_get.xml") -SERVER_INFO_25_XML = os.path.join(TEST_ASSET_DIR, "server_info_25.xml") -SERVER_INFO_404 = os.path.join(TEST_ASSET_DIR, "server_info_404.xml") -SERVER_INFO_AUTH_INFO_XML = os.path.join(TEST_ASSET_DIR, "server_info_auth_info.xml") -SERVER_INFO_WRONG_SITE = os.path.join(TEST_ASSET_DIR, "server_info_wrong_site.html") - - -class ServerInfoTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.baseurl = self.server.server_info.baseurl - self.server.version = "2.4" - - def test_server_info_get(self): - with open(SERVER_INFO_GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.server_info.baseurl, text=response_xml) - actual = self.server.server_info.get() - - self.assertEqual("10.1.0", actual.product_version) - self.assertEqual("10100.16.1024.2100", actual.build_number) - self.assertEqual("3.10", actual.rest_api_version) - - def test_server_info_use_highest_version_downgrades(self): - with open(SERVER_INFO_AUTH_INFO_XML, "rb") as f: - # This is the auth.xml endpoint present back to 9.0 Servers - auth_response_xml = f.read().decode("utf-8") - with open(SERVER_INFO_404, "rb") as f: - # 10.1 serverInfo response - si_response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - # Return a 404 for serverInfo so we can pretend this is an old Server - m.get(self.server.server_address + "/api/2.4/serverInfo", text=si_response_xml, status_code=404) - m.get(self.server.server_address + "/auth?format=xml", text=auth_response_xml) - self.server.use_server_version() - # does server-version[9.2] lookup in PRODUCT_TO_REST_VERSION - self.assertEqual(self.server.version, "2.2") - - def test_server_info_use_highest_version_upgrades(self): - with open(SERVER_INFO_GET_XML, "rb") as f: - si_response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.server_address + "/api/2.8/serverInfo", text=si_response_xml) - # Pretend we're old - self.server.version = "2.8" - self.server.use_server_version() - # Did we upgrade to 3.10? - self.assertEqual(self.server.version, "3.10") - - def test_server_use_server_version_flag(self): - with open(SERVER_INFO_25_XML, "rb") as f: - si_response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get("http://test/api/2.4/serverInfo", text=si_response_xml) - server = TSC.Server("http://test", use_server_version=True) - self.assertEqual(server.version, "2.5") - - def test_server_wrong_site(self): - with open(SERVER_INFO_WRONG_SITE, "rb") as f: - response = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.server_info.baseurl, text=response, status_code=404) - with self.assertRaises(NonXMLResponseError): - self.server.server_info.get() +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +SERVER_INFO_GET_XML = TEST_ASSET_DIR / "server_info_get.xml" +SERVER_INFO_25_XML = TEST_ASSET_DIR / "server_info_25.xml" +SERVER_INFO_404 = TEST_ASSET_DIR / "server_info_404.xml" +SERVER_INFO_AUTH_INFO_XML = TEST_ASSET_DIR / "server_info_auth_info.xml" +SERVER_INFO_WRONG_SITE = TEST_ASSET_DIR / "server_info_wrong_site.html" + + +@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" + server.version = "2.4" + + return server + + +def test_server_info_get(server: TSC.Server) -> None: + response_xml = SERVER_INFO_GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.server_info.baseurl, text=response_xml) + actual = server.server_info.get() + + assert actual is not None + assert "10.1.0" == actual.product_version + assert "10100.16.1024.2100" == actual.build_number + assert "3.10" == actual.rest_api_version + + +def test_server_info_use_highest_version_downgrades(server: TSC.Server) -> None: + # This is the auth.xml endpoint present back to 9.0 Servers + auth_response_xml = SERVER_INFO_AUTH_INFO_XML.read_text() + # 10.1 serverInfo response + si_response_xml = SERVER_INFO_404.read_text() + with requests_mock.mock() as m: + # Return a 404 for serverInfo so we can pretend this is an old Server + m.get(server.server_address + "/api/2.4/serverInfo", text=si_response_xml, status_code=404) + m.get(server.server_address + "/auth?format=xml", text=auth_response_xml) + server.use_server_version() + # does server-version[9.2] lookup in PRODUCT_TO_REST_VERSION + assert server.version == "2.2" + + +def test_server_info_use_highest_version_upgrades(server: TSC.Server) -> None: + si_response_xml = SERVER_INFO_GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.server_address + "/api/2.8/serverInfo", text=si_response_xml) + # Pretend we're old + server.version = "2.8" + server.use_server_version() + # Did we upgrade to 3.10? + assert server.version == "3.10" + + +def test_server_use_server_version_flag(server: TSC.Server) -> None: + si_response_xml = SERVER_INFO_25_XML.read_text() + with requests_mock.mock() as m: + m.get("http://test/api/2.4/serverInfo", text=si_response_xml) + server = TSC.Server("http://test", use_server_version=True) + assert server.version == "2.5" + + +def test_server_wrong_site(server: TSC.Server) -> None: + response = SERVER_INFO_WRONG_SITE.read_text() + with requests_mock.mock() as m: + m.get(server.server_info.baseurl, text=response, status_code=404) + with pytest.raises(NonXMLResponseError): + server.server_info.get() diff --git a/test/test_site.py b/test/test_site.py index 243810254..e976bc1d2 100644 --- a/test/test_site.py +++ b/test/test_site.py @@ -1,288 +1,339 @@ -import os.path -import unittest +from itertools import product +from pathlib import Path +from defusedxml import ElementTree as ET import pytest import requests_mock import tableauserverclient as TSC - -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -GET_XML = os.path.join(TEST_ASSET_DIR, "site_get.xml") -GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "site_get_by_id.xml") -GET_BY_NAME_XML = os.path.join(TEST_ASSET_DIR, "site_get_by_name.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "site_update.xml") -CREATE_XML = os.path.join(TEST_ASSET_DIR, "site_create.xml") -SITE_AUTH_CONFIG_XML = os.path.join(TEST_ASSET_DIR, "site_auth_configurations.xml") - - -class SiteTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - self.server.version = "3.10" - - # Fake signin - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server._site_id = "0626857c-1def-4503-a7d8-7907c3ff9d9f" - self.baseurl = self.server.sites.baseurl - - # sites APIs can only be called on the site being logged in to - self.logged_in_site = self.server.site_id - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_sites, pagination_item = self.server.sites.get() - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("dad65087-b08b-4603-af4e-2887b8aafc67", all_sites[0].id) - self.assertEqual("Active", all_sites[0].state) - self.assertEqual("Default", all_sites[0].name) - self.assertEqual("ContentOnly", all_sites[0].admin_mode) - self.assertEqual(False, all_sites[0].revision_history_enabled) - self.assertEqual(True, all_sites[0].subscribe_others_enabled) - self.assertEqual(25, all_sites[0].revision_limit) - self.assertEqual(None, all_sites[0].num_users) - self.assertEqual(None, all_sites[0].storage) - self.assertEqual(True, all_sites[0].cataloging_enabled) - self.assertEqual(False, all_sites[0].editing_flows_enabled) - self.assertEqual(False, all_sites[0].scheduling_flows_enabled) - self.assertEqual(True, all_sites[0].allow_subscription_attachments) - self.assertEqual("6b7179ba-b82b-4f0f-91ed-812074ac5da6", all_sites[1].id) - self.assertEqual("Active", all_sites[1].state) - self.assertEqual("Samples", all_sites[1].name) - self.assertEqual("ContentOnly", all_sites[1].admin_mode) - self.assertEqual(False, all_sites[1].revision_history_enabled) - self.assertEqual(True, all_sites[1].subscribe_others_enabled) - self.assertEqual(False, all_sites[1].guest_access_enabled) - self.assertEqual(True, all_sites[1].cache_warmup_enabled) - self.assertEqual(True, all_sites[1].commenting_enabled) - self.assertEqual(True, all_sites[1].cache_warmup_enabled) - self.assertEqual(False, all_sites[1].request_access_enabled) - self.assertEqual(True, all_sites[1].run_now_enabled) - self.assertEqual(1, all_sites[1].tier_explorer_capacity) - self.assertEqual(2, all_sites[1].tier_creator_capacity) - self.assertEqual(1, all_sites[1].tier_viewer_capacity) - self.assertEqual(False, all_sites[1].flows_enabled) - self.assertEqual(None, all_sites[1].data_acceleration_mode) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.sites.get) - - def test_get_by_id(self) -> None: - with open(GET_BY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/" + self.logged_in_site, text=response_xml) - single_site = self.server.sites.get_by_id(self.logged_in_site) - - self.assertEqual(self.logged_in_site, single_site.id) - self.assertEqual("Active", single_site.state) - self.assertEqual("Default", single_site.name) - self.assertEqual("ContentOnly", single_site.admin_mode) - self.assertEqual(False, single_site.revision_history_enabled) - self.assertEqual(True, single_site.subscribe_others_enabled) - self.assertEqual(False, single_site.disable_subscriptions) - self.assertEqual(False, single_site.data_alerts_enabled) - self.assertEqual(False, single_site.commenting_mentions_enabled) - self.assertEqual(True, single_site.catalog_obfuscation_enabled) - - def test_get_by_id_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.sites.get_by_id, "") - - def test_get_by_name(self) -> None: - with open(GET_BY_NAME_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/testsite?key=name", text=response_xml) - single_site = self.server.sites.get_by_name("testsite") - - self.assertEqual(self.logged_in_site, single_site.id) - self.assertEqual("Active", single_site.state) - self.assertEqual("testsite", single_site.name) - self.assertEqual("ContentOnly", single_site.admin_mode) - self.assertEqual(False, single_site.revision_history_enabled) - self.assertEqual(True, single_site.subscribe_others_enabled) - self.assertEqual(False, single_site.disable_subscriptions) - - def test_get_by_name_missing_name(self) -> None: - self.assertRaises(ValueError, self.server.sites.get_by_name, "") - - @pytest.mark.filterwarnings("ignore:Tiered license level is set") - @pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed") - def test_update(self) -> None: - with open(UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/" + self.logged_in_site, text=response_xml) - single_site = TSC.SiteItem( - name="Tableau", - content_url="tableau", - admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers, - user_quota=15, - storage_quota=1000, - disable_subscriptions=True, - revision_history_enabled=False, - data_acceleration_mode="disable", - flow_auto_save_enabled=True, - web_extraction_enabled=False, - metrics_content_type_enabled=True, - notify_site_admins_on_throttle=False, - authoring_enabled=True, - custom_subscription_email_enabled=True, - custom_subscription_email="test@test.com", - custom_subscription_footer_enabled=True, - custom_subscription_footer="example_footer", - ask_data_mode="EnabledByDefault", - named_sharing_enabled=False, - mobile_biometrics_enabled=True, - sheet_image_enabled=False, - derived_permissions_enabled=True, - user_visibility_mode="FULL", - use_default_time_zone=False, - time_zone="America/Los_Angeles", - auto_suspend_refresh_enabled=True, - auto_suspend_refresh_inactivity_window=55, - tier_creator_capacity=5, - tier_explorer_capacity=5, - tier_viewer_capacity=5, - ) - single_site._id = self.logged_in_site - self.server.sites.parent_srv = self.server - single_site = self.server.sites.update(single_site) - - self.assertEqual(self.logged_in_site, single_site.id) - self.assertEqual("tableau", single_site.content_url) - self.assertEqual("Suspended", single_site.state) - self.assertEqual("Tableau", single_site.name) - self.assertEqual("ContentAndUsers", single_site.admin_mode) - self.assertEqual(True, single_site.revision_history_enabled) - self.assertEqual(13, single_site.revision_limit) - self.assertEqual(True, single_site.disable_subscriptions) - self.assertEqual(None, single_site.user_quota) - self.assertEqual(5, single_site.tier_creator_capacity) - self.assertEqual(5, single_site.tier_explorer_capacity) - self.assertEqual(5, single_site.tier_viewer_capacity) - self.assertEqual("disable", single_site.data_acceleration_mode) - self.assertEqual(True, single_site.flows_enabled) - self.assertEqual(True, single_site.cataloging_enabled) - self.assertEqual(True, single_site.flow_auto_save_enabled) - self.assertEqual(False, single_site.web_extraction_enabled) - self.assertEqual(True, single_site.metrics_content_type_enabled) - self.assertEqual(False, single_site.notify_site_admins_on_throttle) - self.assertEqual(True, single_site.authoring_enabled) - self.assertEqual(True, single_site.custom_subscription_email_enabled) - self.assertEqual("test@test.com", single_site.custom_subscription_email) - self.assertEqual(True, single_site.custom_subscription_footer_enabled) - self.assertEqual("example_footer", single_site.custom_subscription_footer) - self.assertEqual("EnabledByDefault", single_site.ask_data_mode) - self.assertEqual(False, single_site.named_sharing_enabled) - self.assertEqual(True, single_site.mobile_biometrics_enabled) - self.assertEqual(False, single_site.sheet_image_enabled) - self.assertEqual(True, single_site.derived_permissions_enabled) - self.assertEqual("FULL", single_site.user_visibility_mode) - self.assertEqual(False, single_site.use_default_time_zone) - self.assertEqual("America/Los_Angeles", single_site.time_zone) - self.assertEqual(True, single_site.auto_suspend_refresh_enabled) - self.assertEqual(55, single_site.auto_suspend_refresh_inactivity_window) - - def test_update_missing_id(self) -> None: - single_site = TSC.SiteItem("test", "test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.sites.update, single_site) - - def test_null_site_quota(self) -> None: - test_site = TSC.SiteItem("testname", "testcontenturl", tier_explorer_capacity=1, user_quota=None) - assert test_site.tier_explorer_capacity == 1 - with self.assertRaises(ValueError): - test_site.user_quota = 1 - test_site.tier_explorer_capacity = None +from tableauserverclient.server.request_factory import RequestFactory + +from . import _utils + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "site_get.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "site_get_by_id.xml" +GET_BY_NAME_XML = TEST_ASSET_DIR / "site_get_by_name.xml" +UPDATE_XML = TEST_ASSET_DIR / "site_update.xml" +CREATE_XML = TEST_ASSET_DIR / "site_create.xml" +SITE_AUTH_CONFIG_XML = TEST_ASSET_DIR / "site_auth_configurations.xml" + + +@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 = "0626857c-1def-4503-a7d8-7907c3ff9d9f" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + server.version = "3.10" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.sites.baseurl, text=response_xml) + all_sites, pagination_item = server.sites.get() + + assert 2 == pagination_item.total_available + assert "dad65087-b08b-4603-af4e-2887b8aafc67" == all_sites[0].id + assert "Active" == all_sites[0].state + assert "Default" == all_sites[0].name + assert "ContentOnly" == all_sites[0].admin_mode + assert all_sites[0].revision_history_enabled is False + assert all_sites[0].subscribe_others_enabled is True + assert 25 == all_sites[0].revision_limit + assert None == all_sites[0].num_users + assert None == all_sites[0].storage + assert all_sites[0].cataloging_enabled is True + assert all_sites[0].editing_flows_enabled is False + assert all_sites[0].scheduling_flows_enabled is False + assert all_sites[0].allow_subscription_attachments is True + assert "6b7179ba-b82b-4f0f-91ed-812074ac5da6" == all_sites[1].id + assert "Active" == all_sites[1].state + assert "Samples" == all_sites[1].name + assert "ContentOnly" == all_sites[1].admin_mode + assert all_sites[1].revision_history_enabled is False + assert all_sites[1].subscribe_others_enabled is True + assert all_sites[1].guest_access_enabled is False + assert all_sites[1].cache_warmup_enabled is True + assert all_sites[1].commenting_enabled is True + assert all_sites[1].cache_warmup_enabled is True + assert all_sites[1].request_access_enabled is False + assert all_sites[1].run_now_enabled is True + assert 1 == all_sites[1].tier_explorer_capacity + assert 2 == all_sites[1].tier_creator_capacity + assert 1 == all_sites[1].tier_viewer_capacity + assert all_sites[1].flows_enabled is False + assert None == all_sites[1].data_acceleration_mode + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.sites.get() + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + m.get(server.sites.baseurl + "/" + server.site_id, text=response_xml) + single_site = server.sites.get_by_id(server.site_id) + + assert server.site_id == single_site.id + assert "Active" == single_site.state + assert "Default" == single_site.name + assert "ContentOnly" == single_site.admin_mode + assert single_site.revision_history_enabled is False + assert single_site.subscribe_others_enabled is True + assert single_site.disable_subscriptions is False + assert single_site.data_alerts_enabled is False + assert single_site.commenting_mentions_enabled is False + assert single_site.catalog_obfuscation_enabled is True + + +def test_get_by_id_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.sites.get_by_id("") + + +def test_get_by_name(server: TSC.Server) -> None: + response_xml = GET_BY_NAME_XML.read_text() + with requests_mock.mock() as m: + m.get(server.sites.baseurl + "/testsite?key=name", text=response_xml) + single_site = server.sites.get_by_name("testsite") + + assert server.site_id == single_site.id + assert "Active" == single_site.state + assert "testsite" == single_site.name + assert "ContentOnly" == single_site.admin_mode + assert single_site.revision_history_enabled is False + assert single_site.subscribe_others_enabled is True + assert single_site.disable_subscriptions is False + + +def test_get_by_name_missing_name(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.sites.get_by_name("") + + +@pytest.mark.filterwarnings("ignore:Tiered license level is set") +@pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed") +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.sites.baseurl + "/" + server.site_id, text=response_xml) + single_site = TSC.SiteItem( + name="Tableau", + content_url="tableau", + admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers, + user_quota=15, + storage_quota=1000, + disable_subscriptions=True, + revision_history_enabled=False, + data_acceleration_mode="disable", + flow_auto_save_enabled=True, + web_extraction_enabled=False, + metrics_content_type_enabled=True, + notify_site_admins_on_throttle=False, + authoring_enabled=True, + custom_subscription_email_enabled=True, + custom_subscription_email="test@test.com", + custom_subscription_footer_enabled=True, + custom_subscription_footer="example_footer", + ask_data_mode="EnabledByDefault", + named_sharing_enabled=False, + mobile_biometrics_enabled=True, + sheet_image_enabled=False, + derived_permissions_enabled=True, + user_visibility_mode="FULL", + use_default_time_zone=False, + time_zone="America/Los_Angeles", + auto_suspend_refresh_enabled=True, + auto_suspend_refresh_inactivity_window=55, + tier_creator_capacity=5, + tier_explorer_capacity=5, + tier_viewer_capacity=5, + ) + single_site._id = server.site_id + server.sites.parent_srv = server + single_site = server.sites.update(single_site) + + assert server.site_id == single_site.id + assert "tableau" == single_site.content_url + assert "Suspended" == single_site.state + assert "Tableau" == single_site.name + assert "ContentAndUsers" == single_site.admin_mode + assert single_site.revision_history_enabled is True + assert 13 == single_site.revision_limit + assert single_site.disable_subscriptions is True + assert None == single_site.user_quota + assert 5 == single_site.tier_creator_capacity + assert 5 == single_site.tier_explorer_capacity + assert 5 == single_site.tier_viewer_capacity + assert "disable" == single_site.data_acceleration_mode + assert single_site.flows_enabled is True + assert single_site.cataloging_enabled is True + assert single_site.flow_auto_save_enabled is True + assert single_site.web_extraction_enabled is False + assert single_site.metrics_content_type_enabled is True + assert single_site.notify_site_admins_on_throttle is False + assert single_site.authoring_enabled is True + assert single_site.custom_subscription_email_enabled is True + assert "test@test.com" == single_site.custom_subscription_email + assert single_site.custom_subscription_footer_enabled is True + assert "example_footer" == single_site.custom_subscription_footer + assert "EnabledByDefault" == single_site.ask_data_mode + assert single_site.named_sharing_enabled is False + assert single_site.mobile_biometrics_enabled is True + assert single_site.sheet_image_enabled is False + assert single_site.derived_permissions_enabled is True + assert "FULL" == single_site.user_visibility_mode + assert single_site.use_default_time_zone is False + assert "America/Los_Angeles" == single_site.time_zone + assert single_site.auto_suspend_refresh_enabled is True + assert 55 == single_site.auto_suspend_refresh_inactivity_window + + +def test_update_missing_id(server: TSC.Server) -> None: + single_site = TSC.SiteItem("test", "test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.sites.update(single_site) + + +def test_null_site_quota(server: TSC.Server) -> None: + test_site = TSC.SiteItem("testname", "testcontenturl", tier_explorer_capacity=1, user_quota=None) + assert test_site.tier_explorer_capacity == 1 + with pytest.raises(ValueError): test_site.user_quota = 1 + test_site.tier_explorer_capacity = None + test_site.user_quota = 1 + - def test_replace_license_tiers_with_user_quota(self) -> None: - test_site = TSC.SiteItem("testname", "testcontenturl", tier_explorer_capacity=1, user_quota=None) - assert test_site.tier_explorer_capacity == 1 - with self.assertRaises(ValueError): - test_site.user_quota = 1 - test_site.replace_license_tiers_with_user_quota(1) - self.assertEqual(1, test_site.user_quota) - self.assertIsNone(test_site.tier_explorer_capacity) - - @pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed") - def test_create(self) -> None: - with open(CREATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - new_site = TSC.SiteItem( - name="Tableau", - content_url="tableau", - admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers, - user_quota=15, - storage_quota=1000, - disable_subscriptions=True, - ) - new_site = self.server.sites.create(new_site) - - new_site._tier_viewer_capacity = None - new_site._tier_creator_capacity = None - new_site._tier_explorer_capacity = None - self.assertEqual("0626857c-1def-4503-a7d8-7907c3ff9d9f", new_site.id) - self.assertEqual("tableau", new_site.content_url) - self.assertEqual("Tableau", new_site.name) - self.assertEqual("Active", new_site.state) - self.assertEqual("ContentAndUsers", new_site.admin_mode) - self.assertEqual(False, new_site.revision_history_enabled) - self.assertEqual(True, new_site.subscribe_others_enabled) - self.assertEqual(True, new_site.disable_subscriptions) - self.assertEqual(15, new_site.user_quota) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f", status_code=204) - self.server.sites.delete("0626857c-1def-4503-a7d8-7907c3ff9d9f") - - def test_delete_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.sites.delete, "") - - def test_encrypt(self) -> None: - with requests_mock.mock() as m: - m.post(self.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/encrypt-extracts", status_code=200) - self.server.sites.encrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f") - - def test_recrypt(self) -> None: - with requests_mock.mock() as m: - m.post(self.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/reencrypt-extracts", status_code=200) - self.server.sites.re_encrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f") - - def test_decrypt(self) -> None: - with requests_mock.mock() as m: - m.post(self.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/decrypt-extracts", status_code=200) - self.server.sites.decrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f") - - def test_list_auth_configurations(self) -> None: - self.server.version = "3.24" - self.baseurl = self.server.sites.baseurl - with open(SITE_AUTH_CONFIG_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - - assert self.baseurl == self.server.sites.baseurl - - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{self.server.site_id}/site-auth-configurations", status_code=200, text=response_xml) - configs = self.server.sites.list_auth_configurations() - - assert len(configs) == 2, "Expected 2 auth configurations" - - assert configs[0].auth_setting == "OIDC" - assert configs[0].enabled - assert configs[0].idp_configuration_id == "00000000-0000-0000-0000-000000000000" - assert configs[0].idp_configuration_name == "Initial Salesforce" - assert configs[0].known_provider_alias == "Salesforce" - assert configs[1].auth_setting == "SAML" - assert configs[1].enabled - assert configs[1].idp_configuration_id == "11111111-1111-1111-1111-111111111111" - assert configs[1].idp_configuration_name == "Initial SAML" - assert configs[1].known_provider_alias is None +def test_replace_license_tiers_with_user_quota(server: TSC.Server) -> None: + test_site = TSC.SiteItem("testname", "testcontenturl", tier_explorer_capacity=1, user_quota=None) + assert test_site.tier_explorer_capacity == 1 + with pytest.raises(ValueError): + test_site.user_quota = 1 + test_site.replace_license_tiers_with_user_quota(1) + assert 1 == test_site.user_quota + assert test_site.tier_explorer_capacity is None + + +@pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed") +def test_create(server: TSC.Server) -> None: + response_xml = CREATE_XML.read_text() + with requests_mock.mock() as m: + m.post(server.sites.baseurl, text=response_xml) + new_site = TSC.SiteItem( + name="Tableau", + content_url="tableau", + admin_mode=TSC.SiteItem.AdminMode.ContentAndUsers, + user_quota=15, + storage_quota=1000, + disable_subscriptions=True, + ) + new_site = server.sites.create(new_site) + + new_site._tier_viewer_capacity = None + new_site._tier_creator_capacity = None + new_site._tier_explorer_capacity = None + assert "0626857c-1def-4503-a7d8-7907c3ff9d9f" == new_site.id + assert "tableau" == new_site.content_url + assert "Tableau" == new_site.name + assert "Active" == new_site.state + assert "ContentAndUsers" == new_site.admin_mode + assert new_site.revision_history_enabled is False + assert new_site.subscribe_others_enabled is True + assert new_site.disable_subscriptions is True + assert 15 == new_site.user_quota + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.sites.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f", status_code=204) + server.sites.delete("0626857c-1def-4503-a7d8-7907c3ff9d9f") + + +def test_delete_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.sites.delete("") + + +def test_encrypt(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.post(server.sites.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/encrypt-extracts", status_code=200) + server.sites.encrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f") + + +def test_recrypt(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.post(server.sites.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/reencrypt-extracts", status_code=200) + server.sites.re_encrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f") + + +def test_decrypt(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.post(server.sites.baseurl + "/0626857c-1def-4503-a7d8-7907c3ff9d9f/decrypt-extracts", status_code=200) + server.sites.decrypt_extracts("0626857c-1def-4503-a7d8-7907c3ff9d9f") + + +def test_list_auth_configurations(server: TSC.Server) -> None: + server.version = "3.24" + response_xml = SITE_AUTH_CONFIG_XML.read_text() + + assert server.sites.baseurl == server.sites.baseurl + + with requests_mock.mock() as m: + m.get(f"{server.sites.baseurl}/{server.site_id}/site-auth-configurations", status_code=200, text=response_xml) + configs = server.sites.list_auth_configurations() + + assert len(configs) == 2, "Expected 2 auth configurations" + + assert configs[0].auth_setting == "OIDC" + assert configs[0].enabled + assert configs[0].idp_configuration_id == "00000000-0000-0000-0000-000000000000" + assert configs[0].idp_configuration_name == "Initial Salesforce" + assert configs[0].known_provider_alias == "Salesforce" + assert configs[1].auth_setting == "SAML" + assert configs[1].enabled + assert configs[1].idp_configuration_id == "11111111-1111-1111-1111-111111111111" + assert configs[1].idp_configuration_name == "Initial SAML" + assert configs[1].known_provider_alias is None + + +@pytest.mark.parametrize("capture", [True, False, None]) +def test_parsing_attr_capture(capture): + server = TSC.Server("http://test", False) + server.version = "3.10" + attrs = {"contentUrl": "test", "name": "test"} + if capture is not None: + attrs |= {"attributeCaptureEnabled": str(capture).lower()} + xml = _utils.server_response_factory("site", **attrs) + site = TSC.SiteItem.from_response(xml, server.namespace)[0] + + assert site.attribute_capture_enabled is capture, "Attribute capture not captured correctly" + + +@pytest.mark.filterwarnings("ignore:FlowsEnabled has been removed") +@pytest.mark.parametrize("req, capture", product(["create_req", "update_req"], [True, False, None])) +def test_encoding_attr_capture(req, capture): + site = TSC.SiteItem( + content_url="test", + name="test", + attribute_capture_enabled=capture, + ) + xml = getattr(RequestFactory.Site, req)(site) + site_elem = ET.fromstring(xml).find(".//site") + assert site_elem is not None, "Site element missing from XML body." + + if capture is not None: + assert ( + site_elem.attrib["attributeCaptureEnabled"] == str(capture).lower() + ), "Attribute capture not encoded correctly" + else: + assert "attributeCaptureEnabled" not in site_elem.attrib, "Attribute capture should not be encoded when None" diff --git a/test/test_site_model.py b/test/test_site_model.py index 60ad9c5e5..14914b875 100644 --- a/test/test_site_model.py +++ b/test/test_site_model.py @@ -1,65 +1,73 @@ -import unittest +import pytest import tableauserverclient as TSC -class SiteModelTests(unittest.TestCase): - def test_invalid_name(self): - self.assertRaises(ValueError, TSC.SiteItem, None, "url") - self.assertRaises(ValueError, TSC.SiteItem, "", "url") - site = TSC.SiteItem("site", "url") - with self.assertRaises(ValueError): - site.name = None +def test_invalid_name(): + with pytest.raises(ValueError): + TSC.SiteItem(None, "url") + with pytest.raises(ValueError): + TSC.SiteItem("", "url") + site = TSC.SiteItem("site", "url") + with pytest.raises(ValueError): + site.name = None - with self.assertRaises(ValueError): - site.name = "" + with pytest.raises(ValueError): + site.name = "" - def test_invalid_admin_mode(self): - site = TSC.SiteItem("site", "url") - with self.assertRaises(ValueError): - site.admin_mode = "Hello" - def test_invalid_content_url(self): - with self.assertRaises(ValueError): - site = TSC.SiteItem(name="蚵仔煎", content_url="蚵仔煎") +def test_invalid_admin_mode(): + site = TSC.SiteItem("site", "url") + with pytest.raises(ValueError): + site.admin_mode = "Hello" - with self.assertRaises(ValueError): - site = TSC.SiteItem(name="蚵仔煎", content_url=None) - def test_set_valid_content_url(self): - # Default Site - site = TSC.SiteItem(name="Default", content_url="") - self.assertEqual(site.content_url, "") +def test_invalid_content_url(): + with pytest.raises(ValueError): + site = TSC.SiteItem(name="蚵仔煎", content_url="蚵仔煎") - # Unicode Name and ascii content_url - site = TSC.SiteItem(name="蚵仔煎", content_url="omlette") - self.assertEqual(site.content_url, "omlette") + with pytest.raises(ValueError): + site = TSC.SiteItem(name="蚵仔煎", content_url=None) - def test_invalid_disable_subscriptions(self): - site = TSC.SiteItem("site", "url") - with self.assertRaises(ValueError): - site.disable_subscriptions = "Hello" - with self.assertRaises(ValueError): - site.disable_subscriptions = None +def test_set_valid_content_url(): + # Default Site + site = TSC.SiteItem(name="Default", content_url="") + assert site.content_url == "" - def test_invalid_revision_history_enabled(self): - site = TSC.SiteItem("site", "url") - with self.assertRaises(ValueError): - site.revision_history_enabled = "Hello" + # Unicode Name and ascii content_url + site = TSC.SiteItem(name="蚵仔煎", content_url="omlette") + assert site.content_url == "omlette" - with self.assertRaises(ValueError): - site.revision_history_enabled = None - def test_invalid_state(self): - site = TSC.SiteItem("site", "url") - with self.assertRaises(ValueError): - site.state = "Hello" +def test_invalid_disable_subscriptions(): + site = TSC.SiteItem("site", "url") + with pytest.raises(ValueError): + site.disable_subscriptions = "Hello" - def test_invalid_subscribe_others_enabled(self): - site = TSC.SiteItem("site", "url") - with self.assertRaises(ValueError): - site.subscribe_others_enabled = "Hello" + with pytest.raises(ValueError): + site.disable_subscriptions = None - with self.assertRaises(ValueError): - site.subscribe_others_enabled = None + +def test_invalid_revision_history_enabled(): + site = TSC.SiteItem("site", "url") + with pytest.raises(ValueError): + site.revision_history_enabled = "Hello" + + with pytest.raises(ValueError): + site.revision_history_enabled = None + + +def test_invalid_state(): + site = TSC.SiteItem("site", "url") + with pytest.raises(ValueError): + site.state = "Hello" + + +def test_invalid_subscribe_others_enabled(): + site = TSC.SiteItem("site", "url") + with pytest.raises(ValueError): + site.subscribe_others_enabled = "Hello" + + with pytest.raises(ValueError): + site.subscribe_others_enabled = None diff --git a/test/test_sort.py b/test/test_sort.py index 8eebef6f4..f6ae576f4 100644 --- a/test/test_sort.py +++ b/test/test_sort.py @@ -1,103 +1,107 @@ -import re -import unittest +from urllib.parse import parse_qs +import pytest import requests_mock import tableauserverclient as TSC -class SortTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.server.version = "3.10" - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.baseurl = self.server.workbooks.baseurl +@pytest.fixture(scope="function") +def server(): + """Fixture to create a TSC.Server instance for testing.""" + server = TSC.Server("http://test", False) - def test_empty_filter(self): - self.assertRaises(TypeError, TSC.Filter, "") + # Fake signin + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - def test_filter_equals(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=13) - opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "Superstore")) + return server - resp = self.server.workbooks.get_request(url, request_object=opts) - self.assertTrue(re.search("pagenumber=13", resp.request.query)) - self.assertTrue(re.search("pagesize=13", resp.request.query)) - self.assertTrue(re.search("filter=name%3aeq%3asuperstore", resp.request.query)) +def test_empty_filter() -> None: + with pytest.raises(TypeError): + TSC.Filter("") # type: ignore - def test_filter_equals_list(self): - with self.assertRaises(ValueError) as cm: - TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.Equals, ["foo", "bar"]) - self.assertEqual("Filter values can only be a list if the operator is 'in'.", str(cm.exception)), +def test_filter_equals(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=13) + opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, "Superstore")) - def test_filter_in(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=13) + resp = server.workbooks.get_request(url, request_object=opts) + query = parse_qs(resp.request.query) + assert "pagenumber" in query + assert query["pagenumber"] == ["13"] + assert "pagesize" in query + assert query["pagesize"] == ["13"] + assert "filter" in query + assert query["filter"] == ["name:eq:superstore"] - opts.filter.add( - TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["stocks", "market"]) - ) - resp = self.server.workbooks.get_request(url, request_object=opts) - self.assertTrue(re.search("pagenumber=13", resp.request.query)) - self.assertTrue(re.search("pagesize=13", resp.request.query)) - self.assertTrue(re.search("filter=tags%3ain%3a%5bstocks%2cmarket%5d", resp.request.query)) - - def test_sort_asc(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=13) - opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) - - resp = self.server.workbooks.get_request(url, request_object=opts) - - self.assertTrue(re.search("pagenumber=13", resp.request.query)) - self.assertTrue(re.search("pagesize=13", resp.request.query)) - self.assertTrue(re.search("sort=name%3aasc", resp.request.query)) - - def test_filter_combo(self): - with requests_mock.mock() as m: - m.get(requests_mock.ANY) - url = "http://test/api/2.3/sites/dad65087-b08b-4603-af4e-2887b8aafc67/users" - opts = TSC.RequestOptions(pagesize=13, pagenumber=13) - - opts.filter.add( - TSC.Filter( - TSC.RequestOptions.Field.LastLogin, - TSC.RequestOptions.Operator.GreaterThanOrEqual, - "2017-01-15T00:00:00:00Z", - ) - ) +def test_filter_equals_list() -> None: + with pytest.raises(ValueError, match="Filter values can only be a list if the operator is 'in'.") as cm: + TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.Equals, ["foo", "bar"]) - opts.filter.add( - TSC.Filter(TSC.RequestOptions.Field.SiteRole, TSC.RequestOptions.Operator.Equals, "Publisher") - ) - resp = self.server.workbooks.get_request(url, request_object=opts) +def test_filter_in(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=13) + + opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Tags, TSC.RequestOptions.Operator.In, ["stocks", "market"])) + + resp = server.workbooks.get_request(url, request_object=opts) + query = parse_qs(resp.request.query) + assert "pagenumber" in query + assert query["pagenumber"] == ["13"] + assert "pagesize" in query + assert query["pagesize"] == ["13"] + assert "filter" in query + assert query["filter"] == ["tags:in:[stocks,market]"] - expected = ( - "pagenumber=13&pagesize=13&filter=lastlogin%3agte%3a" - "2017-01-15t00%3a00%3a00%3a00z%2csiterole%3aeq%3apublisher" - ) - self.assertTrue(re.search("pagenumber=13", resp.request.query)) - self.assertTrue(re.search("pagesize=13", resp.request.query)) - self.assertTrue( - re.search( - "filter=lastlogin%3agte%3a2017-01-15t00%3a00%3a00%3a00z%2csiterole%3aeq%3apublisher", - resp.request.query, - ) +def test_sort_asc(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=13) + opts.sort.add(TSC.Sort(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Direction.Asc)) + + resp = server.workbooks.get_request(url, request_object=opts) + query = parse_qs(resp.request.query) + assert "pagenumber" in query + assert query["pagenumber"] == ["13"] + assert "pagesize" in query + assert query["pagesize"] == ["13"] + assert "sort" in query + assert query["sort"] == ["name:asc"] + + +def test_filter_combo(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/users" + opts = TSC.RequestOptions(pagesize=13, pagenumber=13) + + opts.filter.add( + TSC.Filter( + TSC.RequestOptions.Field.LastLogin, + TSC.RequestOptions.Operator.GreaterThanOrEqual, + "2017-01-15T00:00:00:00Z", ) + ) + + opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.SiteRole, TSC.RequestOptions.Operator.Equals, "Publisher")) + resp = server.workbooks.get_request(url, request_object=opts) -if __name__ == "__main__": - unittest.main() + query = parse_qs(resp.request.query) + assert "pagenumber" in query + assert query["pagenumber"] == ["13"] + assert "pagesize" in query + assert query["pagesize"] == ["13"] + assert "filter" in query + assert query["filter"] == ["lastlogin:gte:2017-01-15t00:00:00:00z,siterole:eq:publisher"] diff --git a/test/test_ssl_config.py b/test/test_ssl_config.py index 036a326ca..28ef3fc5e 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" diff --git a/test/test_subscription.py b/test/test_subscription.py index 45dcb0a1c..7c78cc57d 100644 --- a/test/test_subscription.py +++ b/test/test_subscription.py @@ -1,100 +1,102 @@ -import os -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -CREATE_XML = os.path.join(TEST_ASSET_DIR, "subscription_create.xml") -GET_XML = os.path.join(TEST_ASSET_DIR, "subscription_get.xml") -GET_XML_BY_ID = os.path.join(TEST_ASSET_DIR, "subscription_get_by_id.xml") - - -class SubscriptionTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - self.server.version = "2.6" - - # Fake Signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.subscriptions.baseurl - - def test_get_subscriptions(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_subscriptions, pagination_item = self.server.subscriptions.get() - - self.assertEqual(2, pagination_item.total_available) - subscription = all_subscriptions[0] - self.assertEqual("382e9a6e-0c08-4a95-b6c1-c14df7bac3e4", subscription.id) - self.assertEqual("NOT FOUND!", subscription.message) - self.assertTrue(subscription.attach_image) - self.assertFalse(subscription.attach_pdf) - self.assertFalse(subscription.suspended) - self.assertFalse(subscription.send_if_view_empty) - self.assertIsNone(subscription.page_orientation) - self.assertIsNone(subscription.page_size_option) - self.assertEqual("Not Found Alert", subscription.subject) - self.assertEqual("cdd716ca-5818-470e-8bec-086885dbadee", subscription.target.id) - self.assertEqual("View", subscription.target.type) - self.assertEqual("c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e", subscription.user_id) - self.assertEqual("7617c389-cdca-4940-a66e-69956fcebf3e", subscription.schedule_id) - - subscription = all_subscriptions[1] - self.assertEqual("23cb7630-afc8-4c8e-b6cd-83ae0322ec66", subscription.id) - self.assertEqual("overview", subscription.message) - self.assertFalse(subscription.attach_image) - self.assertTrue(subscription.attach_pdf) - self.assertTrue(subscription.suspended) - self.assertTrue(subscription.send_if_view_empty) - self.assertEqual("PORTRAIT", subscription.page_orientation) - self.assertEqual("A5", subscription.page_size_option) - self.assertEqual("Last 7 Days", subscription.subject) - self.assertEqual("2e6b4e8f-22dd-4061-8f75-bf33703da7e5", subscription.target.id) - self.assertEqual("Workbook", subscription.target.type) - self.assertEqual("c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e", subscription.user_id) - self.assertEqual("3407cd38-7b39-4983-86a6-67a1506a5e3f", subscription.schedule_id) - - def test_get_subscription_by_id(self) -> None: - with open(GET_XML_BY_ID, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/382e9a6e-0c08-4a95-b6c1-c14df7bac3e4", text=response_xml) - subscription = self.server.subscriptions.get_by_id("382e9a6e-0c08-4a95-b6c1-c14df7bac3e4") - - self.assertEqual("382e9a6e-0c08-4a95-b6c1-c14df7bac3e4", subscription.id) - self.assertEqual("View", subscription.target.type) - self.assertEqual("cdd716ca-5818-470e-8bec-086885dbadee", subscription.target.id) - self.assertEqual("c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e", subscription.user_id) - self.assertEqual("Not Found Alert", subscription.subject) - self.assertEqual("7617c389-cdca-4940-a66e-69956fcebf3e", subscription.schedule_id) - - def test_create_subscription(self) -> None: - with open(CREATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - - target_item = TSC.Target("960e61f2-1838-40b2-bba2-340c9492f943", "workbook") - new_subscription = TSC.SubscriptionItem( - "subject", "4906c453-d5ec-4972-9ff4-789b629bdfa2", "8d30c8de-0a5f-4bee-b266-c621b4f3eed0", target_item - ) - new_subscription = self.server.subscriptions.create(new_subscription) - - self.assertEqual("78e9318d-2d29-4d67-b60f-3f2f5fd89ecc", new_subscription.id) - self.assertEqual("sub_name", new_subscription.subject) - self.assertEqual("960e61f2-1838-40b2-bba2-340c9492f943", new_subscription.target.id) - self.assertEqual("Workbook", new_subscription.target.type) - self.assertEqual("4906c453-d5ec-4972-9ff4-789b629bdfa2", new_subscription.schedule_id) - self.assertEqual("8d30c8de-0a5f-4bee-b266-c621b4f3eed0", new_subscription.user_id) - - def test_delete_subscription(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/78e9318d-2d29-4d67-b60f-3f2f5fd89ecc", status_code=204) - self.server.subscriptions.delete("78e9318d-2d29-4d67-b60f-3f2f5fd89ecc") +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +CREATE_XML = TEST_ASSET_DIR / "subscription_create.xml" +GET_XML = TEST_ASSET_DIR / "subscription_get.xml" +GET_XML_BY_ID = TEST_ASSET_DIR / "subscription_get_by_id.xml" + + +@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" + server.version = "2.6" + + return server + + +def test_get_subscriptions(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.subscriptions.baseurl, text=response_xml) + all_subscriptions, pagination_item = server.subscriptions.get() + + assert 2 == pagination_item.total_available + subscription = all_subscriptions[0] + assert "382e9a6e-0c08-4a95-b6c1-c14df7bac3e4" == subscription.id + assert "NOT FOUND!" == subscription.message + assert subscription.attach_image is True + assert subscription.attach_pdf is False + assert subscription.suspended is False + assert subscription.send_if_view_empty is False + assert subscription.page_orientation is None + assert subscription.page_size_option is None + assert "Not Found Alert" == subscription.subject + assert "cdd716ca-5818-470e-8bec-086885dbadee" == subscription.target.id + assert "View" == subscription.target.type + assert "c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e" == subscription.user_id + assert "7617c389-cdca-4940-a66e-69956fcebf3e" == subscription.schedule_id + + subscription = all_subscriptions[1] + assert "23cb7630-afc8-4c8e-b6cd-83ae0322ec66" == subscription.id + assert "overview" == subscription.message + assert subscription.attach_image is False + assert subscription.attach_pdf is True + assert subscription.suspended is True + assert subscription.send_if_view_empty is True + assert "PORTRAIT" == subscription.page_orientation + assert "A5" == subscription.page_size_option + assert "Last 7 Days" == subscription.subject + assert "2e6b4e8f-22dd-4061-8f75-bf33703da7e5" == subscription.target.id + assert "Workbook" == subscription.target.type + assert "c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e" == subscription.user_id + assert "3407cd38-7b39-4983-86a6-67a1506a5e3f" == subscription.schedule_id + + +def test_get_subscription_by_id(server: TSC.Server) -> None: + response_xml = GET_XML_BY_ID.read_text() + with requests_mock.mock() as m: + m.get(server.subscriptions.baseurl + "/382e9a6e-0c08-4a95-b6c1-c14df7bac3e4", text=response_xml) + subscription = server.subscriptions.get_by_id("382e9a6e-0c08-4a95-b6c1-c14df7bac3e4") + + assert "382e9a6e-0c08-4a95-b6c1-c14df7bac3e4" == subscription.id + assert "View" == subscription.target.type + assert "cdd716ca-5818-470e-8bec-086885dbadee" == subscription.target.id + assert "c0d5fc44-ad8c-4957-bec0-b70ed0f8df1e" == subscription.user_id + assert "Not Found Alert" == subscription.subject + assert "7617c389-cdca-4940-a66e-69956fcebf3e" == subscription.schedule_id + + +def test_create_subscription(server: TSC.Server) -> None: + response_xml = CREATE_XML.read_text() + with requests_mock.mock() as m: + m.post(server.subscriptions.baseurl, text=response_xml) + + target_item = TSC.Target("960e61f2-1838-40b2-bba2-340c9492f943", "workbook") + new_subscription = TSC.SubscriptionItem( + "subject", "4906c453-d5ec-4972-9ff4-789b629bdfa2", "8d30c8de-0a5f-4bee-b266-c621b4f3eed0", target_item + ) + new_subscription = server.subscriptions.create(new_subscription) + + assert "78e9318d-2d29-4d67-b60f-3f2f5fd89ecc" == new_subscription.id + assert "sub_name" == new_subscription.subject + assert "960e61f2-1838-40b2-bba2-340c9492f943" == new_subscription.target.id + assert "Workbook" == new_subscription.target.type + assert "4906c453-d5ec-4972-9ff4-789b629bdfa2" == new_subscription.schedule_id + assert "8d30c8de-0a5f-4bee-b266-c621b4f3eed0" == new_subscription.user_id + + +def test_delete_subscription(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.subscriptions.baseurl + "/78e9318d-2d29-4d67-b60f-3f2f5fd89ecc", status_code=204) + server.subscriptions.delete("78e9318d-2d29-4d67-b60f-3f2f5fd89ecc") diff --git a/test/test_table.py b/test/test_table.py index 8c6c71f76..2f3c3c8d6 100644 --- a/test/test_table.py +++ b/test/test_table.py @@ -1,59 +1,65 @@ -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC -from ._utils import read_xml_asset - -GET_XML = "table_get.xml" -UPDATE_XML = "table_update.xml" - - -class TableTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.5" - - self.baseurl = self.server.tables.baseurl - - def test_get(self): - response_xml = read_xml_asset(GET_XML) - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_tables, pagination_item = self.server.tables.get() - - self.assertEqual(4, pagination_item.total_available) - self.assertEqual("10224773-ecee-42ac-b822-d786b0b8e4d9", all_tables[0].id) - self.assertEqual("dim_Product", all_tables[0].name) - - self.assertEqual("53c77bc1-fb41-4342-a75a-f68ac0656d0d", all_tables[1].id) - self.assertEqual("customer", all_tables[1].name) - self.assertEqual("dbo", all_tables[1].schema) - self.assertEqual("9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0", all_tables[1].contact_id) - self.assertEqual(False, all_tables[1].certified) - - def test_update(self): - response_xml = read_xml_asset(UPDATE_XML) - with requests_mock.mock() as m: - m.put(self.baseurl + "/10224773-ecee-42ac-b822-d786b0b8e4d9", text=response_xml) - single_table = TSC.TableItem("test") - single_table._id = "10224773-ecee-42ac-b822-d786b0b8e4d9" - - single_table.contact_id = "8e1a8235-c9ee-4d61-ae82-2ffacceed8e0" - single_table.certified = True - single_table.certification_note = "Test" - single_table = self.server.tables.update(single_table) - - self.assertEqual("10224773-ecee-42ac-b822-d786b0b8e4d9", single_table.id) - self.assertEqual("8e1a8235-c9ee-4d61-ae82-2ffacceed8e0", single_table.contact_id) - self.assertEqual(True, single_table.certified) - self.assertEqual("Test", single_table.certification_note) - - def test_delete(self): - with requests_mock.mock() as m: - m.delete(self.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) - self.server.tables.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "table_get.xml" +UPDATE_XML = TEST_ASSET_DIR / "table_update.xml" + + +@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" + server.version = "3.5" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.tables.baseurl, text=response_xml) + all_tables, pagination_item = server.tables.get() + + assert 4 == pagination_item.total_available + assert "10224773-ecee-42ac-b822-d786b0b8e4d9" == all_tables[0].id + assert "dim_Product" == all_tables[0].name + + assert "53c77bc1-fb41-4342-a75a-f68ac0656d0d" == all_tables[1].id + assert "customer" == all_tables[1].name + assert "dbo" == all_tables[1].schema + assert "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" == all_tables[1].contact_id + assert False == all_tables[1].certified + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.tables.baseurl + "/10224773-ecee-42ac-b822-d786b0b8e4d9", text=response_xml) + single_table = TSC.TableItem("test") + single_table._id = "10224773-ecee-42ac-b822-d786b0b8e4d9" + + single_table.contact_id = "8e1a8235-c9ee-4d61-ae82-2ffacceed8e0" + single_table.certified = True + single_table.certification_note = "Test" + single_table = server.tables.update(single_table) + + assert "10224773-ecee-42ac-b822-d786b0b8e4d9" == single_table.id + assert "8e1a8235-c9ee-4d61-ae82-2ffacceed8e0" == single_table.contact_id + assert True == single_table.certified + assert "Test" == single_table.certification_note + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.tables.baseurl + "/0448d2ed-590d-4fa0-b272-a2a8a24555b5", status_code=204) + server.tables.delete("0448d2ed-590d-4fa0-b272-a2a8a24555b5") diff --git a/test/test_tableauauth_model.py b/test/test_tableauauth_model.py index 195bcf0a9..17db52770 100644 --- a/test/test_tableauauth_model.py +++ b/test/test_tableauauth_model.py @@ -1,12 +1,8 @@ -import unittest +import pytest import tableauserverclient as TSC -class TableauAuthModelTests(unittest.TestCase): - def setUp(self): - self.auth = TSC.TableauAuth("user", "password", site_id="site1", user_id_to_impersonate="admin") - - def test_username_password_required(self): - with self.assertRaises(TypeError): - TSC.TableauAuth() +def test_username_password_required(): + with pytest.raises(TypeError): + TSC.TableauAuth() diff --git a/test/test_tagging.py b/test/test_tagging.py index 23dffebfb..8bfc90386 100644 --- a/test/test_tagging.py +++ b/test/test_tagging.py @@ -1,6 +1,7 @@ from contextlib import ExitStack import re from collections.abc import Iterable +from typing import Optional, Protocol import uuid from xml.etree import ElementTree as ET @@ -198,9 +199,14 @@ def test_update_tags(get_server, endpoint_type, item, tags) -> None: endpoint.update_tags(item) +class HasID(Protocol): + @property + def id(self) -> Optional[str]: ... + + def test_tags_batch_add(get_server) -> None: server = get_server - content = [make_workbook(), make_view(), make_datasource(), make_table(), make_database()] + content: list[HasID] = [make_workbook(), make_view(), make_datasource(), make_table(), make_database()] tags = ["a", "b"] add_tags_xml = batch_add_tags_xml_response_factory(tags, content) with requests_mock.mock() as m: @@ -210,8 +216,16 @@ def test_tags_batch_add(get_server) -> None: text=add_tags_xml, ) tag_result = server.tags.batch_add(tags, content) + history = m.request_history assert set(tag_result) == set(tags) + assert len(history) == 1 + body = ET.fromstring(history[0].body) + id_types = {c.id: c.__class__.__name__.replace("Item", "") for c in content} + for tag in body.findall(".//content"): + content_type = tag.attrib.get("contentType", "") + content_id = tag.attrib.get("id", "") + assert content_type == id_types.get(content_id, ""), f"Content type mismatch for {content_id}" def test_tags_batch_delete(get_server) -> None: diff --git a/test/test_task.py b/test/test_task.py index 2d724b879..fb99d58e4 100644 --- a/test/test_task.py +++ b/test/test_task.py @@ -1,8 +1,7 @@ -import os -import unittest from datetime import time from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC @@ -11,179 +10,190 @@ TEST_ASSET_DIR = Path(__file__).parent / "assets" -GET_XML_NO_WORKBOOK = os.path.join(TEST_ASSET_DIR, "tasks_no_workbook_or_datasource.xml") -GET_XML_WITH_WORKBOOK = os.path.join(TEST_ASSET_DIR, "tasks_with_workbook.xml") -GET_XML_WITH_DATASOURCE = os.path.join(TEST_ASSET_DIR, "tasks_with_datasource.xml") -GET_XML_WITH_WORKBOOK_AND_DATASOURCE = os.path.join(TEST_ASSET_DIR, "tasks_with_workbook_and_datasource.xml") -GET_XML_DATAACCELERATION_TASK = os.path.join(TEST_ASSET_DIR, "tasks_with_dataacceleration_task.xml") -GET_XML_RUN_NOW_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_run_now_response.xml") -GET_XML_CREATE_TASK_RESPONSE = os.path.join(TEST_ASSET_DIR, "tasks_create_extract_task.xml") +GET_XML_NO_WORKBOOK = TEST_ASSET_DIR / "tasks_no_workbook_or_datasource.xml" +GET_XML_WITH_WORKBOOK = TEST_ASSET_DIR / "tasks_with_workbook.xml" +GET_XML_WITH_DATASOURCE = TEST_ASSET_DIR / "tasks_with_datasource.xml" +GET_XML_WITH_WORKBOOK_AND_DATASOURCE = TEST_ASSET_DIR / "tasks_with_workbook_and_datasource.xml" +GET_XML_DATAACCELERATION_TASK = TEST_ASSET_DIR / "tasks_with_dataacceleration_task.xml" +GET_XML_RUN_NOW_RESPONSE = TEST_ASSET_DIR / "tasks_run_now_response.xml" +GET_XML_CREATE_TASK_RESPONSE = TEST_ASSET_DIR / "tasks_create_extract_task.xml" GET_XML_WITHOUT_SCHEDULE = TEST_ASSET_DIR / "tasks_without_schedule.xml" GET_XML_WITH_INTERVAL = TEST_ASSET_DIR / "tasks_with_interval.xml" -class TaskTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.server.version = "3.19" - - # Fake Signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - # default task type is extractRefreshes - self.baseurl = "{}/{}".format(self.server.tasks.baseurl, "extractRefreshes") - - def test_get_tasks_with_no_workbook(self): - with open(GET_XML_NO_WORKBOOK, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_tasks, pagination_item = self.server.tasks.get() - - task = all_tasks[0] - self.assertEqual(None, task.target) - - def test_get_tasks_with_workbook(self): - with open(GET_XML_WITH_WORKBOOK, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_tasks, pagination_item = self.server.tasks.get() - - task = all_tasks[0] - self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id) - self.assertEqual("workbook", task.target.type) - - def test_get_tasks_with_datasource(self): - with open(GET_XML_WITH_DATASOURCE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_tasks, pagination_item = self.server.tasks.get() - - task = all_tasks[0] - self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id) - self.assertEqual("datasource", task.target.type) - - def test_get_tasks_with_workbook_and_datasource(self): - with open(GET_XML_WITH_WORKBOOK_AND_DATASOURCE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_tasks, pagination_item = self.server.tasks.get() - - self.assertEqual("workbook", all_tasks[0].target.type) - self.assertEqual("datasource", all_tasks[1].target.type) - self.assertEqual("workbook", all_tasks[2].target.type) - - def test_get_task_with_schedule(self): - with open(GET_XML_WITH_WORKBOOK, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_tasks, pagination_item = self.server.tasks.get() - - task = all_tasks[0] - self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id) - self.assertEqual("workbook", task.target.type) - self.assertEqual("b60b4efd-a6f7-4599-beb3-cb677e7abac1", task.schedule_id) - - def test_get_task_without_schedule(self): - with requests_mock.mock() as m: - m.get(self.baseurl, text=GET_XML_WITHOUT_SCHEDULE.read_text()) - all_tasks, pagination_item = self.server.tasks.get() - - task = all_tasks[0] - self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id) - self.assertEqual("datasource", task.target.type) - - def test_get_task_with_interval(self): - with requests_mock.mock() as m: - m.get(self.baseurl, text=GET_XML_WITH_INTERVAL.read_text()) - all_tasks, pagination_item = self.server.tasks.get() - - task = all_tasks[0] - self.assertEqual("e4de0575-fcc7-4232-5659-be09bb8e7654", task.target.id) - self.assertEqual("datasource", task.target.type) - - def test_delete(self): - with requests_mock.mock() as m: - m.delete(self.baseurl + "/c7a9327e-1cda-4504-b026-ddb43b976d1d", status_code=204) - self.server.tasks.delete("c7a9327e-1cda-4504-b026-ddb43b976d1d") - - def test_delete_missing_id(self): - self.assertRaises(ValueError, self.server.tasks.delete, "") - - def test_get_materializeviews_tasks(self): - with open(GET_XML_DATAACCELERATION_TASK, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(f"{self.server.tasks.baseurl}/{TaskItem.Type.DataAcceleration}", text=response_xml) - all_tasks, pagination_item = self.server.tasks.get(task_type=TaskItem.Type.DataAcceleration) - - task = all_tasks[0] - self.assertEqual("a462c148-fc40-4670-a8e4-39b7f0c58c7f", task.target.id) - self.assertEqual("workbook", task.target.type) - self.assertEqual("b22190b4-6ac2-4eed-9563-4afc03444413", task.schedule_id) - self.assertEqual(parse_datetime("2019-12-09T22:30:00Z"), task.schedule_item.next_run_at) - self.assertEqual(parse_datetime("2019-12-09T20:45:04Z"), task.last_run_at) - self.assertEqual(TSC.TaskItem.Type.DataAcceleration, task.task_type) - - def test_delete_data_acceleration(self): - with requests_mock.mock() as m: - m.delete( - "{}/{}/{}".format( - self.server.tasks.baseurl, TaskItem.Type.DataAcceleration, "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" - ), - status_code=204, - ) - self.server.tasks.delete("c9cff7f9-309c-4361-99ff-d4ba8c9f5467", TaskItem.Type.DataAcceleration) - - def test_get_by_id(self): - with open(GET_XML_WITH_WORKBOOK, "rb") as f: - response_xml = f.read().decode("utf-8") - task_id = "f84901ac-72ad-4f9b-a87e-7a3500402ad6" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{task_id}", text=response_xml) - task = self.server.tasks.get_by_id(task_id) - - self.assertEqual("c7a9327e-1cda-4504-b026-ddb43b976d1d", task.target.id) - self.assertEqual("workbook", task.target.type) - self.assertEqual("b60b4efd-a6f7-4599-beb3-cb677e7abac1", task.schedule_id) - self.assertEqual(TSC.TaskItem.Type.ExtractRefresh, task.task_type) - - def test_run_now(self): - task_id = "f84901ac-72ad-4f9b-a87e-7a3500402ad6" - task = TaskItem(task_id, TaskItem.Type.ExtractRefresh, 100) - with open(GET_XML_RUN_NOW_RESPONSE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(f"{self.baseurl}/{task_id}/runNow", text=response_xml) - job_response_content = self.server.tasks.run(task).decode("utf-8") - - self.assertTrue("7b6b59a8-ac3c-4d1d-2e9e-0b5b4ba8a7b6" in job_response_content) - self.assertTrue("RefreshExtract" in job_response_content) - - def test_create_extract_task(self): - monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), interval_value=15) - monthly_schedule = TSC.ScheduleItem( - None, - None, - None, - None, - monthly_interval, - ) - target_item = TSC.Target("workbook_id", "workbook") +@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" + server.version = "3.19" + + return server + + +@pytest.fixture(scope="function") +def baseurl(server: TSC.Server) -> str: + return f"{server.tasks.baseurl}/extractRefreshes" + + +def test_get_tasks_with_no_workbook(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_NO_WORKBOOK.read_text() + with requests_mock.mock() as m: + m.get(baseurl, text=response_xml) + all_tasks, pagination_item = server.tasks.get() + + task = all_tasks[0] + assert task.target is None + + +def test_get_tasks_with_workbook(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_WITH_WORKBOOK.read_text() + with requests_mock.mock() as m: + m.get(baseurl, text=response_xml) + all_tasks, pagination_item = server.tasks.get() + + task = all_tasks[0] + assert "c7a9327e-1cda-4504-b026-ddb43b976d1d" == task.target.id + assert "workbook" == task.target.type + + +def test_get_tasks_with_datasource(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_WITH_DATASOURCE.read_text() + with requests_mock.mock() as m: + m.get(baseurl, text=response_xml) + all_tasks, pagination_item = server.tasks.get() + + task = all_tasks[0] + assert "c7a9327e-1cda-4504-b026-ddb43b976d1d" == task.target.id + assert "datasource" == task.target.type + + +def test_get_tasks_with_workbook_and_datasource(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_WITH_WORKBOOK_AND_DATASOURCE.read_text() + with requests_mock.mock() as m: + m.get(baseurl, text=response_xml) + all_tasks, pagination_item = server.tasks.get() + + assert "workbook" == all_tasks[0].target.type + assert "datasource" == all_tasks[1].target.type + assert "workbook" == all_tasks[2].target.type - task = TaskItem(None, "FullRefresh", None, schedule_item=monthly_schedule, target=target_item) - with open(GET_XML_CREATE_TASK_RESPONSE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(f"{self.baseurl}", text=response_xml) - create_response_content = self.server.tasks.create(task).decode("utf-8") +def test_get_task_with_schedule(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_WITH_WORKBOOK.read_text() + with requests_mock.mock() as m: + m.get(baseurl, text=response_xml) + all_tasks, pagination_item = server.tasks.get() - self.assertTrue("task_id" in create_response_content) - self.assertTrue("workbook_id" in create_response_content) - self.assertTrue("FullRefresh" in create_response_content) + task = all_tasks[0] + assert "c7a9327e-1cda-4504-b026-ddb43b976d1d" == task.target.id + assert "workbook" == task.target.type + assert "b60b4efd-a6f7-4599-beb3-cb677e7abac1" == task.schedule_id + + +def test_get_task_without_schedule(server: TSC.Server, baseurl: str) -> None: + with requests_mock.mock() as m: + m.get(baseurl, text=GET_XML_WITHOUT_SCHEDULE.read_text()) + all_tasks, pagination_item = server.tasks.get() + + task = all_tasks[0] + assert "c7a9327e-1cda-4504-b026-ddb43b976d1d" == task.target.id + assert "datasource" == task.target.type + + +def test_get_task_with_interval(server: TSC.Server, baseurl: str) -> None: + with requests_mock.mock() as m: + m.get(baseurl, text=GET_XML_WITH_INTERVAL.read_text()) + all_tasks, pagination_item = server.tasks.get() + + task = all_tasks[0] + assert "e4de0575-fcc7-4232-5659-be09bb8e7654" == task.target.id + assert "datasource" == task.target.type + + +def test_delete(server: TSC.Server, baseurl: str) -> None: + with requests_mock.mock() as m: + m.delete(baseurl + "/c7a9327e-1cda-4504-b026-ddb43b976d1d", status_code=204) + server.tasks.delete("c7a9327e-1cda-4504-b026-ddb43b976d1d") + + +def test_delete_missing_id(server: TSC.Server, baseurl: str) -> None: + with pytest.raises(ValueError): + server.tasks.delete("") + + +def test_get_materializeviews_tasks(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_DATAACCELERATION_TASK.read_text() + with requests_mock.mock() as m: + m.get(f"{server.tasks.baseurl}/{TaskItem.Type.DataAcceleration}", text=response_xml) + all_tasks, pagination_item = server.tasks.get(task_type=TaskItem.Type.DataAcceleration) + + task = all_tasks[0] + assert "a462c148-fc40-4670-a8e4-39b7f0c58c7f" == task.target.id + assert "workbook" == task.target.type + assert "b22190b4-6ac2-4eed-9563-4afc03444413" == task.schedule_id + assert parse_datetime("2019-12-09T22:30:00Z") == task.schedule_item.next_run_at + assert parse_datetime("2019-12-09T20:45:04Z") == task.last_run_at + assert TSC.TaskItem.Type.DataAcceleration == task.task_type + + +def test_delete_data_acceleration(server: TSC.Server, baseurl: str) -> None: + with requests_mock.mock() as m: + m.delete( + "{}/{}/{}".format( + server.tasks.baseurl, TaskItem.Type.DataAcceleration, "c9cff7f9-309c-4361-99ff-d4ba8c9f5467" + ), + status_code=204, + ) + server.tasks.delete("c9cff7f9-309c-4361-99ff-d4ba8c9f5467", TaskItem.Type.DataAcceleration) + + +def test_get_by_id(server: TSC.Server, baseurl: str) -> None: + response_xml = GET_XML_WITH_WORKBOOK.read_text() + task_id = "f84901ac-72ad-4f9b-a87e-7a3500402ad6" + with requests_mock.mock() as m: + m.get(f"{baseurl}/{task_id}", text=response_xml) + task = server.tasks.get_by_id(task_id) + + assert "c7a9327e-1cda-4504-b026-ddb43b976d1d" == task.target.id + assert "workbook" == task.target.type + assert "b60b4efd-a6f7-4599-beb3-cb677e7abac1" == task.schedule_id + assert TSC.TaskItem.Type.ExtractRefresh == task.task_type + + +def test_run_now(server: TSC.Server, baseurl: str) -> None: + task_id = "f84901ac-72ad-4f9b-a87e-7a3500402ad6" + task = TaskItem(task_id, TaskItem.Type.ExtractRefresh, 100) + response_xml = GET_XML_RUN_NOW_RESPONSE.read_text() + with requests_mock.mock() as m: + m.post(f"{baseurl}/{task_id}/runNow", text=response_xml) + job_response_content = server.tasks.run(task).decode("utf-8") + + assert "7b6b59a8-ac3c-4d1d-2e9e-0b5b4ba8a7b6" in job_response_content + assert "RefreshExtract" in job_response_content + + +def test_create_extract_task(server: TSC.Server, baseurl: str) -> None: + monthly_interval = TSC.MonthlyInterval(start_time=time(23, 30), interval_value=15) + monthly_schedule = TSC.ScheduleItem( + None, # type: ignore[arg-type] + None, # type: ignore[arg-type] + None, # type: ignore[arg-type] + None, # type: ignore[arg-type] + monthly_interval, + ) + target_item = TSC.Target("workbook_id", "workbook") + + task = TaskItem(None, "FullRefresh", None, schedule_item=monthly_schedule, target=target_item) # type: ignore[arg-type] + + response_xml = GET_XML_CREATE_TASK_RESPONSE.read_text() + with requests_mock.mock() as m: + m.post(f"{baseurl}", text=response_xml) + create_response_content = server.tasks.create(task).decode("utf-8") + + assert "task_id" in create_response_content + assert "workbook_id" in create_response_content + assert "FullRefresh" in create_response_content diff --git a/test/test_user.py b/test/test_user.py index fa2ac3a12..8f489187f 100644 --- a/test/test_user.py +++ b/test/test_user.py @@ -1,322 +1,573 @@ -import os -import unittest +import csv +import io +from pathlib import Path +import re +from unittest.mock import patch +from pathlib import Path from defusedxml import ElementTree as ET +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime, parse_datetime +from tableauserverclient.server.endpoint.users_endpoint import create_users_csv, remove_users_csv + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +BULK_ADD_XML = TEST_ASSET_DIR / "users_bulk_add_job.xml" +GET_XML = TEST_ASSET_DIR / "user_get.xml" +GET_XML_ALL_FIELDS = TEST_ASSET_DIR / "user_get_all_fields.xml" +GET_EMPTY_XML = TEST_ASSET_DIR / "user_get_empty.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "user_get_by_id.xml" +UPDATE_XML = TEST_ASSET_DIR / "user_update.xml" +ADD_XML = TEST_ASSET_DIR / "user_add.xml" +POPULATE_WORKBOOKS_XML = TEST_ASSET_DIR / "user_populate_workbooks.xml" +GET_FAVORITES_XML = TEST_ASSET_DIR / "favorites_get.xml" +POPULATE_GROUPS_XML = TEST_ASSET_DIR / "user_populate_groups.xml" + +USERNAMES = TEST_ASSET_DIR / "Data" / "usernames.csv" +USERS = TEST_ASSET_DIR / "Data" / "user_details.csv" + + +def make_user( + name: str, + site_role: str = "", + auth_setting: str = "", + domain: str = "", + fullname: str = "", + email: str = "", + idp_id: str = "", +) -> TSC.UserItem: + user = TSC.UserItem(name, site_role or None) + if auth_setting: + user.auth_setting = auth_setting + if domain: + user._domain_name = domain + if fullname: + user.fullname = fullname + if email: + user.email = email + if idp_id: + user.idp_configuration_id = idp_id + return user + + +@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_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.users.baseurl + "?fields=_all_", text=response_xml) + all_users, pagination_item = server.users.get() + + assert 2 == pagination_item.total_available + assert 2 == len(all_users) + + assert any(user.id == "dd2239f6-ddf1-4107-981a-4cf94e415794" for user in all_users) + single_user = next(user for user in all_users if user.id == "dd2239f6-ddf1-4107-981a-4cf94e415794") + assert "alice" == single_user.name + assert "Publisher" == single_user.site_role + assert "2016-08-16T23:17:06Z" == format_datetime(single_user.last_login) + assert "alice cook" == single_user.fullname + assert "alicecook@test.com" == single_user.email + + assert any(user.id == "2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3" for user in all_users) + single_user = next(user for user in all_users if user.id == "2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3") + assert "Bob" == single_user.name + assert "Interactor" == single_user.site_role + assert "Bob Smith" == single_user.fullname + assert "bob@test.com" == single_user.email + + +def test_get_empty(server: TSC.Server) -> None: + response_xml = GET_EMPTY_XML.read_text() + with requests_mock.mock() as m: + m.get(server.users.baseurl, text=response_xml) + all_users, pagination_item = server.users.get() + + assert 0 == pagination_item.total_available + assert [] == all_users + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.users.get() + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + m.get(server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794", text=response_xml) + single_user = server.users.get_by_id("dd2239f6-ddf1-4107-981a-4cf94e415794") + + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == single_user.id + assert "alice" == single_user.name + assert "Alice" == single_user.fullname + assert "Publisher" == single_user.site_role + assert "ServerDefault" == single_user.auth_setting + assert "2016-08-16T23:17:06Z" == format_datetime(single_user.last_login) + assert "local" == single_user.domain_name + + +def test_get_by_id_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.users.get_by_id("") + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794", text=response_xml) + single_user = TSC.UserItem("test", "Viewer") + single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_user.name = "Cassie" + single_user.fullname = "Cassie" + single_user.email = "cassie@email.com" + single_user = server.users.update(single_user) + + assert "Cassie" == single_user.name + assert "Cassie" == single_user.fullname + assert "cassie@email.com" == single_user.email + assert "Viewer" == single_user.site_role + + +def test_update_missing_id(server: TSC.Server) -> None: + single_user = TSC.UserItem("test", "Interactor") + with pytest.raises(TSC.MissingRequiredFieldError): + server.users.update(single_user) -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -GET_XML = os.path.join(TEST_ASSET_DIR, "user_get.xml") -GET_XML_ALL_FIELDS = os.path.join(TEST_ASSET_DIR, "user_get_all_fields.xml") -GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "user_get_empty.xml") -GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "user_get_by_id.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "user_update.xml") -ADD_XML = os.path.join(TEST_ASSET_DIR, "user_add.xml") -POPULATE_WORKBOOKS_XML = os.path.join(TEST_ASSET_DIR, "user_populate_workbooks.xml") -GET_FAVORITES_XML = os.path.join(TEST_ASSET_DIR, "favorites_get.xml") -POPULATE_GROUPS_XML = os.path.join(TEST_ASSET_DIR, "user_populate_groups.xml") - -USERNAMES = os.path.join(TEST_ASSET_DIR, "Data", "usernames.csv") -USERS = os.path.join(TEST_ASSET_DIR, "Data", "user_details.csv") - - -class UserTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.users.baseurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "?fields=_all_", text=response_xml) - all_users, pagination_item = self.server.users.get() - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual(2, len(all_users)) - - self.assertTrue(any(user.id == "dd2239f6-ddf1-4107-981a-4cf94e415794" for user in all_users)) - single_user = next(user for user in all_users if user.id == "dd2239f6-ddf1-4107-981a-4cf94e415794") - self.assertEqual("alice", single_user.name) - self.assertEqual("Publisher", single_user.site_role) - self.assertEqual("2016-08-16T23:17:06Z", format_datetime(single_user.last_login)) - self.assertEqual("alice cook", single_user.fullname) - self.assertEqual("alicecook@test.com", single_user.email) - - self.assertTrue(any(user.id == "2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3" for user in all_users)) - single_user = next(user for user in all_users if user.id == "2a47bbf8-8900-4ebb-b0a4-2723bd7c46c3") - self.assertEqual("Bob", single_user.name) - self.assertEqual("Interactor", single_user.site_role) - self.assertEqual("Bob Smith", single_user.fullname) - self.assertEqual("bob@test.com", single_user.email) - - def test_get_empty(self) -> None: - with open(GET_EMPTY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_users, pagination_item = self.server.users.get() - - self.assertEqual(0, pagination_item.total_available) - self.assertEqual([], all_users) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.users.get) - - def test_get_by_id(self) -> None: - with open(GET_BY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794", text=response_xml) - single_user = self.server.users.get_by_id("dd2239f6-ddf1-4107-981a-4cf94e415794") - - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", single_user.id) - self.assertEqual("alice", single_user.name) - self.assertEqual("Alice", single_user.fullname) - self.assertEqual("Publisher", single_user.site_role) - self.assertEqual("ServerDefault", single_user.auth_setting) - self.assertEqual("2016-08-16T23:17:06Z", format_datetime(single_user.last_login)) - self.assertEqual("local", single_user.domain_name) - - def test_get_by_id_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.users.get_by_id, "") - - def test_update(self) -> None: - with open(UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794", text=response_xml) - single_user = TSC.UserItem("test", "Viewer") - single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_user.name = "Cassie" - single_user.fullname = "Cassie" - single_user.email = "cassie@email.com" - single_user = self.server.users.update(single_user) - - self.assertEqual("Cassie", single_user.name) - self.assertEqual("Cassie", single_user.fullname) - self.assertEqual("cassie@email.com", single_user.email) - self.assertEqual("Viewer", single_user.site_role) - - def test_update_missing_id(self) -> None: + +def test_remove(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794", status_code=204) + server.users.remove("dd2239f6-ddf1-4107-981a-4cf94e415794") + + +def test_remove_with_replacement(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete( + server.users.baseurl + + "/dd2239f6-ddf1-4107-981a-4cf94e415794" + + "?mapAssetsTo=4cc4c17f-898a-4de4-abed-a1681c673ced", + status_code=204, + ) + server.users.remove("dd2239f6-ddf1-4107-981a-4cf94e415794", "4cc4c17f-898a-4de4-abed-a1681c673ced") + + +def test_remove_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.users.remove("") + + +def test_add(server: TSC.Server) -> None: + response_xml = ADD_XML.read_text() + with requests_mock.mock() as m: + m.post(server.users.baseurl + "", text=response_xml) + new_user = TSC.UserItem(name="Cassie", site_role="Viewer", auth_setting="ServerDefault") + new_user = server.users.add(new_user) + + assert "4cc4c17f-898a-4de4-abed-a1681c673ced" == new_user.id + assert "Cassie" == new_user.name + assert "Viewer" == new_user.site_role + assert "ServerDefault" == new_user.auth_setting + + +def test_populate_workbooks(server: TSC.Server) -> None: + response_xml = POPULATE_WORKBOOKS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794/workbooks", text=response_xml) single_user = TSC.UserItem("test", "Interactor") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.users.update, single_user) - - def test_remove(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794", status_code=204) - self.server.users.remove("dd2239f6-ddf1-4107-981a-4cf94e415794") - - def test_remove_with_replacement(self) -> None: - with requests_mock.mock() as m: - m.delete( - self.baseurl - + "/dd2239f6-ddf1-4107-981a-4cf94e415794" - + "?mapAssetsTo=4cc4c17f-898a-4de4-abed-a1681c673ced", - status_code=204, - ) - self.server.users.remove("dd2239f6-ddf1-4107-981a-4cf94e415794", "4cc4c17f-898a-4de4-abed-a1681c673ced") - - def test_remove_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.users.remove, "") - - def test_add(self) -> None: - with open(ADD_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "", text=response_xml) - new_user = TSC.UserItem(name="Cassie", site_role="Viewer", auth_setting="ServerDefault") - new_user = self.server.users.add(new_user) - - self.assertEqual("4cc4c17f-898a-4de4-abed-a1681c673ced", new_user.id) - self.assertEqual("Cassie", new_user.name) - self.assertEqual("Viewer", new_user.site_role) - self.assertEqual("ServerDefault", new_user.auth_setting) - - def test_populate_workbooks(self) -> None: - with open(POPULATE_WORKBOOKS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794/workbooks", text=response_xml) - single_user = TSC.UserItem("test", "Interactor") - single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - self.server.users.populate_workbooks(single_user) - - workbook_list = list(single_user.workbooks) - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", workbook_list[0].id) - self.assertEqual("SafariSample", workbook_list[0].name) - self.assertEqual("SafariSample", workbook_list[0].content_url) - self.assertEqual(False, workbook_list[0].show_tabs) - self.assertEqual(26, workbook_list[0].size) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(workbook_list[0].created_at)) - self.assertEqual("2016-07-26T20:35:05Z", format_datetime(workbook_list[0].updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", workbook_list[0].project_id) - self.assertEqual("default", workbook_list[0].project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", workbook_list[0].owner_id) - self.assertEqual({"Safari", "Sample"}, workbook_list[0].tags) - - def test_populate_owned_workbooks(self) -> None: - with open(POPULATE_WORKBOOKS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - # Query parameter ownedBy is case sensitive. - with requests_mock.mock(case_sensitive=True) as m: - m.get(self.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794/workbooks?ownedBy=true", text=response_xml) - single_user = TSC.UserItem("test", "Interactor") - single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - self.server.users.populate_workbooks(single_user, owned_only=True) - list(single_user.workbooks) - - request_history = m.request_history[0] - - assert "ownedBy" in request_history.qs, "ownedBy not in request history" - assert "true" in request_history.qs["ownedBy"], "ownedBy not set to true in request history" - - def test_populate_workbooks_missing_id(self) -> None: + single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + server.users.populate_workbooks(single_user) + + workbook_list = list(single_user.workbooks) + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == workbook_list[0].id + assert "SafariSample" == workbook_list[0].name + assert "SafariSample" == workbook_list[0].content_url + assert False == workbook_list[0].show_tabs + assert 26 == workbook_list[0].size + assert "2016-07-26T20:34:56Z" == format_datetime(workbook_list[0].created_at) + assert "2016-07-26T20:35:05Z" == format_datetime(workbook_list[0].updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == workbook_list[0].project_id + assert "default" == workbook_list[0].project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == workbook_list[0].owner_id + assert {"Safari", "Sample"} == workbook_list[0].tags + + +def test_populate_owned_workbooks(server: TSC.Server) -> None: + response_xml = POPULATE_WORKBOOKS_XML.read_text() + # Query parameter ownedBy is case sensitive. + with requests_mock.mock(case_sensitive=True) as m: + m.get(server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794/workbooks?ownedBy=true", text=response_xml) single_user = TSC.UserItem("test", "Interactor") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.users.populate_workbooks, single_user) - - def test_populate_favorites(self) -> None: - self.server.version = "2.5" - baseurl = self.server.favorites.baseurl + single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + server.users.populate_workbooks(single_user, owned_only=True) + list(single_user.workbooks) + + request_history = m.request_history[0] + + assert "ownedBy" in request_history.qs, "ownedBy not in request history" + assert "true" in request_history.qs["ownedBy"], "ownedBy not set to true in request history" + + +def test_populate_workbooks_missing_id(server: TSC.Server) -> None: + single_user = TSC.UserItem("test", "Interactor") + with pytest.raises(TSC.MissingRequiredFieldError): + server.users.populate_workbooks(single_user) + + +def test_populate_favorites(server: TSC.Server) -> None: + server.version = "2.5" + baseurl = server.favorites.baseurl + single_user = TSC.UserItem("test", "Interactor") + response_xml = GET_FAVORITES_XML.read_text() + with requests_mock.mock() as m: + m.get(f"{baseurl}/{single_user.id}", text=response_xml) + server.users.populate_favorites(single_user) + assert single_user._favorites is not None + assert len(single_user.favorites["workbooks"]) == 1 + assert len(single_user.favorites["views"]) == 1 + assert len(single_user.favorites["projects"]) == 1 + assert len(single_user.favorites["datasources"]) == 1 + + workbook = single_user.favorites["workbooks"][0] + view = single_user.favorites["views"][0] + datasource = single_user.favorites["datasources"][0] + project = single_user.favorites["projects"][0] + + assert workbook.id == "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" + assert view.id == "d79634e1-6063-4ec9-95ff-50acbf609ff5" + assert datasource.id == "e76a1461-3b1d-4588-bf1b-17551a879ad9" + assert project.id == "1d0304cd-3796-429f-b815-7258370b9b74" + + +def test_populate_groups(server: TSC.Server) -> None: + server.version = "3.7" + response_xml = POPULATE_GROUPS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794/groups", text=response_xml) single_user = TSC.UserItem("test", "Interactor") - with open(GET_FAVORITES_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(f"{baseurl}/{single_user.id}", text=response_xml) - self.server.users.populate_favorites(single_user) - self.assertIsNotNone(single_user._favorites) - self.assertEqual(len(single_user.favorites["workbooks"]), 1) - self.assertEqual(len(single_user.favorites["views"]), 1) - self.assertEqual(len(single_user.favorites["projects"]), 1) - self.assertEqual(len(single_user.favorites["datasources"]), 1) - - workbook = single_user.favorites["workbooks"][0] - view = single_user.favorites["views"][0] - datasource = single_user.favorites["datasources"][0] - project = single_user.favorites["projects"][0] - - self.assertEqual(workbook.id, "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00") - self.assertEqual(view.id, "d79634e1-6063-4ec9-95ff-50acbf609ff5") - self.assertEqual(datasource.id, "e76a1461-3b1d-4588-bf1b-17551a879ad9") - self.assertEqual(project.id, "1d0304cd-3796-429f-b815-7258370b9b74") - - def test_populate_groups(self) -> None: - self.server.version = "3.7" - with open(POPULATE_GROUPS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.server.users.baseurl + "/dd2239f6-ddf1-4107-981a-4cf94e415794/groups", text=response_xml) - single_user = TSC.UserItem("test", "Interactor") - single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - self.server.users.populate_groups(single_user) - - group_list = list(single_user.groups) - - self.assertEqual(3, len(group_list)) - self.assertEqual("ef8b19c0-43b6-11e6-af50-63f5805dbe3c", group_list[0].id) - self.assertEqual("All Users", group_list[0].name) - self.assertEqual("local", group_list[0].domain_name) - - self.assertEqual("e7833b48-c6f7-47b5-a2a7-36e7dd232758", group_list[1].id) - self.assertEqual("Another group", group_list[1].name) - self.assertEqual("local", group_list[1].domain_name) - - self.assertEqual("86a66d40-f289-472a-83d0-927b0f954dc8", group_list[2].id) - self.assertEqual("TableauExample", group_list[2].name) - self.assertEqual("local", group_list[2].domain_name) - - def test_get_usernames_from_file(self): - with open(ADD_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.server.users.baseurl, text=response_xml) - user_list, failures = self.server.users.create_from_file(USERNAMES) - assert user_list[0].name == "Cassie", user_list - assert failures == [], failures - - def test_get_users_from_file(self): - with open(ADD_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.server.users.baseurl, text=response_xml) - users, failures = self.server.users.create_from_file(USERS) - assert users[0].name == "Cassie", users - assert failures == [] - - def test_get_users_all_fields(self) -> None: - self.server.version = "3.7" - baseurl = self.server.users.baseurl - with open(GET_XML_ALL_FIELDS) as f: - response_xml = f.read() - - with requests_mock.mock() as m: - m.get(f"{baseurl}?fields=_all_", text=response_xml) - all_users, _ = self.server.users.get() - - assert all_users[0].auth_setting == "TableauIDWithMFA" - assert all_users[0].email == "bob@example.com" - assert all_users[0].external_auth_user_id == "38c870c3ac5e84ec66e6ced9fb23681835b07e56d5660371ac1f705cc65bd610" - assert all_users[0].fullname == "Bob Smith" - assert all_users[0].id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert all_users[0].last_login == parse_datetime("2025-02-04T06:39:20Z") - assert all_users[0].name == "bob@example.com" - assert all_users[0].site_role == "SiteAdministratorCreator" - assert all_users[0].locale is None - assert all_users[0].language == "en" - assert all_users[0].idp_configuration_id == "22222222-2222-2222-2222-222222222222" - assert all_users[0].domain_name == "TABID_WITH_MFA" - assert all_users[1].auth_setting == "TableauIDWithMFA" - assert all_users[1].email == "alice@example.com" - assert all_users[1].external_auth_user_id == "96f66b893b22669cdfa632275d354cd1d92cea0266f3be7702151b9b8c52be29" - assert all_users[1].fullname == "Alice Jones" - assert all_users[1].id == "f6d72445-285b-48e5-8380-f90b519ce682" - assert all_users[1].name == "alice@example.com" - assert all_users[1].site_role == "ExplorerCanPublish" - assert all_users[1].locale is None - assert all_users[1].language == "en" - assert all_users[1].idp_configuration_id == "22222222-2222-2222-2222-222222222222" - assert all_users[1].domain_name == "TABID_WITH_MFA" - - def test_add_user_idp_configuration(self) -> None: - with open(ADD_XML) as f: - response_xml = f.read() - user = TSC.UserItem(name="Cassie", site_role="Viewer") - user.idp_configuration_id = "012345" - - with requests_mock.mock() as m: - m.post(self.server.users.baseurl, text=response_xml) - user = self.server.users.add(user) - - history = m.request_history[0] - - tree = ET.fromstring(history.text) - user_elem = tree.find(".//user") - assert user_elem is not None - assert user_elem.attrib["idpConfigurationId"] == "012345" - - def test_update_user_idp_configuration(self) -> None: - with open(ADD_XML) as f: - response_xml = f.read() - user = TSC.UserItem(name="Cassie", site_role="Viewer") - user._id = "0123456789" - user.idp_configuration_id = "012345" - - with requests_mock.mock() as m: - m.put(f"{self.server.users.baseurl}/{user.id}", text=response_xml) - user = self.server.users.update(user) - - history = m.request_history[0] - - tree = ET.fromstring(history.text) - user_elem = tree.find(".//user") - assert user_elem is not None - assert user_elem.attrib["idpConfigurationId"] == "012345" + single_user._id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + server.users.populate_groups(single_user) + + group_list = list(single_user.groups) + + assert 3 == len(group_list) + assert "ef8b19c0-43b6-11e6-af50-63f5805dbe3c" == group_list[0].id + assert "All Users" == group_list[0].name + assert "local" == group_list[0].domain_name + + assert "e7833b48-c6f7-47b5-a2a7-36e7dd232758" == group_list[1].id + assert "Another group" == group_list[1].name + assert "local" == group_list[1].domain_name + + assert "86a66d40-f289-472a-83d0-927b0f954dc8" == group_list[2].id + assert "TableauExample" == group_list[2].name + assert "local" == group_list[2].domain_name + + +def test_get_usernames_from_file(server: TSC.Server): + response_xml = ADD_XML.read_text() + with requests_mock.mock() as m: + m.post(server.users.baseurl, text=response_xml) + with pytest.warns(DeprecationWarning): + user_list, failures = server.users.create_from_file(str(USERNAMES)) + assert user_list[0].name == "Cassie", user_list + assert failures == [], failures + + +def test_get_users_from_file(server: TSC.Server): + response_xml = ADD_XML.read_text() + with requests_mock.mock() as m: + m.post(server.users.baseurl, text=response_xml) + with pytest.warns(DeprecationWarning): + users, failures = server.users.create_from_file(str(USERS)) + assert users[0].name == "Cassie", users + assert failures == [] + + +def test_get_users_all_fields(server: TSC.Server) -> None: + server.version = "3.7" + baseurl = server.users.baseurl + response_xml = GET_XML_ALL_FIELDS.read_text() + + with requests_mock.mock() as m: + m.get(f"{baseurl}?fields=_all_", text=response_xml) + all_users, _ = server.users.get() + + assert all_users[0].auth_setting == "TableauIDWithMFA" + assert all_users[0].email == "bob@example.com" + assert all_users[0].external_auth_user_id == "38c870c3ac5e84ec66e6ced9fb23681835b07e56d5660371ac1f705cc65bd610" + assert all_users[0].fullname == "Bob Smith" + assert all_users[0].id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert all_users[0].last_login == parse_datetime("2025-02-04T06:39:20Z") + assert all_users[0].name == "bob@example.com" + assert all_users[0].site_role == "SiteAdministratorCreator" + assert all_users[0].locale is None + assert all_users[0].language == "en" + assert all_users[0].idp_configuration_id == "22222222-2222-2222-2222-222222222222" + assert all_users[0].domain_name == "TABID_WITH_MFA" + assert all_users[1].auth_setting == "TableauIDWithMFA" + assert all_users[1].email == "alice@example.com" + assert all_users[1].external_auth_user_id == "96f66b893b22669cdfa632275d354cd1d92cea0266f3be7702151b9b8c52be29" + assert all_users[1].fullname == "Alice Jones" + assert all_users[1].id == "f6d72445-285b-48e5-8380-f90b519ce682" + assert all_users[1].name == "alice@example.com" + assert all_users[1].site_role == "ExplorerCanPublish" + assert all_users[1].locale is None + assert all_users[1].language == "en" + assert all_users[1].idp_configuration_id == "22222222-2222-2222-2222-222222222222" + assert all_users[1].domain_name == "TABID_WITH_MFA" + + +def test_add_user_idp_configuration(server: TSC.Server) -> None: + response_xml = ADD_XML.read_text() + user = TSC.UserItem(name="Cassie", site_role="Viewer") + user.idp_configuration_id = "012345" + + with requests_mock.mock() as m: + m.post(server.users.baseurl, text=response_xml) + user = server.users.add(user) + + history = m.request_history[0] + + tree = ET.fromstring(history.text) + user_elem = tree.find(".//user") + assert user_elem is not None + assert user_elem.attrib["idpConfigurationId"] == "012345" + + +def test_update_user_idp_configuration(server: TSC.Server) -> None: + response_xml = ADD_XML.read_text() + user = TSC.UserItem(name="Cassie", site_role="Viewer") + user._id = "0123456789" + user.idp_configuration_id = "012345" + + with requests_mock.mock() as m: + m.put(f"{server.users.baseurl}/{user.id}", text=response_xml) + user = server.users.update(user) + + history = m.request_history[0] + + tree = ET.fromstring(history.text) + user_elem = tree.find(".//user") + assert user_elem is not None + assert user_elem.attrib["idpConfigurationId"] == "012345" + + +def test_create_users_csv() -> None: + users = [ + make_user("Alice", "Viewer"), + make_user("Bob", "Explorer"), + make_user("Charlie", "Creator", "SAML"), + make_user("Dave"), + make_user("Eve", "ServerAdministrator", "OpenID", "example.com", "Eve Example", "Eve@example.com"), + make_user("Frank", "SiteAdministratorExplorer", "TableauIDWithMFA", email="Frank@example.com"), + make_user("Grace", "SiteAdministratorCreator", "SAML", "example.com", "Grace Example", "gex@example.com"), + make_user("Hank", "Unlicensed"), + ] + + license_map = { + "Viewer": "Viewer", + "Explorer": "Explorer", + "ExplorerCanPublish": "Explorer", + "Creator": "Creator", + "SiteAdministratorExplorer": "Explorer", + "SiteAdministratorCreator": "Creator", + "ServerAdministrator": "Creator", + "Unlicensed": "Unlicensed", + } + publish_map = { + "Unlicensed": 0, + "Viewer": 0, + "Explorer": 0, + "Creator": 1, + "ExplorerCanPublish": 1, + "SiteAdministratorExplorer": 1, + "SiteAdministratorCreator": 1, + "ServerAdministrator": 1, + } + admin_map = { + "SiteAdministratorExplorer": "Site", + "SiteAdministratorCreator": "Site", + "ServerAdministrator": "System", + } + + csv_columns = ["name", "password", "fullname", "license", "admin", "publish", "email"] + csv_data = create_users_csv(users) + csv_file = io.StringIO(csv_data.decode("utf-8")) + csv_reader = csv.reader(csv_file) + for user, row in zip(users, csv_reader): + site_role = user.site_role or "Unlicensed" + name = f"{user.domain_name}\\{user.name}" if user.domain_name else user.name + csv_user = dict(zip(csv_columns, row)) + assert name == csv_user["name"] + assert (user.fullname or "") == csv_user["fullname"] + assert (user.email or "") == csv_user["email"] + assert license_map[site_role] == csv_user["license"] + assert admin_map.get(site_role, "") == csv_user["admin"] + assert publish_map[site_role] == int(csv_user["publish"]) + + +def test_bulk_add(server: TSC.Server) -> None: + server.version = "3.15" + users = [ + make_user("Alice", "Viewer"), + make_user("Bob", "Explorer"), + make_user("Charlie", "Creator", "SAML"), + make_user("Dave"), + make_user("Eve", "ServerAdministrator", "OpenID", "example.com", "Eve Example", "Eve@example.com"), + make_user("Frank", "SiteAdministratorExplorer", "TableauIDWithMFA", email="Frank@example.com"), + make_user("Grace", "SiteAdministratorCreator", "SAML", "example.com", "Grace Example", "gex@example.com"), + make_user("Hank", "Unlicensed"), + make_user("Ivy", "Unlicensed", idp_id="0123456789"), + ] + with requests_mock.mock() as m: + m.post(f"{server.users.baseurl}/import", text=BULK_ADD_XML.read_text()) + + job = server.users.bulk_add(users) + + assert isinstance(job, TSC.JobItem) + + assert m.last_request.method == "POST" + assert m.last_request.url == f"{server.users.baseurl}/import" + + body = m.last_request.body.replace(b"\r\n", b"\n") + assert body.startswith(b"--") # Check if it's a multipart request + boundary = body.split(b"\n")[0].strip() + + # Body starts and ends with a boundary string. Split the body into + # segments and ignore the empty sections at the start and end. + segments = [seg for s in body.split(boundary) if (seg := s.strip()) not in [b"", b"--"]] + assert len(segments) == 2 # Check if there are two segments + + # Check if the first segment is the csv file and the second segment is the xml + assert b'Content-Disposition: form-data; name="tableau_user_import"' in segments[0] + assert b'Content-Disposition: form-data; name="request_payload"' in segments[1] + assert b"Content-Type: file" in segments[0] + assert b"Content-Type: text/xml" in segments[1] + + xml_string = segments[1].split(b"\n\n")[1].strip() + xml = ET.fromstring(xml_string) + xml_users = xml.findall(".//user", namespaces={}) + assert len(xml_users) == len(users) + + for user, xml_user in zip(users, xml_users): + assert user.name == xml_user.get("name") + if user.idp_configuration_id is None: + assert xml_user.get("authSetting") == (user.auth_setting or "ServerDefault") + else: + assert xml_user.get("idpConfigurationId") == user.idp_configuration_id + assert xml_user.get("authSetting") is None + + csv_data = create_users_csv(users).replace(b"\r\n", b"\n") + assert csv_data.strip() == segments[0].split(b"\n\n")[1].strip() + + +def test_bulk_add_no_name(server: TSC.Server) -> None: + server.version = "3.15" + users = [ + TSC.UserItem(site_role="Viewer"), + ] + with requests_mock.mock() as m: + m.post(f"{server.users.baseurl}/import", text=BULK_ADD_XML.read_text()) + + with pytest.raises(ValueError, match="User name must be populated."): + server.users.bulk_add(users) + + +def test_bulk_remove(server: TSC.Server) -> None: + server.version = "3.15" + users = [ + make_user("Alice"), + make_user("Bob", domain="example.com"), + ] + with requests_mock.mock() as m: + m.post(f"{server.users.baseurl}/delete") + + server.users.bulk_remove(users) + + assert m.last_request.method == "POST" + assert m.last_request.url == f"{server.users.baseurl}/delete" + + body = m.last_request.body.replace(b"\r\n", b"\n") + assert body.startswith(b"--") # Check if it's a multipart request + boundary = body.split(b"\n")[0].strip() + + content = next(seg for seg in body.split(boundary) if seg.strip()) + assert b'Content-Disposition: form-data; name="tableau_user_delete"' in content + assert b"Content-Type: file" in content + + content = content.replace(b"\r\n", b"\n") + csv_data = content.split(b"\n\n")[1].decode("utf-8") + for user, row in zip(users, csv_data.split("\n")): + name, *_ = row.split(",") + assert name == f"{user.domain_name}\\{user.name}" if user.domain_name else user.name + + +def test_add_all(server: TSC.Server) -> None: + server.version = "2.0" + users = [ + make_user("Alice", "Viewer"), + make_user("Bob", "Explorer"), + make_user("Charlie", "Creator", "SAML"), + make_user("Dave"), + ] + + with patch("tableauserverclient.server.endpoint.users_endpoint.Users.add", autospec=True) as mock_add: + with pytest.warns(DeprecationWarning): + server.users.add_all(users) + + assert mock_add.call_count == len(users) + + +def test_add_idp_and_auth_error(server: TSC.Server) -> None: + server.version = "3.24" + users = [make_user("Alice", "Viewer", auth_setting="SAML", idp_id="01234")] + + with pytest.raises(ValueError, match="User cannot have both authSetting and idpConfigurationId."): + server.users.bulk_add(users) + + +def test_remove_users_csv(server: TSC.Server) -> None: + server.version = "3.15" + users = [ + make_user("Alice", "Viewer"), + make_user("Bob", "Explorer"), + make_user("Charlie", "Creator", "SAML"), + make_user("Dave"), + make_user("Eve", "ServerAdministrator", "OpenID", "example.com", "Eve Example", "Eve@example.com"), + make_user("Frank", "SiteAdministratorExplorer", "TableauIDWithMFA", email="Frank@example.com"), + make_user("Grace", "SiteAdministratorCreator", "SAML", "example.com", "Grace Example", "gex@example.com"), + make_user("Hank", "Unlicensed"), + make_user("Ivy", "Unlicensed", idp_id="0123456789"), + ] + + data = remove_users_csv(users) + assert isinstance(data, bytes), "remove_users_csv should return bytes" + csv_data = data.decode("utf-8") + records = re.split(r"\r?\n", csv_data.strip()) + assert len(records) == len(users), "Number of records in csv does not match number of users" + + for user, record in zip(users, records): + name, *rest = record.strip().split(",") + assert len(rest) == 6, "Number of fields in csv does not match expected number" + assert all([f == "" for f in rest]), "All fields except name should be empty" + if user.domain_name is None: + assert name == user.name, f"Name in csv does not match expected name: {user.name}" + else: + assert ( + name == f"{user.domain_name}\\{user.name}" + ), f"Name in csv does not match expected name: {user.domain_name}\\{user.name}" diff --git a/test/test_user_model.py b/test/test_user_model.py index a8a2c51cb..49e8dc25c 100644 --- a/test/test_user_model.py +++ b/test/test_user_model.py @@ -1,5 +1,4 @@ import logging -import unittest from unittest.mock import * import io @@ -8,120 +7,132 @@ import tableauserverclient as TSC -class UserModelTests(unittest.TestCase): - def test_invalid_auth_setting(self): - user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher) - with self.assertRaises(ValueError): - user.auth_setting = "Hello" - - def test_invalid_site_role(self): - user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher) - with self.assertRaises(ValueError): - user.site_role = "Hello" - - -class UserDataTest(unittest.TestCase): - logger = logging.getLogger("UserDataTest") - - role_inputs = [ - ["creator", "system", "yes", "SiteAdministrator"], - ["None", "system", "no", "SiteAdministrator"], - ["explorer", "SysTEm", "no", "SiteAdministrator"], - ["creator", "site", "yes", "SiteAdministratorCreator"], - ["explorer", "site", "yes", "SiteAdministratorExplorer"], - ["creator", "SITE", "no", "SiteAdministratorCreator"], - ["creator", "none", "yes", "Creator"], - ["explorer", "none", "yes", "ExplorerCanPublish"], - ["viewer", "None", "no", "Viewer"], - ["explorer", "no", "yes", "ExplorerCanPublish"], - ["EXPLORER", "noNO", "yes", "ExplorerCanPublish"], - ["explorer", "no", "no", "Explorer"], - ["unlicensed", "none", "no", "Unlicensed"], - ["Chef", "none", "yes", "Unlicensed"], - ["yes", "yes", "yes", "Unlicensed"], - ] - - valid_import_content = [ - "username, pword, fname, creator, site, yes, email", - "username, pword, fname, explorer, none, no, email", - "", - "u", - "p", - ] - - valid_username_content = ["jfitzgerald@tableau.com"] - - usernames = [ - "valid", - "valid@email.com", - "domain/valid", - "domain/valid@tmail.com", - "va!@#$%^&*()lid", - "in@v@lid", - "in valid", - "", - ] - - def test_validate_usernames(self): - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[0]) - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[1]) - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[2]) - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[3]) - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[4]) - with self.assertRaises(AttributeError): - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[5]) - with self.assertRaises(AttributeError): - TSC.UserItem.validate_username_or_throw(UserDataTest.usernames[6]) - - def test_evaluate_role(self): - for line in UserDataTest.role_inputs: - actual = TSC.UserItem.CSVImport._evaluate_site_role(line[0], line[1], line[2]) - assert actual == line[3], line + [actual] - - def test_get_user_detail_empty_line(self): - test_line = "" - test_user = TSC.UserItem.CSVImport.create_user_from_line(test_line) - assert test_user is None - - def test_get_user_detail_standard(self): - test_line = "username, pword, fname, license, admin, pub, email" - test_user: TSC.UserItem = TSC.UserItem.CSVImport.create_user_from_line(test_line) - assert test_user.name == "username", test_user.name - assert test_user.fullname == "fname", test_user.fullname - assert test_user.site_role == "Unlicensed", test_user.site_role - assert test_user.email == "email", test_user.email - - def test_get_user_details_only_username(self): - test_line = "username" - test_user: TSC.UserItem = TSC.UserItem.CSVImport.create_user_from_line(test_line) - - def test_populate_user_details_only_some(self): - values = "username, , , creator, admin" - user = TSC.UserItem.CSVImport.create_user_from_line(values) - assert user.name == "username" - - def test_validate_user_detail_standard(self): - test_line = "username, pword, fname, creator, site, 1, email" - TSC.UserItem.CSVImport._validate_import_line_or_throw(test_line, UserDataTest.logger) - TSC.UserItem.CSVImport.create_user_from_line(test_line) - - # for file handling - def _mock_file_content(self, content: list[str]) -> io.TextIOWrapper: - # the empty string represents EOF - # the tests run through the file twice, first to validate then to fetch - mock = MagicMock(io.TextIOWrapper) - content.append("") # EOF - mock.readline.side_effect = content - mock.name = "file-mock" - return mock - - def test_validate_import_file(self): - test_data = self._mock_file_content(UserDataTest.valid_import_content) - valid, invalid = TSC.UserItem.CSVImport.validate_file_for_import(test_data, UserDataTest.logger) - assert valid == 2, f"Expected two lines to be parsed, got {valid}" - assert invalid == [], f"Expected no failures, got {invalid}" - - def test_validate_usernames_file(self): - test_data = self._mock_file_content(UserDataTest.usernames) - valid, invalid = TSC.UserItem.CSVImport.validate_file_for_import(test_data, UserDataTest.logger) - assert valid == 5, f"Exactly 5 of the lines were valid, counted {valid + invalid}" +def test_invalid_auth_setting(): + user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher) + with pytest.raises(ValueError): + user.auth_setting = "Hello" + + +def test_invalid_site_role(): + user = TSC.UserItem("me", TSC.UserItem.Roles.Publisher) + with pytest.raises(ValueError): + user.site_role = "Hello" + + +logger = logging.getLogger("UserModelTest") + + +role_inputs = [ + ["creator", "system", "yes", "SiteAdministrator"], + ["None", "system", "no", "SiteAdministrator"], + ["explorer", "SysTEm", "no", "SiteAdministrator"], + ["creator", "site", "yes", "SiteAdministratorCreator"], + ["explorer", "site", "yes", "SiteAdministratorExplorer"], + ["creator", "SITE", "no", "SiteAdministratorCreator"], + ["creator", "none", "yes", "Creator"], + ["explorer", "none", "yes", "ExplorerCanPublish"], + ["viewer", "None", "no", "Viewer"], + ["explorer", "no", "yes", "ExplorerCanPublish"], + ["EXPLORER", "noNO", "yes", "ExplorerCanPublish"], + ["explorer", "no", "no", "Explorer"], + ["unlicensed", "none", "no", "Unlicensed"], + ["Chef", "none", "yes", "Unlicensed"], + ["yes", "yes", "yes", "Unlicensed"], +] + +valid_import_content = [ + "username, pword, fname, creator, site, yes, email", + "username, pword, fname, explorer, none, no, email", + "", + "u", + "p", +] + +valid_username_content = ["jfitzgerald@tableau.com"] + +usernames = [ + "valid", + "valid@email.com", + "domain/valid", + "domain/valid@tmail.com", + "va!@#$%^&*()lid", + "in@v@lid", + "in valid", + "", +] + + +def test_validate_usernames() -> None: + TSC.UserItem.validate_username_or_throw(usernames[0]) + TSC.UserItem.validate_username_or_throw(usernames[1]) + TSC.UserItem.validate_username_or_throw(usernames[2]) + TSC.UserItem.validate_username_or_throw(usernames[3]) + TSC.UserItem.validate_username_or_throw(usernames[4]) + with pytest.raises(AttributeError): + TSC.UserItem.validate_username_or_throw(usernames[5]) + with pytest.raises(AttributeError): + TSC.UserItem.validate_username_or_throw(usernames[6]) + + +def test_evaluate_role() -> None: + for line in role_inputs: + actual = TSC.UserItem.CSVImport._evaluate_site_role(line[0], line[1], line[2]) + assert actual == line[3], line + [actual] + + +def test_get_user_detail_empty_line() -> None: + test_line = "" + test_user = TSC.UserItem.CSVImport.create_user_from_line(test_line) + assert test_user is None + + +def test_get_user_detail_standard() -> None: + test_line = "username, pword, fname, license, admin, pub, email" + test_user = TSC.UserItem.CSVImport.create_user_from_line(test_line) + assert test_user is not None + assert test_user.name == "username", test_user.name + assert test_user.fullname == "fname", test_user.fullname + assert test_user.site_role == "Unlicensed", test_user.site_role + assert test_user.email == "email", test_user.email + + +def test_get_user_details_only_username() -> None: + test_line = "username" + test_user = TSC.UserItem.CSVImport.create_user_from_line(test_line) + + +def test_populate_user_details_only_some() -> None: + values = "username, , , creator, admin" + user = TSC.UserItem.CSVImport.create_user_from_line(values) + assert user is not None + assert user.name == "username" + + +def test_validate_user_detail_standard() -> None: + test_line = "username, pword, fname, creator, site, 1, email" + TSC.UserItem.CSVImport._validate_import_line_or_throw(test_line, logger) + TSC.UserItem.CSVImport.create_user_from_line(test_line) + + +# for file handling +def _mock_file_content(content: list[str]) -> io.TextIOWrapper: + # the empty string represents EOF + # the tests run through the file twice, first to validate then to fetch + mock = MagicMock(io.TextIOWrapper) + content.append("") # EOF + mock.readline.side_effect = content + mock.name = "file-mock" + return mock + + +def test_validate_import_file() -> None: + test_data = _mock_file_content(valid_import_content) + valid, invalid = TSC.UserItem.CSVImport.validate_file_for_import(test_data, logger) + assert valid == 2, f"Expected two lines to be parsed, got {valid}" + assert invalid == [], f"Expected no failures, got {invalid}" + + +def test_validate_usernames_file() -> None: + test_data = _mock_file_content(usernames) + valid, invalid = TSC.UserItem.CSVImport.validate_file_for_import(test_data, logger) + assert valid == 5, f"Exactly 5 of the lines were valid, counted {valid + len(invalid)}" diff --git a/test/test_view.py b/test/test_view.py index ee6d518de..b16f47c72 100644 --- a/test/test_view.py +++ b/test/test_view.py @@ -1,6 +1,6 @@ -import os -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC @@ -8,511 +8,532 @@ from tableauserverclient.datetime_helpers import format_datetime, parse_datetime from tableauserverclient.server.endpoint.exceptions import UnsupportedAttributeError -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, "view_add_tags.xml") -GET_XML = os.path.join(TEST_ASSET_DIR, "view_get.xml") -GET_XML_ALL_FIELDS = os.path.join(TEST_ASSET_DIR, "view_get_all_fields.xml") -GET_XML_ID = os.path.join(TEST_ASSET_DIR, "view_get_id.xml") -GET_XML_USAGE = os.path.join(TEST_ASSET_DIR, "view_get_usage.xml") -GET_XML_ID_USAGE = os.path.join(TEST_ASSET_DIR, "view_get_id_usage.xml") -POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, "Sample View Image.png") -POPULATE_PDF = os.path.join(TEST_ASSET_DIR, "populate_pdf.pdf") -POPULATE_CSV = os.path.join(TEST_ASSET_DIR, "populate_csv.csv") -POPULATE_EXCEL = os.path.join(TEST_ASSET_DIR, "populate_excel.xlsx") -POPULATE_PERMISSIONS_XML = os.path.join(TEST_ASSET_DIR, "view_populate_permissions.xml") -UPDATE_PERMISSIONS = os.path.join(TEST_ASSET_DIR, "view_update_permissions.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "workbook_update.xml") - - -class ViewTests(unittest.TestCase): - def setUp(self): - self.server = TSC.Server("http://test", False) - self.server.version = "3.2" - - # Fake sign in - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.views.baseurl - self.siteurl = self.server.views.siteurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_views, pagination_item = self.server.views.get() - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", all_views[0].id) - self.assertEqual("ENDANGERED SAFARI", all_views[0].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", all_views[0].content_url) - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", all_views[0].workbook_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_views[0].owner_id) - self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", all_views[0].project_id) - self.assertEqual({"tag1", "tag2"}, all_views[0].tags) - self.assertIsNone(all_views[0].created_at) - self.assertIsNone(all_views[0].updated_at) - self.assertIsNone(all_views[0].sheet_type) - - self.assertEqual("fd252f73-593c-4c4e-8584-c032b8022adc", all_views[1].id) - self.assertEqual("Overview", all_views[1].name) - self.assertEqual("Superstore/sheets/Overview", all_views[1].content_url) - self.assertEqual("6d13b0ca-043d-4d42-8c9d-3f3313ea3a00", all_views[1].workbook_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_views[1].owner_id) - self.assertEqual("5b534f74-3226-11e8-b47a-cb2e00f738a3", all_views[1].project_id) - self.assertEqual("2002-05-30T09:00:00Z", format_datetime(all_views[1].created_at)) - self.assertEqual("2002-06-05T08:00:59Z", format_datetime(all_views[1].updated_at)) - self.assertEqual("story", all_views[1].sheet_type) - - def test_get_by_id(self) -> None: - with open(GET_XML_ID, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5", text=response_xml) - view = self.server.views.get_by_id("d79634e1-6063-4ec9-95ff-50acbf609ff5") - - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", view.id) - self.assertEqual("ENDANGERED SAFARI", view.name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", view.content_url) - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", view.workbook_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", view.owner_id) - self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", view.project_id) - self.assertEqual({"tag1", "tag2"}, view.tags) - self.assertEqual("2002-05-30T09:00:00Z", format_datetime(view.created_at)) - self.assertEqual("2002-06-05T08:00:59Z", format_datetime(view.updated_at)) - self.assertEqual("story", view.sheet_type) - - def test_get_by_id_usage(self) -> None: - with open(GET_XML_ID_USAGE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5?includeUsageStatistics=true", text=response_xml) - view = self.server.views.get_by_id("d79634e1-6063-4ec9-95ff-50acbf609ff5", usage=True) - - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", view.id) - self.assertEqual("ENDANGERED SAFARI", view.name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", view.content_url) - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", view.workbook_id) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", view.owner_id) - self.assertEqual("5241e88d-d384-4fd7-9c2f-648b5247efc5", view.project_id) - self.assertEqual({"tag1", "tag2"}, view.tags) - self.assertEqual("2002-05-30T09:00:00Z", format_datetime(view.created_at)) - self.assertEqual("2002-06-05T08:00:59Z", format_datetime(view.updated_at)) - self.assertEqual("story", view.sheet_type) - self.assertEqual(7, view.total_views) - - def test_get_by_id_missing_id(self) -> None: - self.assertRaises(TSC.MissingRequiredFieldError, self.server.views.get_by_id, None) - - def test_get_with_usage(self) -> None: - with open(GET_XML_USAGE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "?includeUsageStatistics=true", text=response_xml) - all_views, pagination_item = self.server.views.get(usage=True) - - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", all_views[0].id) - self.assertEqual(7, all_views[0].total_views) - self.assertIsNone(all_views[0].created_at) - self.assertIsNone(all_views[0].updated_at) - self.assertIsNone(all_views[0].sheet_type) - - self.assertEqual("fd252f73-593c-4c4e-8584-c032b8022adc", all_views[1].id) - self.assertEqual(13, all_views[1].total_views) - self.assertEqual("2002-05-30T09:00:00Z", format_datetime(all_views[1].created_at)) - self.assertEqual("2002-06-05T08:00:59Z", format_datetime(all_views[1].updated_at)) - self.assertEqual("story", all_views[1].sheet_type) - - def test_get_with_usage_and_filter(self) -> None: - with open(GET_XML_USAGE, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "?includeUsageStatistics=true&filter=name:in:[foo,bar]", text=response_xml) - options = TSC.RequestOptions() - options.filter.add( - TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.In, ["foo", "bar"]) - ) - all_views, pagination_item = self.server.views.get(req_options=options, usage=True) - - self.assertEqual("ENDANGERED SAFARI", all_views[0].name) - self.assertEqual(7, all_views[0].total_views) - self.assertEqual("Overview", all_views[1].name) - self.assertEqual(13, all_views[1].total_views) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.views.get) - - def test_populate_preview_image(self) -> None: - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.siteurl + "/workbooks/3cc6cd06-89ce-4fdc-b935-5294135d6d42/" - "views/d79634e1-6063-4ec9-95ff-50acbf609ff5/previewImage", - content=response, - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - single_view._workbook_id = "3cc6cd06-89ce-4fdc-b935-5294135d6d42" - self.server.views.populate_preview_image(single_view) - self.assertEqual(response, single_view.preview_image) - - def test_populate_preview_image_missing_id(self) -> None: +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +ADD_TAGS_XML = TEST_ASSET_DIR / "view_add_tags.xml" +GET_XML = TEST_ASSET_DIR / "view_get.xml" +GET_XML_ALL_FIELDS = TEST_ASSET_DIR / "view_get_all_fields.xml" +GET_XML_ID = TEST_ASSET_DIR / "view_get_id.xml" +GET_XML_USAGE = TEST_ASSET_DIR / "view_get_usage.xml" +GET_XML_ID_USAGE = TEST_ASSET_DIR / "view_get_id_usage.xml" +POPULATE_PREVIEW_IMAGE = TEST_ASSET_DIR / "Sample View Image.png" +POPULATE_PDF = TEST_ASSET_DIR / "populate_pdf.pdf" +POPULATE_CSV = TEST_ASSET_DIR / "populate_csv.csv" +POPULATE_EXCEL = TEST_ASSET_DIR / "populate_excel.xlsx" +POPULATE_PERMISSIONS_XML = TEST_ASSET_DIR / "view_populate_permissions.xml" +UPDATE_PERMISSIONS = TEST_ASSET_DIR / "view_update_permissions.xml" +UPDATE_XML = TEST_ASSET_DIR / "workbook_update.xml" + + +@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" + server.version = "3.2" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl, text=response_xml) + all_views, pagination_item = server.views.get() + + assert 2 == pagination_item.total_available + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == all_views[0].id + assert "ENDANGERED SAFARI" == all_views[0].name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == all_views[0].content_url + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == all_views[0].workbook_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_views[0].owner_id + assert "5241e88d-d384-4fd7-9c2f-648b5247efc5" == all_views[0].project_id + assert {"tag1", "tag2"} == all_views[0].tags + assert all_views[0].created_at is None + assert all_views[0].updated_at is None + assert all_views[0].sheet_type is None + + assert "fd252f73-593c-4c4e-8584-c032b8022adc" == all_views[1].id + assert "Overview" == all_views[1].name + assert "Superstore/sheets/Overview" == all_views[1].content_url + assert "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" == all_views[1].workbook_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_views[1].owner_id + assert "5b534f74-3226-11e8-b47a-cb2e00f738a3" == all_views[1].project_id + assert "2002-05-30T09:00:00Z" == format_datetime(all_views[1].created_at) + assert "2002-06-05T08:00:59Z" == format_datetime(all_views[1].updated_at) + assert "story" == all_views[1].sheet_type + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = GET_XML_ID.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5", text=response_xml) + view = server.views.get_by_id("d79634e1-6063-4ec9-95ff-50acbf609ff5") + + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == view.id + assert "ENDANGERED SAFARI" == view.name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == view.content_url + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == view.workbook_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == view.owner_id + assert "5241e88d-d384-4fd7-9c2f-648b5247efc5" == view.project_id + assert {"tag1", "tag2"} == view.tags + assert "2002-05-30T09:00:00Z" == format_datetime(view.created_at) + assert "2002-06-05T08:00:59Z" == format_datetime(view.updated_at) + assert "story" == view.sheet_type + + +def test_get_by_id_usage(server: TSC.Server) -> None: + response_xml = GET_XML_ID_USAGE.read_text() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5?includeUsageStatistics=true", + text=response_xml, + ) + view = server.views.get_by_id("d79634e1-6063-4ec9-95ff-50acbf609ff5", usage=True) + + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == view.id + assert "ENDANGERED SAFARI" == view.name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == view.content_url + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == view.workbook_id + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == view.owner_id + assert "5241e88d-d384-4fd7-9c2f-648b5247efc5" == view.project_id + assert {"tag1", "tag2"} == view.tags + assert "2002-05-30T09:00:00Z" == format_datetime(view.created_at) + assert "2002-06-05T08:00:59Z" == format_datetime(view.updated_at) + assert "story" == view.sheet_type + assert 7 == view.total_views + + +def test_get_by_id_missing_id(server: TSC.Server) -> None: + with pytest.raises(TSC.MissingRequiredFieldError): + server.views.get_by_id(None) + + +def test_get_with_usage(server: TSC.Server) -> None: + response_xml = GET_XML_USAGE.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?includeUsageStatistics=true", text=response_xml) + all_views, pagination_item = server.views.get(usage=True) + + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == all_views[0].id + assert 7 == all_views[0].total_views + assert all_views[0].created_at is None + assert all_views[0].updated_at is None + assert all_views[0].sheet_type is None + + assert "fd252f73-593c-4c4e-8584-c032b8022adc" == all_views[1].id + assert 13 == all_views[1].total_views + assert "2002-05-30T09:00:00Z" == format_datetime(all_views[1].created_at) + assert "2002-06-05T08:00:59Z" == format_datetime(all_views[1].updated_at) + assert "story" == all_views[1].sheet_type + + +def test_get_with_usage_and_filter(server: TSC.Server) -> None: + response_xml = GET_XML_USAGE.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "?includeUsageStatistics=true&filter=name:in:[foo,bar]", text=response_xml) + options = TSC.RequestOptions() + options.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.In, ["foo", "bar"])) + all_views, pagination_item = server.views.get(req_options=options, usage=True) + + assert "ENDANGERED SAFARI" == all_views[0].name + assert 7 == all_views[0].total_views + assert "Overview" == all_views[1].name + assert 13 == all_views[1].total_views + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.views.get() + + +def test_populate_preview_image(server: TSC.Server) -> None: + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.siteurl + "/workbooks/3cc6cd06-89ce-4fdc-b935-5294135d6d42/" + "views/d79634e1-6063-4ec9-95ff-50acbf609ff5/previewImage", + content=response, + ) single_view = TSC.ViewItem() single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - self.assertRaises(TSC.MissingRequiredFieldError, self.server.views.populate_preview_image, single_view) - - single_view._id = None single_view._workbook_id = "3cc6cd06-89ce-4fdc-b935-5294135d6d42" - self.assertRaises(TSC.MissingRequiredFieldError, self.server.views.populate_preview_image, single_view) - - def test_populate_image(self) -> None: - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image", content=response) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - self.server.views.populate_image(single_view) - self.assertEqual(response, single_view.image) - - def test_populate_image_unsupported(self) -> None: - self.server.version = "3.8" - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080", - content=response, - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - - req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080) - - with self.assertRaises(UnsupportedAttributeError): - self.server.views.populate_image(single_view, req_option) - - def test_populate_image_viz_dimensions(self) -> None: - self.server.version = "3.23" - self.baseurl = self.server.views.baseurl - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080", - content=response, - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - - req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080) - - self.server.views.populate_image(single_view, req_option) - self.assertEqual(response, single_view.image) - - history = m.request_history - - def test_populate_image_with_options(self) -> None: - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?resolution=high&maxAge=10", content=response - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High, maxage=10) - self.server.views.populate_image(single_view, req_option) - self.assertEqual(response, single_view.image) - - def test_populate_pdf(self) -> None: - with open(POPULATE_PDF, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait&maxAge=5", - content=response, - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - - size = TSC.PDFRequestOptions.PageType.Letter - orientation = TSC.PDFRequestOptions.Orientation.Portrait - req_option = TSC.PDFRequestOptions(size, orientation, 5) - - self.server.views.populate_pdf(single_view, req_option) - self.assertEqual(response, single_view.pdf) - - def test_populate_csv(self) -> None: - with open(POPULATE_CSV, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data?maxAge=1", content=response) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - request_option = TSC.CSVRequestOptions(maxage=1) - self.server.views.populate_csv(single_view, request_option) - - csv_file = b"".join(single_view.csv) - self.assertEqual(response, csv_file) - - def test_populate_csv_default_maxage(self) -> None: - with open(POPULATE_CSV, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data", content=response) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - self.server.views.populate_csv(single_view) - - csv_file = b"".join(single_view.csv) - self.assertEqual(response, csv_file) - - def test_populate_image_missing_id(self) -> None: + server.views.populate_preview_image(single_view) + assert response == single_view.preview_image + + +def test_populate_preview_image_missing_id(server: TSC.Server) -> None: + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + with pytest.raises(TSC.MissingRequiredFieldError): + server.views.populate_preview_image(single_view) + + single_view._id = None + single_view._workbook_id = "3cc6cd06-89ce-4fdc-b935-5294135d6d42" + with pytest.raises(TSC.MissingRequiredFieldError): + server.views.populate_preview_image(single_view) + + +def test_populate_image(server: TSC.Server) -> None: + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image", content=response) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + server.views.populate_image(single_view) + assert response == single_view.image + + +def test_populate_image_unsupported(server: TSC.Server) -> None: + server.version = "3.8" + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080) + + with pytest.raises(UnsupportedAttributeError): + server.views.populate_image(single_view, req_option) + + +def test_populate_image_viz_dimensions(server: TSC.Server) -> None: + server.version = "3.23" + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?vizWidth=1920&vizHeight=1080", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + req_option = TSC.ImageRequestOptions(viz_width=1920, viz_height=1080) + + server.views.populate_image(single_view, req_option) + assert response == single_view.image + + history = m.request_history + + +def test_populate_image_with_options(server: TSC.Server) -> None: + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/image?resolution=high&maxAge=10", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + req_option = TSC.ImageRequestOptions(imageresolution=TSC.ImageRequestOptions.Resolution.High, maxage=10) + server.views.populate_image(single_view, req_option) + assert response == single_view.image + + +def test_populate_pdf(server: TSC.Server) -> None: + response = POPULATE_PDF.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?type=letter&orientation=portrait&maxAge=5", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + size = TSC.PDFRequestOptions.PageType.Letter + orientation = TSC.PDFRequestOptions.Orientation.Portrait + req_option = TSC.PDFRequestOptions(size, orientation, 5) + + server.views.populate_pdf(single_view, req_option) + assert response == single_view.pdf + + +def test_populate_csv(server: TSC.Server) -> None: + response = POPULATE_CSV.read_bytes() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data?maxAge=1", content=response) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + request_option = TSC.CSVRequestOptions(maxage=1) + server.views.populate_csv(single_view, request_option) + + csv_file = b"".join(single_view.csv) + assert response == csv_file + + +def test_populate_csv_default_maxage(server: TSC.Server) -> None: + response = POPULATE_CSV.read_bytes() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/data", content=response) single_view = TSC.ViewItem() - single_view._id = None - self.assertRaises(TSC.MissingRequiredFieldError, self.server.views.populate_image, single_view) - - def test_populate_permissions(self) -> None: - with open(POPULATE_PERMISSIONS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/e490bec4-2652-4fda-8c4e-f087db6fa328/permissions", text=response_xml) - single_view = TSC.ViewItem() - single_view._id = "e490bec4-2652-4fda-8c4e-f087db6fa328" - - self.server.views.populate_permissions(single_view) - permissions = single_view.permissions - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "c8f2773a-c83a-11e8-8c8f-33e6d787b506") - self.assertDictEqual( - permissions[0].capabilities, - { - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, - }, - ) - - def test_add_permissions(self) -> None: - with open(UPDATE_PERMISSIONS, "rb") as f: - response_xml = f.read().decode("utf-8") + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + server.views.populate_csv(single_view) + + csv_file = b"".join(single_view.csv) + assert response == csv_file + + +def test_populate_image_missing_id(server: TSC.Server) -> None: + single_view = TSC.ViewItem() + single_view._id = None + with pytest.raises(TSC.MissingRequiredFieldError): + server.views.populate_image(single_view) + +def test_populate_permissions(server: TSC.Server) -> None: + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/e490bec4-2652-4fda-8c4e-f087db6fa328/permissions", text=response_xml) single_view = TSC.ViewItem() - single_view._id = "21778de4-b7b9-44bc-a599-1506a2639ace" - - bob = UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - group_of_people = GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af") - - new_permissions = [PermissionsRule(bob, {"Write": "Allow"}), PermissionsRule(group_of_people, {"Read": "Deny"})] - - with requests_mock.mock() as m: - m.put(self.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) - permissions = self.server.views.update_permissions(single_view, new_permissions) - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "5e5e1978-71fa-11e4-87dd-7382f5c437af") - self.assertDictEqual(permissions[0].capabilities, {TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny}) - - self.assertEqual(permissions[1].grantee.tag_name, "user") - self.assertEqual(permissions[1].grantee.id, "7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - self.assertDictEqual(permissions[1].capabilities, {TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow}) - - def test_update_tags(self) -> None: - with open(ADD_TAGS_XML, "rb") as f: - add_tags_xml = f.read().decode("utf-8") - with open(UPDATE_XML, "rb") as f: - update_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/tags", text=add_tags_xml) - m.delete(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/tags/b", status_code=204) - m.delete(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/tags/d", status_code=204) - m.put(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5", text=update_xml) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - single_view._initial_tags.update(["a", "b", "c", "d"]) - single_view.tags.update(["a", "c", "e"]) - updated_view = self.server.views.update(single_view) - - self.assertEqual(single_view.tags, updated_view.tags) - self.assertEqual(single_view._initial_tags, updated_view._initial_tags) - - def test_populate_excel(self) -> None: - self.server.version = "3.8" - self.baseurl = self.server.views.baseurl - with open(POPULATE_EXCEL, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/crosstab/excel?maxAge=1", content=response) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - request_option = TSC.ExcelRequestOptions(maxage=1) - self.server.views.populate_excel(single_view, request_option) - - excel_file = b"".join(single_view.excel) - self.assertEqual(response, excel_file) - - def test_filter_excel(self) -> None: - self.server.version = "3.8" - self.baseurl = self.server.views.baseurl - with open(POPULATE_EXCEL, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/crosstab/excel?maxAge=1", content=response) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - request_option = TSC.ExcelRequestOptions(maxage=1) - request_option.vf("stuff", "1") - self.server.views.populate_excel(single_view, request_option) - - excel_file = b"".join(single_view.excel) - self.assertEqual(response, excel_file) - - def test_pdf_height(self) -> None: - self.server.version = "3.8" - self.baseurl = self.server.views.baseurl - with open(POPULATE_PDF, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?vizHeight=1080&vizWidth=1920", - content=response, - ) - single_view = TSC.ViewItem() - single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" - - req_option = TSC.PDFRequestOptions( - viz_height=1080, - viz_width=1920, - ) - - self.server.views.populate_pdf(single_view, req_option) - self.assertEqual(response, single_view.pdf) - - def test_pdf_errors(self) -> None: - req_option = TSC.PDFRequestOptions(viz_height=1080) - with self.assertRaises(ValueError): - req_option.get_query_params() - req_option = TSC.PDFRequestOptions(viz_width=1920) - with self.assertRaises(ValueError): - req_option.get_query_params() - - def test_view_get_all_fields(self) -> None: - self.server.version = "3.21" - self.baseurl = self.server.views.baseurl - with open(GET_XML_ALL_FIELDS) as f: - response_xml = f.read() - - ro = TSC.RequestOptions() - ro.all_fields = True - - with requests_mock.mock() as m: - m.get(f"{self.baseurl}?fields=_all_", text=response_xml) - views, _ = self.server.views.get(req_options=ro) - - assert views[0].id == "2bdcd787-dcc6-4a5d-bc61-2846f1ef4534" - assert views[0].name == "Overview" - assert views[0].content_url == "Superstore/sheets/Overview" - assert views[0].created_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[0].updated_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[0].sheet_type == "dashboard" - assert views[0].favorites_total == 0 - assert views[0].view_url_name == "Overview" - assert isinstance(views[0].workbook, TSC.WorkbookItem) - assert views[0].workbook.id == "9df3e2d1-070e-497a-9578-8cc557ced9df" - assert views[0].workbook.name == "Superstore" - assert views[0].workbook.content_url == "Superstore" - assert views[0].workbook.show_tabs - assert views[0].workbook.size == 2 - assert views[0].workbook.created_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[0].workbook.updated_at == parse_datetime("2024-02-14T04:42:10Z") - assert views[0].workbook.sheet_count == 9 - assert not views[0].workbook.has_extracts - assert isinstance(views[0].owner, TSC.UserItem) - assert views[0].owner.email == "bob@example.com" - assert views[0].owner.fullname == "Bob" - assert views[0].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert views[0].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert views[0].owner.name == "bob@example.com" - assert views[0].owner.site_role == "SiteAdministratorCreator" - assert isinstance(views[0].project, TSC.ProjectItem) - assert views[0].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert views[0].project.name == "Samples" - assert views[0].project.description == "This project includes automatically uploaded samples." - assert views[0].total_views == 0 - assert isinstance(views[0].location, TSC.LocationItem) - assert views[0].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert views[0].location.type == "Project" - assert views[1].id == "2a3fd19d-9129-413d-9ff7-9dfc36bf7f7e" - assert views[1].name == "Product" - assert views[1].content_url == "Superstore/sheets/Product" - assert views[1].created_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[1].updated_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[1].sheet_type == "dashboard" - assert views[1].favorites_total == 0 - assert views[1].view_url_name == "Product" - assert isinstance(views[1].workbook, TSC.WorkbookItem) - assert views[1].workbook.id == "9df3e2d1-070e-497a-9578-8cc557ced9df" - assert views[1].workbook.name == "Superstore" - assert views[1].workbook.content_url == "Superstore" - assert views[1].workbook.show_tabs - assert views[1].workbook.size == 2 - assert views[1].workbook.created_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[1].workbook.updated_at == parse_datetime("2024-02-14T04:42:10Z") - assert views[1].workbook.sheet_count == 9 - assert not views[1].workbook.has_extracts - assert isinstance(views[1].owner, TSC.UserItem) - assert views[1].owner.email == "bob@example.com" - assert views[1].owner.fullname == "Bob" - assert views[1].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert views[1].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert views[1].owner.name == "bob@example.com" - assert views[1].owner.site_role == "SiteAdministratorCreator" - assert isinstance(views[1].project, TSC.ProjectItem) - assert views[1].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert views[1].project.name == "Samples" - assert views[1].project.description == "This project includes automatically uploaded samples." - assert views[1].total_views == 0 - assert isinstance(views[1].location, TSC.LocationItem) - assert views[1].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert views[1].location.type == "Project" - assert views[2].id == "459eda9a-85e4-46bf-a2f2-62936bd2e99a" - assert views[2].name == "Customers" - assert views[2].content_url == "Superstore/sheets/Customers" - assert views[2].created_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[2].updated_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[2].sheet_type == "dashboard" - assert views[2].favorites_total == 0 - assert views[2].view_url_name == "Customers" - assert isinstance(views[2].workbook, TSC.WorkbookItem) - assert views[2].workbook.id == "9df3e2d1-070e-497a-9578-8cc557ced9df" - assert views[2].workbook.name == "Superstore" - assert views[2].workbook.content_url == "Superstore" - assert views[2].workbook.show_tabs - assert views[2].workbook.size == 2 - assert views[2].workbook.created_at == parse_datetime("2024-02-14T04:42:09Z") - assert views[2].workbook.updated_at == parse_datetime("2024-02-14T04:42:10Z") - assert views[2].workbook.sheet_count == 9 - assert not views[2].workbook.has_extracts - assert isinstance(views[2].owner, TSC.UserItem) - assert views[2].owner.email == "bob@example.com" - assert views[2].owner.fullname == "Bob" - assert views[2].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert views[2].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert views[2].owner.name == "bob@example.com" - assert views[2].owner.site_role == "SiteAdministratorCreator" - assert isinstance(views[2].project, TSC.ProjectItem) - assert views[2].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert views[2].project.name == "Samples" - assert views[2].project.description == "This project includes automatically uploaded samples." - assert views[2].total_views == 0 - assert isinstance(views[2].location, TSC.LocationItem) - assert views[2].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert views[2].location.type == "Project" + single_view._id = "e490bec4-2652-4fda-8c4e-f087db6fa328" + + server.views.populate_permissions(single_view) + permissions = single_view.permissions + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "c8f2773a-c83a-11e8-8c8f-33e6d787b506" + assert permissions[0].capabilities == { + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, + } + + +def test_add_permissions(server: TSC.Server) -> None: + response_xml = UPDATE_PERMISSIONS.read_text() + + single_view = TSC.ViewItem() + single_view._id = "21778de4-b7b9-44bc-a599-1506a2639ace" + + bob = UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a") + group_of_people = GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af") + + new_permissions = [PermissionsRule(bob, {"Write": "Allow"}), PermissionsRule(group_of_people, {"Read": "Deny"})] + + with requests_mock.mock() as m: + m.put(server.views.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) + permissions = server.views.update_permissions(single_view, new_permissions) + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "5e5e1978-71fa-11e4-87dd-7382f5c437af" + assert permissions[0].capabilities == {TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny} + + assert permissions[1].grantee.tag_name == "user" + assert permissions[1].grantee.id == "7c37ee24-c4b1-42b6-a154-eaeab7ee330a" + assert permissions[1].capabilities == {TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow} + + +def test_update_tags(server: TSC.Server) -> None: + add_tags_xml = ADD_TAGS_XML.read_text() + update_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/tags", text=add_tags_xml) + m.delete(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/tags/b", status_code=204) + m.delete(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/tags/d", status_code=204) + m.put(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5", text=update_xml) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + single_view._initial_tags.update(["a", "b", "c", "d"]) + single_view.tags.update(["a", "c", "e"]) + updated_view = server.views.update(single_view) + + assert single_view.tags == updated_view.tags + assert single_view._initial_tags == updated_view._initial_tags + + +def test_populate_excel(server: TSC.Server) -> None: + server.version = "3.8" + response = POPULATE_EXCEL.read_bytes() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/crosstab/excel?maxAge=1", content=response) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + request_option = TSC.ExcelRequestOptions(maxage=1) + server.views.populate_excel(single_view, request_option) + + excel_file = b"".join(single_view.excel) + assert response == excel_file + + +def test_filter_excel(server: TSC.Server) -> None: + server.version = "3.8" + response = POPULATE_EXCEL.read_bytes() + with requests_mock.mock() as m: + m.get(server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/crosstab/excel?maxAge=1", content=response) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + request_option = TSC.ExcelRequestOptions(maxage=1) + request_option.vf("stuff", "1") + server.views.populate_excel(single_view, request_option) + + excel_file = b"".join(single_view.excel) + assert response == excel_file + + +def test_pdf_height(server: TSC.Server) -> None: + server.version = "3.8" + response = POPULATE_PDF.read_bytes() + with requests_mock.mock() as m: + m.get( + server.views.baseurl + "/d79634e1-6063-4ec9-95ff-50acbf609ff5/pdf?vizHeight=1080&vizWidth=1920", + content=response, + ) + single_view = TSC.ViewItem() + single_view._id = "d79634e1-6063-4ec9-95ff-50acbf609ff5" + + req_option = TSC.PDFRequestOptions( + viz_height=1080, + viz_width=1920, + ) + + server.views.populate_pdf(single_view, req_option) + assert response == single_view.pdf + + +def test_pdf_errors(server: TSC.Server) -> None: + req_option = TSC.PDFRequestOptions(viz_height=1080) + with pytest.raises(ValueError): + req_option.get_query_params() + req_option = TSC.PDFRequestOptions(viz_width=1920) + with pytest.raises(ValueError): + req_option.get_query_params() + + +def test_view_get_all_fields(server: TSC.Server) -> None: + server.version = "3.21" + response_xml = GET_XML_ALL_FIELDS.read_text() + + ro = TSC.RequestOptions() + ro.all_fields = True + + with requests_mock.mock() as m: + m.get(f"{server.views.baseurl}?fields=_all_", text=response_xml) + views, _ = server.views.get(req_options=ro) + + assert views[0].id == "2bdcd787-dcc6-4a5d-bc61-2846f1ef4534" + assert views[0].name == "Overview" + assert views[0].content_url == "Superstore/sheets/Overview" + assert views[0].created_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[0].updated_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[0].sheet_type == "dashboard" + assert views[0].favorites_total == 0 + assert views[0].view_url_name == "Overview" + assert isinstance(views[0].workbook, TSC.WorkbookItem) + assert views[0].workbook.id == "9df3e2d1-070e-497a-9578-8cc557ced9df" + assert views[0].workbook.name == "Superstore" + assert views[0].workbook.content_url == "Superstore" + assert views[0].workbook.show_tabs + assert views[0].workbook.size == 2 + assert views[0].workbook.created_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[0].workbook.updated_at == parse_datetime("2024-02-14T04:42:10Z") + assert views[0].workbook.sheet_count == 9 + assert not views[0].workbook.has_extracts + assert isinstance(views[0].owner, TSC.UserItem) + assert views[0].owner.email == "bob@example.com" + assert views[0].owner.fullname == "Bob" + assert views[0].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert views[0].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert views[0].owner.name == "bob@example.com" + assert views[0].owner.site_role == "SiteAdministratorCreator" + assert isinstance(views[0].project, TSC.ProjectItem) + assert views[0].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert views[0].project.name == "Samples" + assert views[0].project.description == "This project includes automatically uploaded samples." + assert views[0].total_views == 0 + assert isinstance(views[0].location, TSC.LocationItem) + assert views[0].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert views[0].location.type == "Project" + assert views[1].id == "2a3fd19d-9129-413d-9ff7-9dfc36bf7f7e" + assert views[1].name == "Product" + assert views[1].content_url == "Superstore/sheets/Product" + assert views[1].created_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[1].updated_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[1].sheet_type == "dashboard" + assert views[1].favorites_total == 0 + assert views[1].view_url_name == "Product" + assert isinstance(views[1].workbook, TSC.WorkbookItem) + assert views[1].workbook.id == "9df3e2d1-070e-497a-9578-8cc557ced9df" + assert views[1].workbook.name == "Superstore" + assert views[1].workbook.content_url == "Superstore" + assert views[1].workbook.show_tabs + assert views[1].workbook.size == 2 + assert views[1].workbook.created_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[1].workbook.updated_at == parse_datetime("2024-02-14T04:42:10Z") + assert views[1].workbook.sheet_count == 9 + assert not views[1].workbook.has_extracts + assert isinstance(views[1].owner, TSC.UserItem) + assert views[1].owner.email == "bob@example.com" + assert views[1].owner.fullname == "Bob" + assert views[1].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert views[1].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert views[1].owner.name == "bob@example.com" + assert views[1].owner.site_role == "SiteAdministratorCreator" + assert isinstance(views[1].project, TSC.ProjectItem) + assert views[1].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert views[1].project.name == "Samples" + assert views[1].project.description == "This project includes automatically uploaded samples." + assert views[1].total_views == 0 + assert isinstance(views[1].location, TSC.LocationItem) + assert views[1].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert views[1].location.type == "Project" + assert views[2].id == "459eda9a-85e4-46bf-a2f2-62936bd2e99a" + assert views[2].name == "Customers" + assert views[2].content_url == "Superstore/sheets/Customers" + assert views[2].created_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[2].updated_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[2].sheet_type == "dashboard" + assert views[2].favorites_total == 0 + assert views[2].view_url_name == "Customers" + assert isinstance(views[2].workbook, TSC.WorkbookItem) + assert views[2].workbook.id == "9df3e2d1-070e-497a-9578-8cc557ced9df" + assert views[2].workbook.name == "Superstore" + assert views[2].workbook.content_url == "Superstore" + assert views[2].workbook.show_tabs + assert views[2].workbook.size == 2 + assert views[2].workbook.created_at == parse_datetime("2024-02-14T04:42:09Z") + assert views[2].workbook.updated_at == parse_datetime("2024-02-14T04:42:10Z") + assert views[2].workbook.sheet_count == 9 + assert not views[2].workbook.has_extracts + assert isinstance(views[2].owner, TSC.UserItem) + assert views[2].owner.email == "bob@example.com" + assert views[2].owner.fullname == "Bob" + assert views[2].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert views[2].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert views[2].owner.name == "bob@example.com" + assert views[2].owner.site_role == "SiteAdministratorCreator" + assert isinstance(views[2].project, TSC.ProjectItem) + assert views[2].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert views[2].project.name == "Samples" + assert views[2].project.description == "This project includes automatically uploaded samples." + assert views[2].total_views == 0 + assert isinstance(views[2].location, TSC.LocationItem) + assert views[2].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert views[2].location.type == "Project" + + +def make_view() -> TSC.ViewItem: + view = TSC.ViewItem() + view._id = "1234" + return view + + +@pytest.mark.parametrize("view", [make_view, "1234"]) +def test_delete_view(server: TSC.Server, view: TSC.ViewItem | str) -> None: + server.version = "3.27" + id_ = getattr(view, "id", view) + with requests_mock.mock() as m: + m.delete(f"{server.views.baseurl}/{id_}") + server.views.delete(view) + assert m.called + assert m.call_count == 1 diff --git a/test/test_view_acceleration.py b/test/test_view_acceleration.py index 766831b0a..cbd1dc194 100644 --- a/test/test_view_acceleration.py +++ b/test/test_view_acceleration.py @@ -1,119 +1,120 @@ -import os +from pathlib import Path import requests_mock -import unittest + +import pytest import tableauserverclient as TSC from tableauserverclient.datetime_helpers import format_datetime -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -GET_BY_ID_ACCELERATION_STATUS_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_by_id_acceleration_status.xml") -POPULATE_VIEWS_XML = os.path.join(TEST_ASSET_DIR, "workbook_populate_views.xml") -UPDATE_VIEWS_ACCELERATION_STATUS_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_views_acceleration_status.xml") -UPDATE_WORKBOOK_ACCELERATION_STATUS_XML = os.path.join(TEST_ASSET_DIR, "workbook_update_acceleration_status.xml") - - -class WorkbookTests(unittest.TestCase): - def setUp(self) -> None: - 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_get_by_id(self) -> None: - with open(GET_BY_ID_ACCELERATION_STATUS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", text=response_xml) - single_workbook = self.server.workbooks.get_by_id("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", single_workbook.id) - self.assertEqual("SafariSample", single_workbook.name) - self.assertEqual("SafariSample", single_workbook.content_url) - self.assertEqual("http://tableauserver/#/workbooks/2/views", single_workbook.webpage_url) - self.assertEqual(False, single_workbook.show_tabs) - self.assertEqual(26, single_workbook.size) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(single_workbook.created_at)) - self.assertEqual("description for SafariSample", single_workbook.description) - self.assertEqual("2016-07-26T20:35:05Z", format_datetime(single_workbook.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", single_workbook.project_id) - self.assertEqual("default", single_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_workbook.owner_id) - self.assertEqual({"Safari", "Sample"}, single_workbook.tags) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_workbook.views[0].id) - self.assertEqual("ENDANGERED SAFARI", single_workbook.views[0].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) - self.assertEqual(True, single_workbook.views[0].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Enabled", single_workbook.views[0].data_acceleration_config["acceleration_status"]) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff9", single_workbook.views[1].id) - self.assertEqual("ENDANGERED SAFARI 2", single_workbook.views[1].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI2", single_workbook.views[1].content_url) - self.assertEqual(False, single_workbook.views[1].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Suspended", single_workbook.views[1].data_acceleration_config["acceleration_status"]) - - def test_update_workbook_acceleration(self) -> None: - with open(UPDATE_WORKBOOK_ACCELERATION_STATUS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_acceleration_config = { - "acceleration_enabled": True, - "accelerate_now": False, - "last_updated_at": None, - "acceleration_status": None, - } - # update with parameter includeViewAccelerationStatus=True - single_workbook = self.server.workbooks.update(single_workbook, True) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", single_workbook.project_id) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) - self.assertEqual(True, single_workbook.views[0].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Pending", single_workbook.views[0].data_acceleration_config["acceleration_status"]) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff9", single_workbook.views[1].id) - self.assertEqual("ENDANGERED SAFARI 2", single_workbook.views[1].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI2", single_workbook.views[1].content_url) - self.assertEqual(True, single_workbook.views[1].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Pending", single_workbook.views[1].data_acceleration_config["acceleration_status"]) - - def test_update_views_acceleration(self) -> None: - with open(POPULATE_VIEWS_XML, "rb") as f: - views_xml = f.read().decode("utf-8") - with open(UPDATE_VIEWS_ACCELERATION_STATUS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/views", text=views_xml) - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.data_acceleration_config = { - "acceleration_enabled": False, - "accelerate_now": False, - "last_updated_at": None, - "acceleration_status": None, - } - self.server.workbooks.populate_views(single_workbook) - single_workbook.views = [single_workbook.views[1], single_workbook.views[2]] - # update with parameter includeViewAccelerationStatus=True - single_workbook = self.server.workbooks.update(single_workbook, True) - - views_list = single_workbook.views - self.assertEqual("097dbe13-de89-445f-b2c3-02f28bd010c1", views_list[0].id) - self.assertEqual("GDP per capita", views_list[0].name) - self.assertEqual(False, views_list[0].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Disabled", views_list[0].data_acceleration_config["acceleration_status"]) - - self.assertEqual("2c1ab9d7-8d64-4cc6-b495-52e40c60c330", views_list[1].id) - self.assertEqual("Country ranks", views_list[1].name) - self.assertEqual(True, views_list[1].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Pending", views_list[1].data_acceleration_config["acceleration_status"]) - - self.assertEqual("0599c28c-6d82-457e-a453-e52c1bdb00f5", views_list[2].id) - self.assertEqual("Interest rates", views_list[2].name) - self.assertEqual(True, views_list[2].data_acceleration_config["acceleration_enabled"]) - self.assertEqual("Pending", views_list[2].data_acceleration_config["acceleration_status"]) +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_BY_ID_ACCELERATION_STATUS_XML = TEST_ASSET_DIR / "workbook_get_by_id_acceleration_status.xml" +POPULATE_VIEWS_XML = TEST_ASSET_DIR / "workbook_populate_views.xml" +UPDATE_VIEWS_ACCELERATION_STATUS_XML = TEST_ASSET_DIR / "workbook_update_views_acceleration_status.xml" +UPDATE_WORKBOOK_ACCELERATION_STATUS_XML = TEST_ASSET_DIR / "workbook_update_acceleration_status.xml" + + +@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_get_by_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_ACCELERATION_STATUS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", text=response_xml) + single_workbook = server.workbooks.get_by_id("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == single_workbook.id + assert "SafariSample" == single_workbook.name + assert "SafariSample" == single_workbook.content_url + assert "http://tableauserver/#/workbooks/2/views" == single_workbook.webpage_url + assert single_workbook.show_tabs is False + assert 26 == single_workbook.size + assert "2016-07-26T20:34:56Z" == format_datetime(single_workbook.created_at) + assert "description for SafariSample" == single_workbook.description + assert "2016-07-26T20:35:05Z" == format_datetime(single_workbook.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == single_workbook.project_id + assert "default" == single_workbook.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == single_workbook.owner_id + assert {"Safari", "Sample"} == single_workbook.tags + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == single_workbook.views[0].id + assert "ENDANGERED SAFARI" == single_workbook.views[0].name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == single_workbook.views[0].content_url + assert single_workbook.views[0].data_acceleration_config["acceleration_enabled"] + assert "Enabled" == single_workbook.views[0].data_acceleration_config["acceleration_status"] + assert "d79634e1-6063-4ec9-95ff-50acbf609ff9" == single_workbook.views[1].id + assert "ENDANGERED SAFARI 2" == single_workbook.views[1].name + assert "SafariSample/sheets/ENDANGEREDSAFARI2" == single_workbook.views[1].content_url + assert single_workbook.views[1].data_acceleration_config["acceleration_enabled"] is False + assert "Suspended" == single_workbook.views[1].data_acceleration_config["acceleration_status"] + + +def test_update_workbook_acceleration(server: TSC.Server) -> None: + response_xml = UPDATE_WORKBOOK_ACCELERATION_STATUS_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_acceleration_config = { + "acceleration_enabled": True, + "accelerate_now": False, + "last_updated_at": None, + "acceleration_status": None, + } + # update with parameter includeViewAccelerationStatus=True + single_workbook = server.workbooks.update(single_workbook, True) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert "1d0304cd-3796-429f-b815-7258370b9b74" == single_workbook.project_id + assert "SafariSample/sheets/ENDANGEREDSAFARI" == single_workbook.views[0].content_url + assert single_workbook.views[0].data_acceleration_config["acceleration_enabled"] + assert "Pending" == single_workbook.views[0].data_acceleration_config["acceleration_status"] + assert "d79634e1-6063-4ec9-95ff-50acbf609ff9" == single_workbook.views[1].id + assert "ENDANGERED SAFARI 2" == single_workbook.views[1].name + assert "SafariSample/sheets/ENDANGEREDSAFARI2" == single_workbook.views[1].content_url + assert single_workbook.views[1].data_acceleration_config["acceleration_enabled"] + assert "Pending" == single_workbook.views[1].data_acceleration_config["acceleration_status"] + + +def test_update_views_acceleration(server: TSC.Server) -> None: + views_xml = POPULATE_VIEWS_XML.read_text() + response_xml = UPDATE_VIEWS_ACCELERATION_STATUS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/views", text=views_xml) + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.data_acceleration_config = { + "acceleration_enabled": False, + "accelerate_now": False, + "last_updated_at": None, + "acceleration_status": None, + } + server.workbooks.populate_views(single_workbook) + single_workbook.views = [single_workbook.views[1], single_workbook.views[2]] + # update with parameter includeViewAccelerationStatus=True + single_workbook = server.workbooks.update(single_workbook, True) + + views_list = single_workbook.views + assert "097dbe13-de89-445f-b2c3-02f28bd010c1" == views_list[0].id + assert "GDP per capita" == views_list[0].name + assert views_list[0].data_acceleration_config["acceleration_enabled"] is False + assert "Disabled" == views_list[0].data_acceleration_config["acceleration_status"] + + assert "2c1ab9d7-8d64-4cc6-b495-52e40c60c330" == views_list[1].id + assert "Country ranks" == views_list[1].name + assert views_list[1].data_acceleration_config["acceleration_enabled"] + assert "Pending" == views_list[1].data_acceleration_config["acceleration_status"] + + assert "0599c28c-6d82-457e-a453-e52c1bdb00f5" == views_list[2].id + assert "Interest rates" == views_list[2].name + assert views_list[2].data_acceleration_config["acceleration_enabled"] + assert "Pending" == views_list[2].data_acceleration_config["acceleration_status"] diff --git a/test/test_virtual_connection.py b/test/test_virtual_connection.py index 5d9a2d1bc..210f605c8 100644 --- a/test/test_virtual_connection.py +++ b/test/test_virtual_connection.py @@ -1,6 +1,5 @@ import json from pathlib import Path -import unittest import pytest import requests_mock @@ -22,227 +21,249 @@ ADD_PERMISSIONS = ASSET_DIR / "virtual_connection_add_permissions.xml" -class TestVirtualConnections(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test") - - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - self.server.version = "3.23" - - self.baseurl = f"{self.server.baseurl}/sites/{self.server.site_id}/virtualConnections" - return super().setUp() - - def test_from_xml(self): - items = VirtualConnectionItem.from_response(VIRTUAL_CONNECTION_GET_XML.read_bytes(), self.server.namespace) - - assert len(items) == 1 - virtual_connection = items[0] - assert virtual_connection.created_at == parse_datetime("2024-05-30T09:00:00Z") - assert not virtual_connection.has_extracts - assert virtual_connection.id == "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - assert virtual_connection.is_certified - assert virtual_connection.name == "vconn" - assert virtual_connection.updated_at == parse_datetime("2024-06-18T09:00:00Z") - assert virtual_connection.webpage_url == "https://test/#/site/site-name/virtualconnections/3" - - def test_virtual_connection_get(self): - with requests_mock.mock() as m: - m.get(self.baseurl, text=VIRTUAL_CONNECTION_GET_XML.read_text()) - items, pagination_item = self.server.virtual_connections.get() - - assert len(items) == 1 - assert pagination_item.total_available == 1 - assert items[0].name == "vconn" - - def test_virtual_connection_populate_connections(self): - for i, populate_connections_xml in enumerate( - (VIRTUAL_CONNECTION_POPULATE_CONNECTIONS, VIRTUAL_CONNECTION_POPULATE_CONNECTIONS2) - ): - with self.subTest(i): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{vconn.id}/connections", text=populate_connections_xml.read_text()) - vc_out = self.server.virtual_connections.populate_connections(vconn) - connection_list = list(vconn.connections) - - assert vc_out is vconn - assert vc_out._connections is not None - - assert len(connection_list) == 1 - connection = connection_list[0] - assert connection.id == "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" - assert connection.connection_type == "postgres" - assert connection.server_address == "localhost" - assert connection.server_port == "5432" - assert connection.username == "pgadmin" - - def test_virtual_connection_update_connection_db_connection(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - connection = TSC.ConnectionItem() - connection._id = "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" - connection.server_address = "localhost" - connection.server_port = "5432" - connection.username = "pgadmin" - connection.password = "password" - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{vconn.id}/connections/{connection.id}/modify", text=VC_DB_CONN_UPDATE.read_text()) - updated_connection = self.server.virtual_connections.update_connection_db_connection(vconn, connection) - - assert updated_connection.id == "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" - assert updated_connection.server_address == "localhost" - assert updated_connection.server_port == "5432" - assert updated_connection.username == "pgadmin" - assert updated_connection.password is None - - def test_virtual_connection_get_by_id(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{vconn.id}", text=VIRTUAL_CONNECTION_DOWNLOAD.read_text()) - vconn = self.server.virtual_connections.get_by_id(vconn) - - assert vconn.content - assert vconn.created_at is None - assert vconn.id is None - assert "policyCollection" in vconn.content - assert "revision" in vconn.content - - def test_virtual_connection_update(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - vconn.is_certified = True - vconn.certification_note = "demo certification note" - vconn.project_id = "5286d663-8668-4ac2-8c8d-91af7d585f6b" - vconn.owner_id = "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{vconn.id}", text=VIRTUAL_CONNECTION_UPDATE.read_text()) - vconn = self.server.virtual_connections.update(vconn) - - assert not vconn.has_extracts - assert vconn.id is None - assert vconn.is_certified - assert vconn.name == "testv1" - assert vconn.certification_note == "demo certification note" - assert vconn.project_id == "5286d663-8668-4ac2-8c8d-91af7d585f6b" - assert vconn.owner_id == "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" - - def test_virtual_connection_get_revisions(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{vconn.id}/revisions", text=VIRTUAL_CONNECTION_REVISIONS.read_text()) - revisions, pagination_item = self.server.virtual_connections.get_revisions(vconn) - - assert len(revisions) == 3 - assert pagination_item.total_available == 3 - assert revisions[0].resource_id == vconn.id - assert revisions[0].resource_name == vconn.name - assert revisions[0].created_at == parse_datetime("2016-07-26T20:34:56Z") - assert revisions[0].revision_number == "1" - assert not revisions[0].current - assert not revisions[0].deleted - assert revisions[0].user_name == "Cassie" - assert revisions[0].user_id == "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" - assert revisions[1].resource_id == vconn.id - assert revisions[1].resource_name == vconn.name - assert revisions[1].created_at == parse_datetime("2016-07-27T20:34:56Z") - assert revisions[1].revision_number == "2" - assert not revisions[1].current - assert not revisions[1].deleted - assert revisions[2].resource_id == vconn.id - assert revisions[2].resource_name == vconn.name - assert revisions[2].created_at == parse_datetime("2016-07-28T20:34:56Z") - assert revisions[2].revision_number == "3" - assert revisions[2].current - assert not revisions[2].deleted - assert revisions[2].user_name == "Cassie" - assert revisions[2].user_id == "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" - - def test_virtual_connection_download_revision(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{vconn.id}/revisions/1", text=VIRTUAL_CONNECTION_DOWNLOAD.read_text()) - content = self.server.virtual_connections.download_revision(vconn, 1) - - assert content - assert "policyCollection" in content - data = json.loads(content) - assert "policyCollection" in data - assert "revision" in data - - def test_virtual_connection_delete(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{vconn.id}") - self.server.virtual_connections.delete(vconn) - self.server.virtual_connections.delete(vconn.id) - - assert m.call_count == 2 - - def test_virtual_connection_publish(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - vconn.project_id = "9836791c-9468-40f0-b7f3-d10b9562a046" - vconn.owner_id = "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - with requests_mock.mock() as m: - m.post(f"{self.baseurl}?overwrite=false&publishAsDraft=false", text=VIRTUAL_CONNECTION_PUBLISH.read_text()) - vconn = self.server.virtual_connections.publish( - vconn, '{"test": 0}', mode="CreateNew", publish_as_draft=False - ) - - assert vconn.name == "vconn_test" - assert vconn.owner_id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert vconn.project_id == "9836791c-9468-40f0-b7f3-d10b9562a046" - assert vconn.content - assert "policyCollection" in vconn.content - assert "revision" in vconn.content - - def test_virtual_connection_publish_draft_overwrite(self): - vconn = VirtualConnectionItem("vconn") - vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" - vconn.project_id = "9836791c-9468-40f0-b7f3-d10b9562a046" - vconn.owner_id = "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - with requests_mock.mock() as m: - m.post(f"{self.baseurl}?overwrite=true&publishAsDraft=true", text=VIRTUAL_CONNECTION_PUBLISH.read_text()) - vconn = self.server.virtual_connections.publish( - vconn, '{"test": 0}', mode="Overwrite", publish_as_draft=True - ) - - assert vconn.name == "vconn_test" - assert vconn.owner_id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert vconn.project_id == "9836791c-9468-40f0-b7f3-d10b9562a046" - assert vconn.content - assert "policyCollection" in vconn.content - assert "revision" in vconn.content - - def test_add_permissions(self) -> None: - with open(ADD_PERMISSIONS, "rb") as f: - response_xml = f.read().decode("utf-8") - - single_virtual_connection = TSC.VirtualConnectionItem("test") - single_virtual_connection._id = "21778de4-b7b9-44bc-a599-1506a2639ace" - - bob = TSC.UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - group_of_people = TSC.GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af") - - new_permissions = [ - TSC.PermissionsRule(bob, {"Write": "Allow"}), - TSC.PermissionsRule(group_of_people, {"Read": "Deny"}), - ] - - with requests_mock.mock() as m: - m.put(self.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) - permissions = self.server.virtual_connections.add_permissions(single_virtual_connection, new_permissions) - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "5e5e1978-71fa-11e4-87dd-7382f5c437af") - self.assertDictEqual(permissions[0].capabilities, {TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny}) - - self.assertEqual(permissions[1].grantee.tag_name, "user") - self.assertEqual(permissions[1].grantee.id, "7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - self.assertDictEqual(permissions[1].capabilities, {TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow}) +@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" + server.version = "3.23" + + return server + + +def test_from_xml(server: TSC.Server) -> None: + items = VirtualConnectionItem.from_response(VIRTUAL_CONNECTION_GET_XML.read_bytes(), server.namespace) + + assert len(items) == 1 + virtual_connection = items[0] + assert virtual_connection.created_at == parse_datetime("2024-05-30T09:00:00Z") + assert not virtual_connection.has_extracts + assert virtual_connection.id == "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + assert virtual_connection.is_certified + assert virtual_connection.name == "vconn" + assert virtual_connection.updated_at == parse_datetime("2024-06-18T09:00:00Z") + assert virtual_connection.webpage_url == "https://test/#/site/site-name/virtualconnections/3" + + +def test_virtual_connection_get(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.virtual_connections.baseurl, text=VIRTUAL_CONNECTION_GET_XML.read_text()) + items, pagination_item = server.virtual_connections.get() + + assert len(items) == 1 + assert pagination_item.total_available == 1 + assert items[0].name == "vconn" + + +@pytest.mark.parametrize( + "populate_connections_xml", [VIRTUAL_CONNECTION_POPULATE_CONNECTIONS, VIRTUAL_CONNECTION_POPULATE_CONNECTIONS2] +) +def test_virtual_connection_populate_connections(server: TSC.Server, populate_connections_xml: Path) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + with requests_mock.mock() as m: + m.get(f"{server.virtual_connections.baseurl}/{vconn.id}/connections", text=populate_connections_xml.read_text()) + vc_out = server.virtual_connections.populate_connections(vconn) + connection_list = list(vconn.connections) + + assert vc_out is vconn + assert vc_out._connections is not None + + assert len(connection_list) == 1 + connection = connection_list[0] + assert connection.id == "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" + assert connection.connection_type == "postgres" + assert connection.server_address == "localhost" + assert connection.server_port == "5432" + assert connection.username == "pgadmin" + + +def test_virtual_connection_update_connection_db_connection(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + connection = TSC.ConnectionItem() + connection._id = "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" + connection.server_address = "localhost" + connection.server_port = "5432" + connection.username = "pgadmin" + connection.password = "password" + with requests_mock.mock() as m: + m.put( + f"{server.virtual_connections.baseurl}/{vconn.id}/connections/{connection.id}/modify", + text=VC_DB_CONN_UPDATE.read_text(), + ) + updated_connection = server.virtual_connections.update_connection_db_connection(vconn, connection) + + assert updated_connection.id == "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" + assert updated_connection.server_address == "localhost" + assert updated_connection.server_port == "5432" + assert updated_connection.username == "pgadmin" + assert updated_connection.password is None + + +def test_virtual_connection_get_by_id(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + with requests_mock.mock() as m: + m.get(f"{server.virtual_connections.baseurl}/{vconn.id}", text=VIRTUAL_CONNECTION_DOWNLOAD.read_text()) + vconn = server.virtual_connections.get_by_id(vconn) + + assert vconn.content + assert vconn.created_at is None + assert vconn.id is None + assert "policyCollection" in vconn.content + assert "revision" in vconn.content + + +def test_virtual_connection_update(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + vconn.is_certified = True + vconn.certification_note = "demo certification note" + vconn.project_id = "5286d663-8668-4ac2-8c8d-91af7d585f6b" + vconn.owner_id = "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" + with requests_mock.mock() as m: + m.put(f"{server.virtual_connections.baseurl}/{vconn.id}", text=VIRTUAL_CONNECTION_UPDATE.read_text()) + vconn = server.virtual_connections.update(vconn) + + assert not vconn.has_extracts + assert vconn.id is None + assert vconn.is_certified + assert vconn.name == "testv1" + assert vconn.certification_note == "demo certification note" + assert vconn.project_id == "5286d663-8668-4ac2-8c8d-91af7d585f6b" + assert vconn.owner_id == "9324cf6b-ba72-4b8e-b895-ac3f28d2f0e0" + + +def test_virtual_connection_get_revisions(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + with requests_mock.mock() as m: + m.get( + f"{server.virtual_connections.baseurl}/{vconn.id}/revisions", text=VIRTUAL_CONNECTION_REVISIONS.read_text() + ) + revisions, pagination_item = server.virtual_connections.get_revisions(vconn) + + assert len(revisions) == 3 + assert pagination_item.total_available == 3 + assert revisions[0].resource_id == vconn.id + assert revisions[0].resource_name == vconn.name + assert revisions[0].created_at == parse_datetime("2016-07-26T20:34:56Z") + assert revisions[0].revision_number == "1" + assert not revisions[0].current + assert not revisions[0].deleted + assert revisions[0].user_name == "Cassie" + assert revisions[0].user_id == "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" + assert revisions[1].resource_id == vconn.id + assert revisions[1].resource_name == vconn.name + assert revisions[1].created_at == parse_datetime("2016-07-27T20:34:56Z") + assert revisions[1].revision_number == "2" + assert not revisions[1].current + assert not revisions[1].deleted + assert revisions[2].resource_id == vconn.id + assert revisions[2].resource_name == vconn.name + assert revisions[2].created_at == parse_datetime("2016-07-28T20:34:56Z") + assert revisions[2].revision_number == "3" + assert revisions[2].current + assert not revisions[2].deleted + assert revisions[2].user_name == "Cassie" + assert revisions[2].user_id == "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" + + +def test_virtual_connection_download_revision(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + with requests_mock.mock() as m: + m.get( + f"{server.virtual_connections.baseurl}/{vconn.id}/revisions/1", text=VIRTUAL_CONNECTION_DOWNLOAD.read_text() + ) + content = server.virtual_connections.download_revision(vconn, 1) + + assert content + assert "policyCollection" in content + data = json.loads(content) + assert "policyCollection" in data + assert "revision" in data + + +def test_virtual_connection_delete(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + with requests_mock.mock() as m: + m.delete(f"{server.virtual_connections.baseurl}/{vconn.id}") + server.virtual_connections.delete(vconn) + server.virtual_connections.delete(vconn.id) + + assert m.call_count == 2 + + +def test_virtual_connection_publish(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + vconn.project_id = "9836791c-9468-40f0-b7f3-d10b9562a046" + vconn.owner_id = "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + with requests_mock.mock() as m: + m.post( + f"{server.virtual_connections.baseurl}?overwrite=false&publishAsDraft=false", + text=VIRTUAL_CONNECTION_PUBLISH.read_text(), + ) + vconn = server.virtual_connections.publish(vconn, '{"test": 0}', mode="CreateNew", publish_as_draft=False) + + assert vconn.name == "vconn_test" + assert vconn.owner_id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert vconn.project_id == "9836791c-9468-40f0-b7f3-d10b9562a046" + assert vconn.content + assert "policyCollection" in vconn.content + assert "revision" in vconn.content + + +def test_virtual_connection_publish_draft_overwrite(server: TSC.Server) -> None: + vconn = VirtualConnectionItem("vconn") + vconn._id = "8fd7cc02-bb55-4d15-b8b1-9650239efe79" + vconn.project_id = "9836791c-9468-40f0-b7f3-d10b9562a046" + vconn.owner_id = "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + with requests_mock.mock() as m: + m.post( + f"{server.virtual_connections.baseurl}?overwrite=true&publishAsDraft=true", + text=VIRTUAL_CONNECTION_PUBLISH.read_text(), + ) + vconn = server.virtual_connections.publish(vconn, '{"test": 0}', mode="Overwrite", publish_as_draft=True) + + assert vconn.name == "vconn_test" + assert vconn.owner_id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert vconn.project_id == "9836791c-9468-40f0-b7f3-d10b9562a046" + assert vconn.content + assert "policyCollection" in vconn.content + assert "revision" in vconn.content + + +def test_add_permissions(server: TSC.Server) -> None: + response_xml = ADD_PERMISSIONS.read_text() + + single_virtual_connection = TSC.VirtualConnectionItem("test") + single_virtual_connection._id = "21778de4-b7b9-44bc-a599-1506a2639ace" + + bob = TSC.UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a") + group_of_people = TSC.GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af") + + new_permissions = [ + TSC.PermissionsRule(bob, {"Write": "Allow"}), + TSC.PermissionsRule(group_of_people, {"Read": "Deny"}), + ] + + with requests_mock.mock() as m: + m.put( + server.virtual_connections.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml + ) + permissions = server.virtual_connections.add_permissions(single_virtual_connection, new_permissions) + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "5e5e1978-71fa-11e4-87dd-7382f5c437af" + assert permissions[0].capabilities == {TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny} + + assert permissions[1].grantee.tag_name == "user" + assert permissions[1].grantee.id == "7c37ee24-c4b1-42b6-a154-eaeab7ee330a" + assert permissions[1].capabilities == {TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow} diff --git a/test/test_webhook.py b/test/test_webhook.py index 5f26266b2..e0217e93f 100644 --- a/test/test_webhook.py +++ b/test/test_webhook.py @@ -1,84 +1,89 @@ -import os -import unittest +from pathlib import Path +import pytest import requests_mock import tableauserverclient as TSC from tableauserverclient.server import RequestFactory from tableauserverclient.models import WebhookItem -from ._utils import asset - -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -GET_XML = asset("webhook_get.xml") -CREATE_XML = asset("webhook_create.xml") -CREATE_REQUEST_XML = asset("webhook_create_request.xml") - - -class WebhookTests(unittest.TestCase): - def setUp(self) -> None: - self.server = TSC.Server("http://test", False) - self.server.version = "3.6" - - # Fake signin - self.server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" - self.server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" - - self.baseurl = self.server.webhooks.baseurl - - def test_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - webhooks, _ = self.server.webhooks.get() - self.assertEqual(len(webhooks), 1) - webhook = webhooks[0] - - self.assertEqual(webhook.url, "url") - self.assertEqual(webhook.event, "datasource-created") - self.assertEqual(webhook.owner_id, "webhook_owner_luid") - self.assertEqual(webhook.name, "webhook-name") - self.assertEqual(webhook.id, "webhook-id") - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.webhooks.get) - - def test_delete(self) -> None: - with requests_mock.mock() as m: - m.delete(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) - self.server.webhooks.delete("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - def test_delete_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.webhooks.delete, "") - - def test_test(self) -> None: - with requests_mock.mock() as m: - m.get(self.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760/test", status_code=200) - self.server.webhooks.test("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - - def test_create(self) -> None: - with open(CREATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) - webhook_model = TSC.WebhookItem() - webhook_model.name = "Test Webhook" - webhook_model.url = "https://ifttt.com/maker-url" - webhook_model.event = "datasource-created" - - new_webhook = self.server.webhooks.create(webhook_model) - - self.assertNotEqual(new_webhook.id, None) - - def test_request_factory(self): - with open(CREATE_REQUEST_XML, "rb") as f: - webhook_request_expected = f.read().decode("utf-8") - - webhook_item = WebhookItem() - webhook_item._set_values("webhook-id", "webhook-name", "url", "api-event-name", None) - webhook_request_actual = "{}\n".format(RequestFactory.Webhook.create_req(webhook_item).decode("utf-8")) - self.maxDiff = None - # windows does /r/n for linebreaks, remove the extra char if it is there - self.assertEqual(webhook_request_expected.replace("\r", ""), webhook_request_actual) + +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +GET_XML = TEST_ASSET_DIR / "webhook_get.xml" +CREATE_XML = TEST_ASSET_DIR / "webhook_create.xml" +CREATE_REQUEST_XML = TEST_ASSET_DIR / "webhook_create_request.xml" + + +@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" + server.version = "3.6" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.webhooks.baseurl, text=response_xml) + webhooks, _ = server.webhooks.get() + assert len(webhooks) == 1 + webhook = webhooks[0] + + assert webhook.url == "url" + assert webhook.event == "datasource-created" + assert webhook.owner_id == "webhook_owner_luid" + assert webhook.name == "webhook-name" + assert webhook.id == "webhook-id" + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.webhooks.get() + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.webhooks.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", status_code=204) + server.webhooks.delete("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + +def test_delete_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.webhooks.delete("") + + +def test_test(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get(server.webhooks.baseurl + "/ee8c6e70-43b6-11e6-af4f-f7b0d8e20760/test", status_code=200) + server.webhooks.test("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + + +def test_create(server: TSC.Server) -> None: + response_xml = CREATE_XML.read_text() + with requests_mock.mock() as m: + m.post(server.webhooks.baseurl, text=response_xml) + webhook_model = TSC.WebhookItem() + webhook_model.name = "Test Webhook" + webhook_model.url = "https://ifttt.com/maker-url" + webhook_model.event = "datasource-created" + + new_webhook = server.webhooks.create(webhook_model) + + assert new_webhook.id is not None + + +def test_request_factory(): + webhook_request_expected = CREATE_REQUEST_XML.read_text() + + webhook_item = WebhookItem() + webhook_item._set_values("webhook-id", "webhook-name", "url", "api-event-name", None) + webhook_request_actual = "{}\n".format(RequestFactory.Webhook.create_req(webhook_item).decode("utf-8")) + # windows does /r/n for linebreaks, remove the extra char if it is there + assert webhook_request_expected.replace("\r", "") == webhook_request_actual diff --git a/test/test_workbook.py b/test/test_workbook.py index 84afd7fcb..e6e807f89 100644 --- a/test/test_workbook.py +++ b/test/test_workbook.py @@ -2,7 +2,6 @@ import re import requests_mock import tempfile -import unittest from defusedxml.ElementTree import fromstring from io import BytesIO from pathlib import Path @@ -14,1071 +13,1123 @@ from tableauserverclient.models import UserItem, GroupItem, PermissionsRule from tableauserverclient.server.endpoint.exceptions import InternalServerError, UnsupportedAttributeError from tableauserverclient.server.request_factory import RequestFactory -from ._utils import asset - -TEST_ASSET_DIR = os.path.join(os.path.dirname(__file__), "assets") - -ADD_TAGS_XML = os.path.join(TEST_ASSET_DIR, "workbook_add_tags.xml") -GET_BY_ID_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_by_id.xml") -GET_BY_ID_XML_PERSONAL = os.path.join(TEST_ASSET_DIR, "workbook_get_by_id_personal.xml") -GET_EMPTY_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_empty.xml") -GET_INVALID_DATE_XML = os.path.join(TEST_ASSET_DIR, "workbook_get_invalid_date.xml") -GET_XML = os.path.join(TEST_ASSET_DIR, "workbook_get.xml") -GET_XML_ALL_FIELDS = os.path.join(TEST_ASSET_DIR, "workbook_get_all_fields.xml") -ODATA_XML = os.path.join(TEST_ASSET_DIR, "odata_connection.xml") -POPULATE_CONNECTIONS_XML = os.path.join(TEST_ASSET_DIR, "workbook_populate_connections.xml") -POPULATE_PDF = os.path.join(TEST_ASSET_DIR, "populate_pdf.pdf") -POPULATE_POWERPOINT = os.path.join(TEST_ASSET_DIR, "populate_powerpoint.pptx") -POPULATE_PERMISSIONS_XML = os.path.join(TEST_ASSET_DIR, "workbook_populate_permissions.xml") -POPULATE_PREVIEW_IMAGE = os.path.join(TEST_ASSET_DIR, "RESTAPISample Image.png") -POPULATE_VIEWS_XML = os.path.join(TEST_ASSET_DIR, "workbook_populate_views.xml") -POPULATE_VIEWS_USAGE_XML = os.path.join(TEST_ASSET_DIR, "workbook_populate_views_usage.xml") -PUBLISH_XML = os.path.join(TEST_ASSET_DIR, "workbook_publish.xml") -PUBLISH_ASYNC_XML = os.path.join(TEST_ASSET_DIR, "workbook_publish_async.xml") -REFRESH_XML = os.path.join(TEST_ASSET_DIR, "workbook_refresh.xml") -REVISION_XML = os.path.join(TEST_ASSET_DIR, "workbook_revision.xml") -UPDATE_XML = os.path.join(TEST_ASSET_DIR, "workbook_update.xml") -UPDATE_PERMISSIONS = os.path.join(TEST_ASSET_DIR, "workbook_update_permissions.xml") - - -class WorkbookTests(unittest.TestCase): - def setUp(self) -> None: - 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_get(self) -> None: - with open(GET_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_workbooks, pagination_item = self.server.workbooks.get() - - self.assertEqual(2, pagination_item.total_available) - self.assertEqual("6d13b0ca-043d-4d42-8c9d-3f3313ea3a00", all_workbooks[0].id) - self.assertEqual("Superstore", all_workbooks[0].name) - self.assertEqual("Superstore", all_workbooks[0].content_url) - self.assertEqual(False, all_workbooks[0].show_tabs) - self.assertEqual("http://tableauserver/#/workbooks/1/views", all_workbooks[0].webpage_url) - self.assertEqual(1, all_workbooks[0].size) - self.assertEqual("2016-08-03T20:34:04Z", format_datetime(all_workbooks[0].created_at)) - self.assertEqual("description for Superstore", all_workbooks[0].description) - self.assertEqual("2016-08-04T17:56:41Z", format_datetime(all_workbooks[0].updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_workbooks[0].project_id) - self.assertEqual("default", all_workbooks[0].project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_workbooks[0].owner_id) - - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", all_workbooks[1].id) - self.assertEqual("SafariSample", all_workbooks[1].name) - self.assertEqual("SafariSample", all_workbooks[1].content_url) - self.assertEqual("http://tableauserver/#/workbooks/2/views", all_workbooks[1].webpage_url) - self.assertEqual(False, all_workbooks[1].show_tabs) - self.assertEqual(26, all_workbooks[1].size) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(all_workbooks[1].created_at)) - self.assertEqual("description for SafariSample", all_workbooks[1].description) - self.assertEqual("2016-07-26T20:35:05Z", format_datetime(all_workbooks[1].updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", all_workbooks[1].project_id) - self.assertEqual("default", all_workbooks[1].project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", all_workbooks[1].owner_id) - self.assertEqual({"Safari", "Sample"}, all_workbooks[1].tags) - - def test_get_ignore_invalid_date(self) -> None: - with open(GET_INVALID_DATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_workbooks, pagination_item = self.server.workbooks.get() - self.assertEqual(None, format_datetime(all_workbooks[0].created_at)) - self.assertEqual("2016-08-04T17:56:41Z", format_datetime(all_workbooks[0].updated_at)) - - def test_get_before_signin(self) -> None: - self.server._auth_token = None - self.assertRaises(TSC.NotSignedInError, self.server.workbooks.get) - - def test_get_empty(self) -> None: - with open(GET_EMPTY_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl, text=response_xml) - all_workbooks, pagination_item = self.server.workbooks.get() - self.assertEqual(0, pagination_item.total_available) - self.assertEqual([], all_workbooks) +TEST_ASSET_DIR = Path(__file__).parent / "assets" + +ADD_TAGS_XML = TEST_ASSET_DIR / "workbook_add_tags.xml" +GET_BY_ID_XML = TEST_ASSET_DIR / "workbook_get_by_id.xml" +GET_BY_ID_XML_PERSONAL = TEST_ASSET_DIR / "workbook_get_by_id_personal.xml" +GET_EMPTY_XML = TEST_ASSET_DIR / "workbook_get_empty.xml" +GET_INVALID_DATE_XML = TEST_ASSET_DIR / "workbook_get_invalid_date.xml" +GET_XML = TEST_ASSET_DIR / "workbook_get.xml" +GET_XML_ALL_FIELDS = TEST_ASSET_DIR / "workbook_get_all_fields.xml" +ODATA_XML = TEST_ASSET_DIR / "odata_connection.xml" +POPULATE_CONNECTIONS_XML = TEST_ASSET_DIR / "workbook_populate_connections.xml" +POPULATE_PDF = TEST_ASSET_DIR / "populate_pdf.pdf" +POPULATE_POWERPOINT = TEST_ASSET_DIR / "populate_powerpoint.pptx" +POPULATE_PERMISSIONS_XML = TEST_ASSET_DIR / "workbook_populate_permissions.xml" +POPULATE_PREVIEW_IMAGE = TEST_ASSET_DIR / "RESTAPISample Image.png" +POPULATE_VIEWS_XML = TEST_ASSET_DIR / "workbook_populate_views.xml" +POPULATE_VIEWS_USAGE_XML = TEST_ASSET_DIR / "workbook_populate_views_usage.xml" +PUBLISH_XML = TEST_ASSET_DIR / "workbook_publish.xml" +PUBLISH_ASYNC_XML = TEST_ASSET_DIR / "workbook_publish_async.xml" +REFRESH_XML = TEST_ASSET_DIR / "workbook_refresh.xml" +REVISION_XML = TEST_ASSET_DIR / "workbook_revision.xml" +UPDATE_XML = TEST_ASSET_DIR / "workbook_update.xml" +UPDATE_PERMISSIONS = TEST_ASSET_DIR / "workbook_update_permissions.xml" +UPDATE_CONNECTIONS_XML = TEST_ASSET_DIR / "workbook_update_connections.xml" + + +@pytest.fixture(scope="function") +def server() -> TSC.Server: + server = TSC.Server("http://test", False) + + # Fake sign in + server._site_id = "dad65087-b08b-4603-af4e-2887b8aafc67" + server._auth_token = "j80k54ll2lfMZ0tv97mlPvvSCRyD0DOM" + + return server + + +def test_get(server: TSC.Server) -> None: + response_xml = GET_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl, text=response_xml) + all_workbooks, pagination_item = server.workbooks.get() + + assert 2 == pagination_item.total_available + assert "6d13b0ca-043d-4d42-8c9d-3f3313ea3a00" == all_workbooks[0].id + assert "Superstore" == all_workbooks[0].name + assert "Superstore" == all_workbooks[0].content_url + assert not all_workbooks[0].show_tabs + assert "http://tableauserver/#/workbooks/1/views" == all_workbooks[0].webpage_url + assert 1 == all_workbooks[0].size + assert "2016-08-03T20:34:04Z" == format_datetime(all_workbooks[0].created_at) + assert "description for Superstore" == all_workbooks[0].description + assert "2016-08-04T17:56:41Z" == format_datetime(all_workbooks[0].updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == all_workbooks[0].project_id + assert "default" == all_workbooks[0].project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_workbooks[0].owner_id + + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == all_workbooks[1].id + assert "SafariSample" == all_workbooks[1].name + assert "SafariSample" == all_workbooks[1].content_url + assert "http://tableauserver/#/workbooks/2/views" == all_workbooks[1].webpage_url + assert not all_workbooks[1].show_tabs + assert 26 == all_workbooks[1].size + assert "2016-07-26T20:34:56Z" == format_datetime(all_workbooks[1].created_at) + assert "description for SafariSample" == all_workbooks[1].description + assert "2016-07-26T20:35:05Z" == format_datetime(all_workbooks[1].updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == all_workbooks[1].project_id + assert "default" == all_workbooks[1].project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == all_workbooks[1].owner_id + assert {"Safari", "Sample"} == all_workbooks[1].tags + + +def test_get_ignore_invalid_date(server: TSC.Server) -> None: + response_xml = GET_INVALID_DATE_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl, text=response_xml) + all_workbooks, pagination_item = server.workbooks.get() + assert format_datetime(all_workbooks[0].created_at) is None + assert "2016-08-04T17:56:41Z" == format_datetime(all_workbooks[0].updated_at) + + +def test_get_before_signin(server: TSC.Server) -> None: + server._auth_token = None + with pytest.raises(TSC.NotSignedInError): + server.workbooks.get() + + +def test_get_empty(server: TSC.Server) -> None: + response_xml = GET_EMPTY_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl, text=response_xml) + all_workbooks, pagination_item = server.workbooks.get() + + assert 0 == pagination_item.total_available + assert [] == all_workbooks + + +def test_get_by_id(server: TSC.Server) -> None: + response_xml = GET_BY_ID_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", text=response_xml) + single_workbook = server.workbooks.get_by_id("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d42" == single_workbook.id + assert "SafariSample" == single_workbook.name + assert "SafariSample" == single_workbook.content_url + assert "http://tableauserver/#/workbooks/2/views" == single_workbook.webpage_url + assert not single_workbook.show_tabs + assert 26 == single_workbook.size + assert "2016-07-26T20:34:56Z" == format_datetime(single_workbook.created_at) + assert "description for SafariSample" == single_workbook.description + assert "2016-07-26T20:35:05Z" == format_datetime(single_workbook.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == single_workbook.project_id + assert "default" == single_workbook.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == single_workbook.owner_id + assert {"Safari", "Sample"} == single_workbook.tags + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == single_workbook.views[0].id + assert "ENDANGERED SAFARI" == single_workbook.views[0].name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == single_workbook.views[0].content_url + + +def test_get_by_id_personal(server: TSC.Server) -> None: + # workbooks in personal space don't have project_id or project_name + response_xml = GET_BY_ID_XML_PERSONAL.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d43", text=response_xml) + single_workbook = server.workbooks.get_by_id("3cc6cd06-89ce-4fdc-b935-5294135d6d43") + + assert "3cc6cd06-89ce-4fdc-b935-5294135d6d43" == single_workbook.id + assert "SafariSample" == single_workbook.name + assert "SafariSample" == single_workbook.content_url + assert "http://tableauserver/#/workbooks/2/views" == single_workbook.webpage_url + assert not single_workbook.show_tabs + assert 26 == single_workbook.size + assert "2016-07-26T20:34:56Z" == format_datetime(single_workbook.created_at) + assert "description for SafariSample" == single_workbook.description + assert "2016-07-26T20:35:05Z" == format_datetime(single_workbook.updated_at) + assert single_workbook.project_id + assert single_workbook.project_name is None + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == single_workbook.owner_id + assert {"Safari", "Sample"} == single_workbook.tags + assert "d79634e1-6063-4ec9-95ff-50acbf609ff5" == single_workbook.views[0].id + assert "ENDANGERED SAFARI" == single_workbook.views[0].name + assert "SafariSample/sheets/ENDANGEREDSAFARI" == single_workbook.views[0].content_url + + +def test_get_by_id_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.workbooks.get_by_id("") + + +def test_refresh_id(server: TSC.Server) -> None: + server.version = "2.8" + server.workbooks.baseurl + response_xml = REFRESH_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/refresh", + status_code=202, + text=response_xml, + ) + server.workbooks.refresh("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + +def test_refresh_object(server: TSC.Server) -> None: + server.version = "2.8" + server.workbooks.baseurl + workbook = TSC.WorkbookItem("") + workbook._id = "3cc6cd06-89ce-4fdc-b935-5294135d6d42" + response_xml = REFRESH_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/refresh", + status_code=202, + text=response_xml, + ) + server.workbooks.refresh(workbook) + + +def test_delete(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.delete(server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", status_code=204) + server.workbooks.delete("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + +def test_delete_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.workbooks.delete("") + + +def test_update(server: TSC.Server) -> None: + response_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" + single_workbook.name = "renamedWorkbook" + single_workbook.data_acceleration_config = { + "acceleration_enabled": True, + "accelerate_now": False, + "last_updated_at": None, + "acceleration_status": None, + } + single_workbook = server.workbooks.update(single_workbook) + + assert "1f951daf-4061-451a-9df1-69a8062664f2" == single_workbook.id + assert single_workbook.show_tabs + assert "1d0304cd-3796-429f-b815-7258370b9b74" == single_workbook.project_id + assert "dd2239f6-ddf1-4107-981a-4cf94e415794" == single_workbook.owner_id + assert "renamedWorkbook" == single_workbook.name + assert single_workbook.data_acceleration_config["acceleration_enabled"] + assert not single_workbook.data_acceleration_config["accelerate_now"] + + +def test_update_missing_id(server: TSC.Server) -> None: + single_workbook = TSC.WorkbookItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.workbooks.update(single_workbook) + + +def test_update_copy_fields(server: TSC.Server) -> None: + connection_xml = POPULATE_CONNECTIONS_XML.read_text() + update_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/connections", text=connection_xml) + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=update_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + server.workbooks.populate_connections(single_workbook) + updated_workbook = server.workbooks.update(single_workbook) + + assert single_workbook._connections == updated_workbook._connections + assert single_workbook._views == updated_workbook._views + assert single_workbook.tags == updated_workbook.tags + assert single_workbook._initial_tags == updated_workbook._initial_tags + assert single_workbook._preview_image == updated_workbook._preview_image + + +def test_update_tags(server: TSC.Server) -> None: + add_tags_xml = ADD_TAGS_XML.read_text() + update_xml = UPDATE_XML.read_text() + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/tags", text=add_tags_xml) + m.delete(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/tags/b", status_code=204) + m.delete(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/tags/d", status_code=204) + m.put(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=update_xml) + single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + single_workbook._initial_tags.update(["a", "b", "c", "d"]) + single_workbook.tags.update(["a", "c", "e"]) + updated_workbook = server.workbooks.update(single_workbook) + + assert single_workbook.tags == updated_workbook.tags + assert single_workbook._initial_tags == updated_workbook._initial_tags + + +def test_download(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", + headers={"Content-Disposition": 'name="tableau_workbook"; filename="RESTAPISample.twbx"'}, + ) + file_path = server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2") + assert os.path.exists(file_path) + os.remove(file_path) - def test_get_by_id(self) -> None: - with open(GET_BY_ID_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", text=response_xml) - single_workbook = self.server.workbooks.get_by_id("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d42", single_workbook.id) - self.assertEqual("SafariSample", single_workbook.name) - self.assertEqual("SafariSample", single_workbook.content_url) - self.assertEqual("http://tableauserver/#/workbooks/2/views", single_workbook.webpage_url) - self.assertEqual(False, single_workbook.show_tabs) - self.assertEqual(26, single_workbook.size) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(single_workbook.created_at)) - self.assertEqual("description for SafariSample", single_workbook.description) - self.assertEqual("2016-07-26T20:35:05Z", format_datetime(single_workbook.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", single_workbook.project_id) - self.assertEqual("default", single_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_workbook.owner_id) - self.assertEqual({"Safari", "Sample"}, single_workbook.tags) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_workbook.views[0].id) - self.assertEqual("ENDANGERED SAFARI", single_workbook.views[0].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) - - def test_get_by_id_personal(self) -> None: - # workbooks in personal space don't have project_id or project_name - with open(GET_BY_ID_XML_PERSONAL, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d43", text=response_xml) - single_workbook = self.server.workbooks.get_by_id("3cc6cd06-89ce-4fdc-b935-5294135d6d43") - - self.assertEqual("3cc6cd06-89ce-4fdc-b935-5294135d6d43", single_workbook.id) - self.assertEqual("SafariSample", single_workbook.name) - self.assertEqual("SafariSample", single_workbook.content_url) - self.assertEqual("http://tableauserver/#/workbooks/2/views", single_workbook.webpage_url) - self.assertEqual(False, single_workbook.show_tabs) - self.assertEqual(26, single_workbook.size) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(single_workbook.created_at)) - self.assertEqual("description for SafariSample", single_workbook.description) - self.assertEqual("2016-07-26T20:35:05Z", format_datetime(single_workbook.updated_at)) - self.assertTrue(single_workbook.project_id) - self.assertIsNone(single_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", single_workbook.owner_id) - self.assertEqual({"Safari", "Sample"}, single_workbook.tags) - self.assertEqual("d79634e1-6063-4ec9-95ff-50acbf609ff5", single_workbook.views[0].id) - self.assertEqual("ENDANGERED SAFARI", single_workbook.views[0].name) - self.assertEqual("SafariSample/sheets/ENDANGEREDSAFARI", single_workbook.views[0].content_url) - - def test_get_by_id_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.workbooks.get_by_id, "") - - def test_refresh_id(self) -> None: - self.server.version = "2.8" - self.baseurl = self.server.workbooks.baseurl - with open(REFRESH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/refresh", status_code=202, text=response_xml) - self.server.workbooks.refresh("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - - def test_refresh_object(self) -> None: - self.server.version = "2.8" - self.baseurl = self.server.workbooks.baseurl - workbook = TSC.WorkbookItem("") - workbook._id = "3cc6cd06-89ce-4fdc-b935-5294135d6d42" - with open(REFRESH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/refresh", status_code=202, text=response_xml) - self.server.workbooks.refresh(workbook) - def test_delete(self) -> None: +def test_download_object(server: TSC.Server) -> None: + with BytesIO() as file_object: with requests_mock.mock() as m: - m.delete(self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42", status_code=204) - self.server.workbooks.delete("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", + headers={"Content-Disposition": 'name="tableau_workbook"; filename="RESTAPISample.twbx"'}, + ) + file_path = server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2", filepath=file_object) + assert isinstance(file_path, BytesIO) - def test_delete_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.workbooks.delete, "") - def test_update(self) -> None: - with open(UPDATE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=response_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74", show_tabs=True) - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook.owner_id = "dd2239f6-ddf1-4107-981a-4cf94e415794" - single_workbook.name = "renamedWorkbook" - single_workbook.data_acceleration_config = { - "acceleration_enabled": True, - "accelerate_now": False, - "last_updated_at": None, - "acceleration_status": None, - } - single_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual("1f951daf-4061-451a-9df1-69a8062664f2", single_workbook.id) - self.assertEqual(True, single_workbook.show_tabs) - self.assertEqual("1d0304cd-3796-429f-b815-7258370b9b74", single_workbook.project_id) - self.assertEqual("dd2239f6-ddf1-4107-981a-4cf94e415794", single_workbook.owner_id) - self.assertEqual("renamedWorkbook", single_workbook.name) - self.assertEqual(True, single_workbook.data_acceleration_config["acceleration_enabled"]) - self.assertEqual(False, single_workbook.data_acceleration_config["accelerate_now"]) - - def test_update_missing_id(self) -> None: +def test_download_sanitizes_name(server: TSC.Server) -> None: + filename = "Name,With,Commas.twbx" + disposition = f'name="tableau_workbook"; filename="{filename}"' + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", + headers={"Content-Disposition": disposition}, + ) + file_path = server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2") + assert os.path.basename(file_path) == "NameWithCommas.twbx" + assert os.path.exists(file_path) + os.remove(file_path) + + +def test_download_extract_only(server: TSC.Server) -> None: + # Pretend we're 2.5 for 'extract_only' + server.version = "2.5" + server.workbooks.baseurl + + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content?includeExtract=False", + headers={"Content-Disposition": 'name="tableau_workbook"; filename="RESTAPISample.twbx"'}, + complete_qs=True, + ) + # Technically this shouldn't download a twbx, but we are interested in the qs, not the file + file_path = server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2", include_extract=False) + assert os.path.exists(file_path) + os.remove(file_path) + + +def test_download_missing_id(server: TSC.Server) -> None: + with pytest.raises(ValueError): + server.workbooks.download("") + + +def test_populate_views(server: TSC.Server) -> None: + response_xml = POPULATE_VIEWS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/views", text=response_xml) single_workbook = TSC.WorkbookItem("test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.workbooks.update, single_workbook) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + server.workbooks.populate_views(single_workbook) - def test_update_copy_fields(self) -> None: - with open(POPULATE_CONNECTIONS_XML, "rb") as f: - connection_xml = f.read().decode("utf-8") - with open(UPDATE_XML, "rb") as f: - update_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/connections", text=connection_xml) - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=update_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - self.server.workbooks.populate_connections(single_workbook) - updated_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual(single_workbook._connections, updated_workbook._connections) - self.assertEqual(single_workbook._views, updated_workbook._views) - self.assertEqual(single_workbook.tags, updated_workbook.tags) - self.assertEqual(single_workbook._initial_tags, updated_workbook._initial_tags) - self.assertEqual(single_workbook._preview_image, updated_workbook._preview_image) - - def test_update_tags(self) -> None: - with open(ADD_TAGS_XML, "rb") as f: - add_tags_xml = f.read().decode("utf-8") - with open(UPDATE_XML, "rb") as f: - update_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/tags", text=add_tags_xml) - m.delete(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/tags/b", status_code=204) - m.delete(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/tags/d", status_code=204) - m.put(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2", text=update_xml) - single_workbook = TSC.WorkbookItem("1d0304cd-3796-429f-b815-7258370b9b74") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - single_workbook._initial_tags.update(["a", "b", "c", "d"]) - single_workbook.tags.update(["a", "c", "e"]) - updated_workbook = self.server.workbooks.update(single_workbook) - - self.assertEqual(single_workbook.tags, updated_workbook.tags) - self.assertEqual(single_workbook._initial_tags, updated_workbook._initial_tags) - - def test_download(self) -> None: - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", - headers={"Content-Disposition": 'name="tableau_workbook"; filename="RESTAPISample.twbx"'}, - ) - file_path = self.server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2") - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) - - def test_download_object(self) -> None: - with BytesIO() as file_object: - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", - headers={"Content-Disposition": 'name="tableau_workbook"; filename="RESTAPISample.twbx"'}, - ) - file_path = self.server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2", filepath=file_object) - self.assertTrue(isinstance(file_path, BytesIO)) - - def test_download_sanitizes_name(self) -> None: - filename = "Name,With,Commas.twbx" - disposition = f'name="tableau_workbook"; filename="{filename}"' - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content", - headers={"Content-Disposition": disposition}, - ) - file_path = self.server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2") - self.assertEqual(os.path.basename(file_path), "NameWithCommas.twbx") - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) + views_list = single_workbook.views + assert "097dbe13-de89-445f-b2c3-02f28bd010c1" == views_list[0].id + assert "GDP per capita" == views_list[0].name + assert "RESTAPISample/sheets/GDPpercapita" == views_list[0].content_url - def test_download_extract_only(self) -> None: - # Pretend we're 2.5 for 'extract_only' - self.server.version = "2.5" - self.baseurl = self.server.workbooks.baseurl + assert "2c1ab9d7-8d64-4cc6-b495-52e40c60c330" == views_list[1].id + assert "Country ranks" == views_list[1].name + assert "RESTAPISample/sheets/Countryranks" == views_list[1].content_url - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/content?includeExtract=False", - headers={"Content-Disposition": 'name="tableau_workbook"; filename="RESTAPISample.twbx"'}, - complete_qs=True, - ) - # Technically this shouldn't download a twbx, but we are interested in the qs, not the file - file_path = self.server.workbooks.download("1f951daf-4061-451a-9df1-69a8062664f2", include_extract=False) - self.assertTrue(os.path.exists(file_path)) - os.remove(file_path) + assert "0599c28c-6d82-457e-a453-e52c1bdb00f5" == views_list[2].id + assert "Interest rates" == views_list[2].name + assert "RESTAPISample/sheets/Interestrates" == views_list[2].content_url - def test_download_missing_id(self) -> None: - self.assertRaises(ValueError, self.server.workbooks.download, "") - def test_populate_views(self) -> None: - with open(POPULATE_VIEWS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/views", text=response_xml) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - self.server.workbooks.populate_views(single_workbook) - - views_list = single_workbook.views - self.assertEqual("097dbe13-de89-445f-b2c3-02f28bd010c1", views_list[0].id) - self.assertEqual("GDP per capita", views_list[0].name) - self.assertEqual("RESTAPISample/sheets/GDPpercapita", views_list[0].content_url) - - self.assertEqual("2c1ab9d7-8d64-4cc6-b495-52e40c60c330", views_list[1].id) - self.assertEqual("Country ranks", views_list[1].name) - self.assertEqual("RESTAPISample/sheets/Countryranks", views_list[1].content_url) - - self.assertEqual("0599c28c-6d82-457e-a453-e52c1bdb00f5", views_list[2].id) - self.assertEqual("Interest rates", views_list[2].name) - self.assertEqual("RESTAPISample/sheets/Interestrates", views_list[2].content_url) - - def test_populate_views_with_usage(self) -> None: - with open(POPULATE_VIEWS_USAGE_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/views?includeUsageStatistics=true", - text=response_xml, - ) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - self.server.workbooks.populate_views(single_workbook, usage=True) - - views_list = single_workbook.views - self.assertEqual("097dbe13-de89-445f-b2c3-02f28bd010c1", views_list[0].id) - self.assertEqual(2, views_list[0].total_views) - self.assertEqual("2c1ab9d7-8d64-4cc6-b495-52e40c60c330", views_list[1].id) - self.assertEqual(37, views_list[1].total_views) - self.assertEqual("0599c28c-6d82-457e-a453-e52c1bdb00f5", views_list[2].id) - self.assertEqual(0, views_list[2].total_views) - - def test_populate_views_missing_id(self) -> None: +def test_populate_views_with_usage(server: TSC.Server) -> None: + response_xml = POPULATE_VIEWS_USAGE_XML.read_text() + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/views?includeUsageStatistics=true", + text=response_xml, + ) single_workbook = TSC.WorkbookItem("test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.workbooks.populate_views, single_workbook) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + server.workbooks.populate_views(single_workbook, usage=True) - def test_populate_connections(self) -> None: - with open(POPULATE_CONNECTIONS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/connections", text=response_xml) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - self.server.workbooks.populate_connections(single_workbook) - - self.assertEqual("37ca6ced-58d7-4dcf-99dc-f0a85223cbef", single_workbook.connections[0].id) - self.assertEqual("dataengine", single_workbook.connections[0].connection_type) - self.assertEqual("4506225a-0d32-4ab1-82d3-c24e85f7afba", single_workbook.connections[0].datasource_id) - self.assertEqual("World Indicators", single_workbook.connections[0].datasource_name) - - def test_populate_permissions(self) -> None: - with open(POPULATE_PERMISSIONS_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(self.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "21778de4-b7b9-44bc-a599-1506a2639ace" - - self.server.workbooks.populate_permissions(single_workbook) - permissions = single_workbook.permissions - - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "5e5e1978-71fa-11e4-87dd-7382f5c437af") - self.assertDictEqual( - permissions[0].capabilities, - { - TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, - }, - ) + views_list = single_workbook.views + assert "097dbe13-de89-445f-b2c3-02f28bd010c1" == views_list[0].id + assert 2 == views_list[0].total_views + assert "2c1ab9d7-8d64-4cc6-b495-52e40c60c330" == views_list[1].id + assert 37 == views_list[1].total_views + assert "0599c28c-6d82-457e-a453-e52c1bdb00f5" == views_list[2].id + assert 0 == views_list[2].total_views - self.assertEqual(permissions[1].grantee.tag_name, "user") - self.assertEqual(permissions[1].grantee.id, "7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - self.assertDictEqual( - permissions[1].capabilities, - { - TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow, - TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Deny, - TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Deny, - }, - ) - def test_add_permissions(self) -> None: - with open(UPDATE_PERMISSIONS, "rb") as f: - response_xml = f.read().decode("utf-8") +def test_populate_views_missing_id(server: TSC.Server) -> None: + single_workbook = TSC.WorkbookItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.workbooks.populate_views(single_workbook) + + +def test_populate_connections(server: TSC.Server) -> None: + response_xml = POPULATE_CONNECTIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/connections", text=response_xml) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + server.workbooks.populate_connections(single_workbook) + + assert "37ca6ced-58d7-4dcf-99dc-f0a85223cbef" == single_workbook.connections[0].id + assert "dataengine" == single_workbook.connections[0].connection_type + assert "4506225a-0d32-4ab1-82d3-c24e85f7afba" == single_workbook.connections[0].datasource_id + assert "World Indicators" == single_workbook.connections[0].datasource_name + +def test_populate_permissions(server: TSC.Server) -> None: + response_xml = POPULATE_PERMISSIONS_XML.read_text() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) single_workbook = TSC.WorkbookItem("test") single_workbook._id = "21778de4-b7b9-44bc-a599-1506a2639ace" - bob = UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - group_of_people = GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af") + server.workbooks.populate_permissions(single_workbook) + permissions = single_workbook.permissions + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "5e5e1978-71fa-11e4-87dd-7382f5c437af" + assert permissions[0].capabilities == { + TSC.Permission.Capability.WebAuthoring: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Read: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.Filter: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.AddComment: TSC.Permission.Mode.Allow, + } + + assert permissions[1].grantee.tag_name == "user" + assert permissions[1].grantee.id == "7c37ee24-c4b1-42b6-a154-eaeab7ee330a" + assert permissions[1].capabilities == { + TSC.Permission.Capability.ExportImage: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ShareView: TSC.Permission.Mode.Allow, + TSC.Permission.Capability.ExportData: TSC.Permission.Mode.Deny, + TSC.Permission.Capability.ViewComments: TSC.Permission.Mode.Deny, + } + + +def test_add_permissions(server: TSC.Server) -> None: + response_xml = UPDATE_PERMISSIONS.read_text() + + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "21778de4-b7b9-44bc-a599-1506a2639ace" + + bob = UserItem.as_reference("7c37ee24-c4b1-42b6-a154-eaeab7ee330a") + group_of_people = GroupItem.as_reference("5e5e1978-71fa-11e4-87dd-7382f5c437af") + + new_permissions = [PermissionsRule(bob, {"Write": "Allow"}), PermissionsRule(group_of_people, {"Read": "Deny"})] + + with requests_mock.mock() as m: + m.put(server.workbooks.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) + permissions = server.workbooks.update_permissions(single_workbook, new_permissions) + + assert permissions[0].grantee.tag_name == "group" + assert permissions[0].grantee.id == "5e5e1978-71fa-11e4-87dd-7382f5c437af" + assert permissions[0].capabilities == {TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny} + assert permissions[1].grantee.tag_name == "user" + assert permissions[1].grantee.id == "7c37ee24-c4b1-42b6-a154-eaeab7ee330a" + assert permissions[1].capabilities == {TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow} + + +def test_populate_connections_missing_id(server: TSC.Server) -> None: + single_workbook = TSC.WorkbookItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.workbooks.populate_connections(single_workbook) + + +def test_populate_pdf(server: TSC.Server) -> None: + server.version = "3.4" + server.workbooks.baseurl + response = POPULATE_PDF.read_bytes() + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape", + content=response, + ) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + + type = TSC.PDFRequestOptions.PageType.A5 + orientation = TSC.PDFRequestOptions.Orientation.Landscape + req_option = TSC.PDFRequestOptions(type, orientation) - new_permissions = [PermissionsRule(bob, {"Write": "Allow"}), PermissionsRule(group_of_people, {"Read": "Deny"})] + server.workbooks.populate_pdf(single_workbook, req_option) + assert response == single_workbook.pdf - with requests_mock.mock() as m: - m.put(self.baseurl + "/21778de4-b7b9-44bc-a599-1506a2639ace/permissions", text=response_xml) - permissions = self.server.workbooks.update_permissions(single_workbook, new_permissions) - self.assertEqual(permissions[0].grantee.tag_name, "group") - self.assertEqual(permissions[0].grantee.id, "5e5e1978-71fa-11e4-87dd-7382f5c437af") - self.assertDictEqual(permissions[0].capabilities, {TSC.Permission.Capability.Read: TSC.Permission.Mode.Deny}) +def test_populate_pdf_unsupported(server: TSC.Server) -> None: + server.version = "3.4" + server.workbooks.baseurl + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape", + content=b"", + ) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + + type = TSC.PDFRequestOptions.PageType.A5 + orientation = TSC.PDFRequestOptions.Orientation.Landscape + req_option = TSC.PDFRequestOptions(type, orientation) + req_option.vf("Region", "West") + + with pytest.raises(UnsupportedAttributeError): + server.workbooks.populate_pdf(single_workbook, req_option) + + +def test_populate_pdf_vf_dims(server: TSC.Server) -> None: + server.version = "3.23" + server.workbooks.baseurl + response = POPULATE_PDF.read_bytes() + with requests_mock.mock() as m: + m.get( + server.workbooks.baseurl + + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape&vf_Region=West&vizWidth=1920&vizHeight=1080", + content=response, + ) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + + type = TSC.PDFRequestOptions.PageType.A5 + orientation = TSC.PDFRequestOptions.Orientation.Landscape + req_option = TSC.PDFRequestOptions(type, orientation) + req_option.vf("Region", "West") + req_option.viz_width = 1920 + req_option.viz_height = 1080 + + server.workbooks.populate_pdf(single_workbook, req_option) + assert response == single_workbook.pdf - self.assertEqual(permissions[1].grantee.tag_name, "user") - self.assertEqual(permissions[1].grantee.id, "7c37ee24-c4b1-42b6-a154-eaeab7ee330a") - self.assertDictEqual(permissions[1].capabilities, {TSC.Permission.Capability.Write: TSC.Permission.Mode.Allow}) - def test_populate_connections_missing_id(self) -> None: +def test_populate_powerpoint(server: TSC.Server) -> None: + server.version = "3.8" + server.workbooks.baseurl + response = POPULATE_POWERPOINT.read_bytes() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint?maxAge=1", content=response) single_workbook = TSC.WorkbookItem("test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.workbooks.populate_connections, single_workbook) + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - def test_populate_pdf(self) -> None: - self.server.version = "3.4" - self.baseurl = self.server.workbooks.baseurl - with open(POPULATE_PDF, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape", - content=response, - ) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + ro = TSC.PPTXRequestOptions(maxage=1) - type = TSC.PDFRequestOptions.PageType.A5 - orientation = TSC.PDFRequestOptions.Orientation.Landscape - req_option = TSC.PDFRequestOptions(type, orientation) + server.workbooks.populate_powerpoint(single_workbook, ro) + assert response == single_workbook.powerpoint - self.server.workbooks.populate_pdf(single_workbook, req_option) - self.assertEqual(response, single_workbook.pdf) - def test_populate_pdf_unsupported(self) -> None: - self.server.version = "3.4" - self.baseurl = self.server.workbooks.baseurl - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape", - content=b"", - ) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - - type = TSC.PDFRequestOptions.PageType.A5 - orientation = TSC.PDFRequestOptions.Orientation.Landscape - req_option = TSC.PDFRequestOptions(type, orientation) - req_option.vf("Region", "West") - - with self.assertRaises(UnsupportedAttributeError): - self.server.workbooks.populate_pdf(single_workbook, req_option) - - def test_populate_pdf_vf_dims(self) -> None: - self.server.version = "3.23" - self.baseurl = self.server.workbooks.baseurl - with open(POPULATE_PDF, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl - + "/1f951daf-4061-451a-9df1-69a8062664f2/pdf?type=a5&orientation=landscape&vf_Region=West&vizWidth=1920&vizHeight=1080", - content=response, - ) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - - type = TSC.PDFRequestOptions.PageType.A5 - orientation = TSC.PDFRequestOptions.Orientation.Landscape - req_option = TSC.PDFRequestOptions(type, orientation) - req_option.vf("Region", "West") - req_option.viz_width = 1920 - req_option.viz_height = 1080 - - self.server.workbooks.populate_pdf(single_workbook, req_option) - self.assertEqual(response, single_workbook.pdf) - - def test_populate_powerpoint(self) -> None: - self.server.version = "3.8" - self.baseurl = self.server.workbooks.baseurl - with open(POPULATE_POWERPOINT, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get( - self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/powerpoint?maxAge=1", - content=response, - ) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" +def test_populate_preview_image(server: TSC.Server) -> None: + response = POPULATE_PREVIEW_IMAGE.read_bytes() + with requests_mock.mock() as m: + m.get(server.workbooks.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/previewImage", content=response) + single_workbook = TSC.WorkbookItem("test") + single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" + server.workbooks.populate_preview_image(single_workbook) - ro = TSC.PPTXRequestOptions(maxage=1) + assert response == single_workbook.preview_image - self.server.workbooks.populate_powerpoint(single_workbook, ro) - self.assertEqual(response, single_workbook.powerpoint) - def test_populate_preview_image(self) -> None: - with open(POPULATE_PREVIEW_IMAGE, "rb") as f: - response = f.read() - with requests_mock.mock() as m: - m.get(self.baseurl + "/1f951daf-4061-451a-9df1-69a8062664f2/previewImage", content=response) - single_workbook = TSC.WorkbookItem("test") - single_workbook._id = "1f951daf-4061-451a-9df1-69a8062664f2" - self.server.workbooks.populate_preview_image(single_workbook) +def test_populate_preview_image_missing_id(server: TSC.Server) -> None: + single_workbook = TSC.WorkbookItem("test") + with pytest.raises(TSC.MissingRequiredFieldError): + server.workbooks.populate_preview_image(single_workbook) - self.assertEqual(response, single_workbook.preview_image) - def test_populate_preview_image_missing_id(self) -> None: - single_workbook = TSC.WorkbookItem("test") - self.assertRaises(TSC.MissingRequiredFieldError, self.server.workbooks.populate_preview_image, single_workbook) +def test_publish(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) - def test_publish(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) + new_workbook = TSC.WorkbookItem( + name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + ) - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) + new_workbook.description = "REST API Testing" + + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + publish_mode = server.PublishMode.CreateNew + + new_workbook = server.workbooks.publish(new_workbook, sample_workbook, publish_mode) + assert "a8076ca1-e9d8-495e-bae6-c684dbb55836" == new_workbook.id + assert "RESTAPISample" == new_workbook.name + assert "RESTAPISample_0" == new_workbook.content_url + assert not new_workbook.show_tabs + assert 1 == new_workbook.size + assert "2016-08-18T18:33:24Z" == format_datetime(new_workbook.created_at) + assert "2016-08-18T20:31:34Z" == format_datetime(new_workbook.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_workbook.project_id + assert "default" == new_workbook.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_workbook.owner_id + assert "fe0b4e89-73f4-435e-952d-3a263fbfa56c" == new_workbook.views[0].id + assert "GDP per capita" == new_workbook.views[0].name + assert "RESTAPISample_0/sheets/GDPpercapita" == new_workbook.views[0].content_url + assert "REST API Testing" == new_workbook.description + + +def test_publish_a_packaged_file_object(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) - new_workbook.description = "REST API Testing" - - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - publish_mode = self.server.PublishMode.CreateNew - - new_workbook = self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode) - - self.assertEqual("a8076ca1-e9d8-495e-bae6-c684dbb55836", new_workbook.id) - self.assertEqual("RESTAPISample", new_workbook.name) - self.assertEqual("RESTAPISample_0", new_workbook.content_url) - self.assertEqual(False, new_workbook.show_tabs) - self.assertEqual(1, new_workbook.size) - self.assertEqual("2016-08-18T18:33:24Z", format_datetime(new_workbook.created_at)) - self.assertEqual("2016-08-18T20:31:34Z", format_datetime(new_workbook.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_workbook.project_id) - self.assertEqual("default", new_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_workbook.owner_id) - self.assertEqual("fe0b4e89-73f4-435e-952d-3a263fbfa56c", new_workbook.views[0].id) - self.assertEqual("GDP per capita", new_workbook.views[0].name) - self.assertEqual("RESTAPISample_0/sheets/GDPpercapita", new_workbook.views[0].content_url) - self.assertEqual("REST API Testing", new_workbook.description) - - def test_publish_a_packaged_file_object(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) + new_workbook = TSC.WorkbookItem( + name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + ) - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - - with open(sample_workbook, "rb") as fp: - publish_mode = self.server.PublishMode.CreateNew - - new_workbook = self.server.workbooks.publish(new_workbook, fp, publish_mode) - - self.assertEqual("a8076ca1-e9d8-495e-bae6-c684dbb55836", new_workbook.id) - self.assertEqual("RESTAPISample", new_workbook.name) - self.assertEqual("RESTAPISample_0", new_workbook.content_url) - self.assertEqual(False, new_workbook.show_tabs) - self.assertEqual(1, new_workbook.size) - self.assertEqual("2016-08-18T18:33:24Z", format_datetime(new_workbook.created_at)) - self.assertEqual("2016-08-18T20:31:34Z", format_datetime(new_workbook.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_workbook.project_id) - self.assertEqual("default", new_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_workbook.owner_id) - self.assertEqual("fe0b4e89-73f4-435e-952d-3a263fbfa56c", new_workbook.views[0].id) - self.assertEqual("GDP per capita", new_workbook.views[0].name) - self.assertEqual("RESTAPISample_0/sheets/GDPpercapita", new_workbook.views[0].content_url) - - def test_publish_non_packeged_file_object(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) + with open(sample_workbook, "rb") as fp: + publish_mode = server.PublishMode.CreateNew - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) + new_workbook = server.workbooks.publish(new_workbook, fp, publish_mode) - sample_workbook = os.path.join(TEST_ASSET_DIR, "RESTAPISample.twb") - - with open(sample_workbook, "rb") as fp: - publish_mode = self.server.PublishMode.CreateNew - - new_workbook = self.server.workbooks.publish(new_workbook, fp, publish_mode) - - self.assertEqual("a8076ca1-e9d8-495e-bae6-c684dbb55836", new_workbook.id) - self.assertEqual("RESTAPISample", new_workbook.name) - self.assertEqual("RESTAPISample_0", new_workbook.content_url) - self.assertEqual(False, new_workbook.show_tabs) - self.assertEqual(1, new_workbook.size) - self.assertEqual("2016-08-18T18:33:24Z", format_datetime(new_workbook.created_at)) - self.assertEqual("2016-08-18T20:31:34Z", format_datetime(new_workbook.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_workbook.project_id) - self.assertEqual("default", new_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_workbook.owner_id) - self.assertEqual("fe0b4e89-73f4-435e-952d-3a263fbfa56c", new_workbook.views[0].id) - self.assertEqual("GDP per capita", new_workbook.views[0].name) - self.assertEqual("RESTAPISample_0/sheets/GDPpercapita", new_workbook.views[0].content_url) - - def test_publish_path_object(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) + assert "a8076ca1-e9d8-495e-bae6-c684dbb55836" == new_workbook.id + assert "RESTAPISample" == new_workbook.name + assert "RESTAPISample_0" == new_workbook.content_url + assert not new_workbook.show_tabs + assert 1 == new_workbook.size + assert "2016-08-18T18:33:24Z" == format_datetime(new_workbook.created_at) + assert "2016-08-18T20:31:34Z" == format_datetime(new_workbook.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_workbook.project_id + assert "default" == new_workbook.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_workbook.owner_id + assert "fe0b4e89-73f4-435e-952d-3a263fbfa56c" == new_workbook.views[0].id + assert "GDP per capita" == new_workbook.views[0].name + assert "RESTAPISample_0/sheets/GDPpercapita" == new_workbook.views[0].content_url - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) - sample_workbook = Path(TEST_ASSET_DIR) / "SampleWB.twbx" - publish_mode = self.server.PublishMode.CreateNew - - new_workbook = self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode) - - self.assertEqual("a8076ca1-e9d8-495e-bae6-c684dbb55836", new_workbook.id) - self.assertEqual("RESTAPISample", new_workbook.name) - self.assertEqual("RESTAPISample_0", new_workbook.content_url) - self.assertEqual(False, new_workbook.show_tabs) - self.assertEqual(1, new_workbook.size) - self.assertEqual("2016-08-18T18:33:24Z", format_datetime(new_workbook.created_at)) - self.assertEqual("2016-08-18T20:31:34Z", format_datetime(new_workbook.updated_at)) - self.assertEqual("ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", new_workbook.project_id) - self.assertEqual("default", new_workbook.project_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", new_workbook.owner_id) - self.assertEqual("fe0b4e89-73f4-435e-952d-3a263fbfa56c", new_workbook.views[0].id) - self.assertEqual("GDP per capita", new_workbook.views[0].name) - self.assertEqual("RESTAPISample_0/sheets/GDPpercapita", new_workbook.views[0].content_url) - - def test_publish_with_hidden_views_on_workbook(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) +def test_publish_non_packeged_file_object(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) + new_workbook = TSC.WorkbookItem( + name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + ) - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - publish_mode = self.server.PublishMode.CreateNew + sample_workbook = os.path.join(TEST_ASSET_DIR, "RESTAPISample.twb") - new_workbook.hidden_views = ["GDP per capita"] - new_workbook = self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode) - request_body = m._adapter.request_history[0]._request.body - # order of attributes in xml is unspecified - self.assertTrue(re.search(rb"<\/views>", request_body)) - self.assertTrue(re.search(rb"<\/views>", request_body)) + with open(sample_workbook, "rb") as fp: + publish_mode = server.PublishMode.CreateNew - def test_publish_with_thumbnails_user_id(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) + new_workbook = server.workbooks.publish(new_workbook, fp, publish_mode) - new_workbook = TSC.WorkbookItem( - name="Sample", - show_tabs=False, - project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", - thumbnails_user_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20761", - ) + assert "a8076ca1-e9d8-495e-bae6-c684dbb55836" == new_workbook.id + assert "RESTAPISample" == new_workbook.name + assert "RESTAPISample_0" == new_workbook.content_url + assert not new_workbook.show_tabs + assert 1 == new_workbook.size + assert "2016-08-18T18:33:24Z" == format_datetime(new_workbook.created_at) + assert "2016-08-18T20:31:34Z" == format_datetime(new_workbook.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_workbook.project_id + assert "default" == new_workbook.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_workbook.owner_id + assert "fe0b4e89-73f4-435e-952d-3a263fbfa56c" == new_workbook.views[0].id + assert "GDP per capita" == new_workbook.views[0].name + assert "RESTAPISample_0/sheets/GDPpercapita" == new_workbook.views[0].content_url - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - publish_mode = self.server.PublishMode.CreateNew - new_workbook = self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode) - request_body = m._adapter.request_history[0]._request.body - # order of attributes in xml is unspecified - self.assertTrue(re.search(rb"thumbnailsUserId=\"ee8c6e70-43b6-11e6-af4f-f7b0d8e20761\"", request_body)) - def test_publish_with_thumbnails_group_id(self) -> None: - with open(PUBLISH_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) +def test_publish_path_object(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) - new_workbook = TSC.WorkbookItem( - name="Sample", - show_tabs=False, - project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", - thumbnails_group_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20762", - ) + new_workbook = TSC.WorkbookItem( + name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + ) - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - publish_mode = self.server.PublishMode.CreateNew - new_workbook = self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode) - request_body = m._adapter.request_history[0]._request.body - self.assertTrue(re.search(rb"thumbnailsGroupId=\"ee8c6e70-43b6-11e6-af4f-f7b0d8e20762\"", request_body)) + sample_workbook = Path(TEST_ASSET_DIR) / "SampleWB.twbx" + publish_mode = server.PublishMode.CreateNew - @pytest.mark.filterwarnings("ignore:'as_job' not available") - def test_publish_with_query_params(self) -> None: - with open(PUBLISH_ASYNC_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(self.baseurl, text=response_xml) + new_workbook = server.workbooks.publish(new_workbook, sample_workbook, publish_mode) - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) + assert "a8076ca1-e9d8-495e-bae6-c684dbb55836" == new_workbook.id + assert "RESTAPISample" == new_workbook.name + assert "RESTAPISample_0" == new_workbook.content_url + assert not new_workbook.show_tabs + assert 1 == new_workbook.size + assert "2016-08-18T18:33:24Z" == format_datetime(new_workbook.created_at) + assert "2016-08-18T20:31:34Z" == format_datetime(new_workbook.updated_at) + assert "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" == new_workbook.project_id + assert "default" == new_workbook.project_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == new_workbook.owner_id + assert "fe0b4e89-73f4-435e-952d-3a263fbfa56c" == new_workbook.views[0].id + assert "GDP per capita" == new_workbook.views[0].name + assert "RESTAPISample_0/sheets/GDPpercapita" == new_workbook.views[0].content_url - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - publish_mode = self.server.PublishMode.CreateNew - self.server.workbooks.publish( - new_workbook, sample_workbook, publish_mode, as_job=True, skip_connection_check=True - ) +def test_publish_with_hidden_views_on_workbook(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) - request_query_params = m._adapter.request_history[0].qs - self.assertTrue("asjob" in request_query_params) - self.assertTrue(request_query_params["asjob"]) - self.assertTrue("skipconnectioncheck" in request_query_params) - self.assertTrue(request_query_params["skipconnectioncheck"]) - - def test_publish_async(self) -> None: - self.server.version = "3.0" - baseurl = self.server.workbooks.baseurl - with open(PUBLISH_ASYNC_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post(baseurl, text=response_xml) + new_workbook = TSC.WorkbookItem( + name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" + ) + + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + publish_mode = server.PublishMode.CreateNew + + new_workbook.hidden_views = ["GDP per capita"] + new_workbook = server.workbooks.publish(new_workbook, sample_workbook, publish_mode) + request_body = m._adapter.request_history[0]._request.body + # order of attributes in xml is unspecified + assert re.search(b'<\\/views>', request_body) + assert re.search(b'<\\/views>', request_body) - new_workbook = TSC.WorkbookItem( - name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" - ) - sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") - publish_mode = self.server.PublishMode.CreateNew - - new_job = self.server.workbooks.publish(new_workbook, sample_workbook, publish_mode, as_job=True) - - self.assertEqual("7c3d599e-949f-44c3-94a1-f30ba85757e4", new_job.id) - self.assertEqual("PublishWorkbook", new_job.type) - self.assertEqual("0", new_job.progress) - self.assertEqual("2018-06-29T23:22:32Z", format_datetime(new_job.created_at)) - self.assertEqual(1, new_job.finish_code) - - def test_publish_invalid_file(self) -> None: - new_workbook = TSC.WorkbookItem("test", "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - self.assertRaises(IOError, self.server.workbooks.publish, new_workbook, ".", self.server.PublishMode.CreateNew) - - def test_publish_invalid_file_type(self) -> None: - new_workbook = TSC.WorkbookItem("test", "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") - self.assertRaises( - ValueError, - self.server.workbooks.publish, - new_workbook, - os.path.join(TEST_ASSET_DIR, "SampleDS.tds"), - self.server.PublishMode.CreateNew, +def test_publish_with_thumbnails_user_id(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) + + new_workbook = TSC.WorkbookItem( + name="Sample", + show_tabs=False, + project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", + thumbnails_user_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20761", ) - def test_publish_unnamed_file_object(self) -> None: - new_workbook = TSC.WorkbookItem("test") + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + publish_mode = server.PublishMode.CreateNew + new_workbook = server.workbooks.publish(new_workbook, sample_workbook, publish_mode) + request_body = m._adapter.request_history[0]._request.body + # order of attributes in xml is unspecified + assert re.search(b'thumbnailsUserId=\\"ee8c6e70-43b6-11e6-af4f-f7b0d8e20761\\"', request_body) - with open(os.path.join(TEST_ASSET_DIR, "SampleWB.twbx"), "rb") as f: - self.assertRaises( - ValueError, self.server.workbooks.publish, new_workbook, f, self.server.PublishMode.CreateNew - ) - def test_publish_non_bytes_file_object(self) -> None: - new_workbook = TSC.WorkbookItem("test") +def test_publish_with_thumbnails_group_id(server: TSC.Server) -> None: + response_xml = PUBLISH_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) - with open(os.path.join(TEST_ASSET_DIR, "SampleWB.twbx")) as f: - self.assertRaises( - TypeError, self.server.workbooks.publish, new_workbook, f, self.server.PublishMode.CreateNew - ) + new_workbook = TSC.WorkbookItem( + name="Sample", + show_tabs=False, + project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760", + thumbnails_group_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20762", + ) + + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + publish_mode = server.PublishMode.CreateNew + new_workbook = server.workbooks.publish(new_workbook, sample_workbook, publish_mode) + request_body = m._adapter.request_history[0]._request.body + assert re.search(b'thumbnailsGroupId=\\"ee8c6e70-43b6-11e6-af4f-f7b0d8e20762\\"', request_body) - def test_publish_file_object_of_unknown_type_raises_exception(self) -> None: - new_workbook = TSC.WorkbookItem("test") - with BytesIO() as file_object: - file_object.write(bytes.fromhex("89504E470D0A1A0A")) - file_object.seek(0) - self.assertRaises( - ValueError, self.server.workbooks.publish, new_workbook, file_object, self.server.PublishMode.CreateNew - ) - def test_publish_multi_connection(self) -> None: +@pytest.mark.filterwarnings("ignore:'as_job' not available") +def test_publish_with_query_params(server: TSC.Server) -> None: + response_xml = PUBLISH_ASYNC_XML.read_text() + with requests_mock.mock() as m: + m.post(server.workbooks.baseurl, text=response_xml) + new_workbook = TSC.WorkbookItem( name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" ) - connection1 = TSC.ConnectionItem() - connection1.server_address = "mysql.test.com" - connection1.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) - connection2 = TSC.ConnectionItem() - connection2.server_address = "pgsql.test.com" - connection2.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) - - response = RequestFactory.Workbook._generate_xml(new_workbook, connections=[connection1, connection2]) - # Can't use ConnectionItem parser due to xml namespace problems - connection_results = fromstring(response).findall(".//connection") - - self.assertEqual(connection_results[0].get("serverAddress", None), "mysql.test.com") - self.assertEqual(connection_results[0].find("connectionCredentials").get("name", None), "test") # type: ignore[union-attr] - self.assertEqual(connection_results[1].get("serverAddress", None), "pgsql.test.com") - self.assertEqual(connection_results[1].find("connectionCredentials").get("password", None), "secret") # type: ignore[union-attr] - - def test_publish_multi_connection_flat(self) -> None: + + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + publish_mode = server.PublishMode.CreateNew + + server.workbooks.publish(new_workbook, sample_workbook, publish_mode, as_job=True, skip_connection_check=True) + + request_query_params = m._adapter.request_history[0].qs + assert "asjob" in request_query_params + assert request_query_params["asjob"] + assert "skipconnectioncheck" in request_query_params + assert request_query_params["skipconnectioncheck"] + + +def test_publish_async(server: TSC.Server) -> None: + server.version = "3.0" + baseurl = server.workbooks.baseurl + response_xml = PUBLISH_ASYNC_XML.read_text() + with requests_mock.mock() as m: + m.post(baseurl, text=response_xml) + new_workbook = TSC.WorkbookItem( name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760" ) - connection1 = TSC.ConnectionItem() - connection1.server_address = "mysql.test.com" - connection1.username = "test" - connection1.password = "secret" - connection1.embed_password = True - connection2 = TSC.ConnectionItem() - connection2.server_address = "pgsql.test.com" - connection2.username = "test" - connection2.password = "secret" - connection2.embed_password = True - - response = RequestFactory.Workbook._generate_xml(new_workbook, connections=[connection1, connection2]) - # Can't use ConnectionItem parser due to xml namespace problems - connection_results = fromstring(response).findall(".//connection") - - self.assertEqual(connection_results[0].get("serverAddress", None), "mysql.test.com") - self.assertEqual(connection_results[0].find("connectionCredentials").get("name", None), "test") # type: ignore[union-attr] - self.assertEqual(connection_results[1].get("serverAddress", None), "pgsql.test.com") - self.assertEqual(connection_results[1].find("connectionCredentials").get("password", None), "secret") # type: ignore[union-attr] - - def test_synchronous_publish_timeout_error(self) -> None: - with requests_mock.mock() as m: - m.register_uri("POST", self.baseurl, status_code=504) - - new_workbook = TSC.WorkbookItem(project_id="") - publish_mode = self.server.PublishMode.CreateNew - - self.assertRaisesRegex( - InternalServerError, - "Please use asynchronous publishing to avoid timeouts", - self.server.workbooks.publish, - new_workbook, - asset("SampleWB.twbx"), - publish_mode, - ) - def test_delete_extracts_all(self) -> None: - self.server.version = "3.10" - self.baseurl = self.server.workbooks.baseurl + sample_workbook = os.path.join(TEST_ASSET_DIR, "SampleWB.twbx") + publish_mode = server.PublishMode.CreateNew - with open(PUBLISH_ASYNC_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post( - self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/deleteExtract", status_code=200, text=response_xml - ) - self.server.workbooks.delete_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + new_job = server.workbooks.publish(new_workbook, sample_workbook, publish_mode, as_job=True) - def test_create_extracts_all(self) -> None: - self.server.version = "3.10" - self.baseurl = self.server.workbooks.baseurl + assert "7c3d599e-949f-44c3-94a1-f30ba85757e4" == new_job.id + assert "PublishWorkbook" == new_job.type + assert "0" == new_job.progress + assert "2018-06-29T23:22:32Z" == format_datetime(new_job.created_at) + assert 1 == new_job.finish_code - with open(PUBLISH_ASYNC_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post( - self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", status_code=200, text=response_xml - ) - self.server.workbooks.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") - def test_create_extracts_one(self) -> None: - self.server.version = "3.10" - self.baseurl = self.server.workbooks.baseurl +def test_publish_invalid_file(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem("test", "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + with pytest.raises(IOError): + server.workbooks.publish(new_workbook, ".", server.PublishMode.CreateNew) - datasource = TSC.DatasourceItem("test") - datasource._id = "1f951daf-4061-451a-9df1-69a8062664f2" - with open(PUBLISH_ASYNC_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.post( - self.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", status_code=200, text=response_xml - ) - self.server.workbooks.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42", False, datasource) +def test_publish_invalid_file_type(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem("test", "ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + with pytest.raises(ValueError): + server.workbooks.publish( + new_workbook, os.path.join(TEST_ASSET_DIR, "SampleDS.tds"), server.PublishMode.CreateNew + ) - def test_revisions(self) -> None: - self.baseurl = self.server.workbooks.baseurl - workbook = TSC.WorkbookItem("project", "test") - workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" - with open(REVISION_XML, "rb") as f: - response_xml = f.read().decode("utf-8") - with requests_mock.mock() as m: - m.get(f"{self.baseurl}/{workbook.id}/revisions", text=response_xml) - self.server.workbooks.populate_revisions(workbook) - revisions = workbook.revisions - - self.assertEqual(len(revisions), 3) - self.assertEqual("2016-07-26T20:34:56Z", format_datetime(revisions[0].created_at)) - self.assertEqual("2016-07-27T20:34:56Z", format_datetime(revisions[1].created_at)) - self.assertEqual("2016-07-28T20:34:56Z", format_datetime(revisions[2].created_at)) - - self.assertEqual(False, revisions[0].deleted) - self.assertEqual(False, revisions[0].current) - self.assertEqual(False, revisions[1].deleted) - self.assertEqual(False, revisions[1].current) - self.assertEqual(False, revisions[2].deleted) - self.assertEqual(True, revisions[2].current) - - self.assertEqual("Cassie", revisions[0].user_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[0].user_id) - self.assertIsNone(revisions[1].user_name) - self.assertIsNone(revisions[1].user_id) - self.assertEqual("Cassie", revisions[2].user_name) - self.assertEqual("5de011f8-5aa9-4d5b-b991-f462c8dd6bb7", revisions[2].user_id) - - def test_delete_revision(self) -> None: - self.baseurl = self.server.workbooks.baseurl - workbook = TSC.WorkbookItem("project", "test") - workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" +def test_publish_unnamed_file_object(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem("test") - with requests_mock.mock() as m: - m.delete(f"{self.baseurl}/{workbook.id}/revisions/3") - self.server.workbooks.delete_revision(workbook.id, "3") + with open(os.path.join(TEST_ASSET_DIR, "SampleWB.twbx"), "rb") as f: + with pytest.raises(ValueError): + server.workbooks.publish(new_workbook, f, server.PublishMode.CreateNew) - def test_download_revision(self) -> None: - with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/revisions/3/content", - headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, - ) - file_path = self.server.workbooks.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) - self.assertTrue(os.path.exists(file_path)) - def test_bad_download_response(self) -> None: - with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: - m.get( - self.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", - headers={"Content-Disposition": '''name="tableau_workbook"; filename*=UTF-8''"Sample workbook.twb"'''}, - ) - file_path = self.server.workbooks.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) - self.assertTrue(os.path.exists(file_path)) - - def test_odata_connection(self) -> None: - self.baseurl = self.server.workbooks.baseurl - workbook = TSC.WorkbookItem("project", "test") - workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" - connection = TSC.ConnectionItem() - url = "https://odata.website.com/TestODataEndpoint" - connection.server_address = url - connection._connection_type = "odata" - connection._id = "17376070-64d1-4d17-acb4-a56e4b5b1768" - - creds = TSC.ConnectionCredentials("", "", True) - connection.connection_credentials = creds - with open(ODATA_XML, "rb") as f: - response_xml = f.read().decode("utf-8") +def test_publish_non_bytes_file_object(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem("test") - with requests_mock.mock() as m: - m.put(f"{self.baseurl}/{workbook.id}/connections/{connection.id}", text=response_xml) - self.server.workbooks.update_connection(workbook, connection) + with open(os.path.join(TEST_ASSET_DIR, "SampleWB.twbx")) as f: + with pytest.raises(TypeError): + server.workbooks.publish(new_workbook, f, server.PublishMode.CreateNew) - history = m.request_history - request = history[0] - xml = fromstring(request.body) - xml_connection = xml.find(".//connection") +def test_publish_file_object_of_unknown_type_raises_exception(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem("test") + with BytesIO() as file_object: + file_object.write(bytes.fromhex("89504E470D0A1A0A")) + file_object.seek(0) + with pytest.raises(ValueError): + server.workbooks.publish(new_workbook, file_object, server.PublishMode.CreateNew) - assert xml_connection is not None - self.assertEqual(xml_connection.get("serverAddress"), url) - def test_get_workbook_all_fields(self) -> None: - self.server.version = "3.21" - baseurl = self.server.workbooks.baseurl +def test_publish_multi_connection(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem(name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + connection1 = TSC.ConnectionItem() + connection1.server_address = "mysql.test.com" + connection1.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) + connection2 = TSC.ConnectionItem() + connection2.server_address = "pgsql.test.com" + connection2.connection_credentials = TSC.ConnectionCredentials("test", "secret", True) - with open(GET_XML_ALL_FIELDS) as f: - response = f.read() + response = RequestFactory.Workbook._generate_xml(new_workbook, connections=[connection1, connection2]) + # Can't use ConnectionItem parser due to xml namespace problems + connection_results = fromstring(response).findall(".//connection") - ro = TSC.RequestOptions() - ro.all_fields = True + assert connection_results[0].get("serverAddress", None) == "mysql.test.com" + assert connection_results[0].find("connectionCredentials").get("name", None) == "test" + assert connection_results[1].get("serverAddress", None) == "pgsql.test.com" + assert connection_results[1].find("connectionCredentials").get("password", None) == "secret" - with requests_mock.mock() as m: - m.get(f"{baseurl}?fields=_all_", text=response) - workbooks, _ = self.server.workbooks.get(req_options=ro) - - assert workbooks[0].id == "9df3e2d1-070e-497a-9578-8cc557ced9df" - assert workbooks[0].name == "Superstore" - assert workbooks[0].content_url == "Superstore" - assert workbooks[0].webpage_url == "https://10ax.online.tableau.com/#/site/exampledev/workbooks/265605" - assert workbooks[0].show_tabs - assert workbooks[0].size == 2 - assert workbooks[0].created_at == parse_datetime("2024-02-14T04:42:09Z") - assert workbooks[0].updated_at == parse_datetime("2024-02-14T04:42:10Z") - assert workbooks[0].sheet_count == 9 - assert not workbooks[0].has_extracts - assert not workbooks[0].encrypt_extracts - assert workbooks[0].default_view_id == "2bdcd787-dcc6-4a5d-bc61-2846f1ef4534" - assert workbooks[0].share_description == "Superstore" - assert workbooks[0].last_published_at == parse_datetime("2024-02-14T04:42:09Z") - assert isinstance(workbooks[0].project, TSC.ProjectItem) - assert workbooks[0].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert workbooks[0].project.name == "Samples" - assert workbooks[0].project.description == "This project includes automatically uploaded samples." - assert isinstance(workbooks[0].location, TSC.LocationItem) - assert workbooks[0].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert workbooks[0].location.type == "Project" - assert workbooks[0].location.name == "Samples" - assert isinstance(workbooks[0].owner, TSC.UserItem) - assert workbooks[0].owner.email == "bob@example.com" - assert workbooks[0].owner.fullname == "Bob Smith" - assert workbooks[0].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert workbooks[0].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert workbooks[0].owner.name == "bob@example.com" - assert workbooks[0].owner.site_role == "SiteAdministratorCreator" - assert workbooks[1].id == "6693cb26-9507-4174-ad3e-9de81a18c971" - assert workbooks[1].name == "World Indicators" - assert workbooks[1].content_url == "WorldIndicators" - assert workbooks[1].webpage_url == "https://10ax.online.tableau.com/#/site/exampledev/workbooks/265606" - assert workbooks[1].show_tabs - assert workbooks[1].size == 1 - assert workbooks[1].created_at == parse_datetime("2024-02-14T04:42:11Z") - assert workbooks[1].updated_at == parse_datetime("2024-02-14T04:42:12Z") - assert workbooks[1].sheet_count == 8 - assert not workbooks[1].has_extracts - assert not workbooks[1].encrypt_extracts - assert workbooks[1].default_view_id == "3d10dbcf-a206-47c7-91ba-ebab3ab33d7c" - assert workbooks[1].share_description == "World Indicators" - assert workbooks[1].last_published_at == parse_datetime("2024-02-14T04:42:11Z") - assert isinstance(workbooks[1].project, TSC.ProjectItem) - assert workbooks[1].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert workbooks[1].project.name == "Samples" - assert workbooks[1].project.description == "This project includes automatically uploaded samples." - assert isinstance(workbooks[1].location, TSC.LocationItem) - assert workbooks[1].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" - assert workbooks[1].location.type == "Project" - assert workbooks[1].location.name == "Samples" - assert isinstance(workbooks[1].owner, TSC.UserItem) - assert workbooks[1].owner.email == "bob@example.com" - assert workbooks[1].owner.fullname == "Bob Smith" - assert workbooks[1].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert workbooks[1].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert workbooks[1].owner.name == "bob@example.com" - assert workbooks[1].owner.site_role == "SiteAdministratorCreator" - assert workbooks[2].id == "dbc0f162-909f-4edf-8392-0d12a80af955" - assert workbooks[2].name == "Superstore" - assert workbooks[2].description == "This is a superstore workbook" - assert workbooks[2].content_url == "Superstore_17078880698360" - assert workbooks[2].webpage_url == "https://10ax.online.tableau.com/#/site/exampledev/workbooks/265621" - assert not workbooks[2].show_tabs - assert workbooks[2].size == 1 - assert workbooks[2].created_at == parse_datetime("2024-02-14T05:21:09Z") - assert workbooks[2].updated_at == parse_datetime("2024-07-02T02:19:59Z") - assert workbooks[2].sheet_count == 7 - assert workbooks[2].has_extracts - assert not workbooks[2].encrypt_extracts - assert workbooks[2].default_view_id == "8c4b1d3e-3f31-4d2a-8b9f-492b92f27987" - assert workbooks[2].share_description == "Superstore" - assert workbooks[2].last_published_at == parse_datetime("2024-07-02T02:19:58Z") - assert isinstance(workbooks[2].project, TSC.ProjectItem) - assert workbooks[2].project.id == "9836791c-9468-40f0-b7f3-d10b9562a046" - assert workbooks[2].project.name == "default" - assert workbooks[2].project.description == "The default project that was automatically created by Tableau." - assert isinstance(workbooks[2].location, TSC.LocationItem) - assert workbooks[2].location.id == "9836791c-9468-40f0-b7f3-d10b9562a046" - assert workbooks[2].location.type == "Project" - assert workbooks[2].location.name == "default" - assert isinstance(workbooks[2].owner, TSC.UserItem) - assert workbooks[2].owner.email == "bob@example.com" - assert workbooks[2].owner.fullname == "Bob Smith" - assert workbooks[2].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" - assert workbooks[2].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") - assert workbooks[2].owner.name == "bob@example.com" - assert workbooks[2].owner.site_role == "SiteAdministratorCreator" + +def test_publish_multi_connection_flat(server: TSC.Server) -> None: + new_workbook = TSC.WorkbookItem(name="Sample", show_tabs=False, project_id="ee8c6e70-43b6-11e6-af4f-f7b0d8e20760") + connection1 = TSC.ConnectionItem() + connection1.server_address = "mysql.test.com" + connection1.username = "test" + connection1.password = "secret" + connection1.embed_password = True + connection2 = TSC.ConnectionItem() + connection2.server_address = "pgsql.test.com" + connection2.username = "test" + connection2.password = "secret" + connection2.embed_password = True + + response = RequestFactory.Workbook._generate_xml(new_workbook, connections=[connection1, connection2]) + # Can't use ConnectionItem parser due to xml namespace problems + connection_results = fromstring(response).findall(".//connection") + + assert connection_results[0].get("serverAddress", None) == "mysql.test.com" + assert connection_results[0].find("connectionCredentials").get("name", None) == "test" + assert connection_results[1].get("serverAddress", None) == "pgsql.test.com" + assert connection_results[1].find("connectionCredentials").get("password", None) == "secret" + + +def test_synchronous_publish_timeout_error(server: TSC.Server) -> None: + with requests_mock.mock() as m: + m.register_uri("POST", server.workbooks.baseurl, status_code=504) + + new_workbook = TSC.WorkbookItem(project_id="") + publish_mode = server.PublishMode.CreateNew + + with pytest.raises(InternalServerError, match="Please use asynchronous publishing to avoid timeouts"): + server.workbooks.publish(new_workbook, TEST_ASSET_DIR / "SampleWB.twbx", publish_mode) + + +def test_delete_extracts_all(server: TSC.Server) -> None: + server.version = "3.10" + server.workbooks.baseurl + + response_xml = PUBLISH_ASYNC_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/deleteExtract", + status_code=200, + text=response_xml, + ) + server.workbooks.delete_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + +def test_create_extracts_all(server: TSC.Server) -> None: + server.version = "3.10" + server.workbooks.baseurl + + response_xml = PUBLISH_ASYNC_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", + status_code=200, + text=response_xml, + ) + server.workbooks.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42") + + +def test_create_extracts_one(server: TSC.Server) -> None: + server.version = "3.10" + server.workbooks.baseurl + + datasource = TSC.DatasourceItem("test") + datasource._id = "1f951daf-4061-451a-9df1-69a8062664f2" + + response_xml = PUBLISH_ASYNC_XML.read_text() + with requests_mock.mock() as m: + m.post( + server.workbooks.baseurl + "/3cc6cd06-89ce-4fdc-b935-5294135d6d42/createExtract", + status_code=200, + text=response_xml, + ) + server.workbooks.create_extract("3cc6cd06-89ce-4fdc-b935-5294135d6d42", False, datasource) + + +def test_revisions(server: TSC.Server) -> None: + server.workbooks.baseurl + workbook = TSC.WorkbookItem("project", "test") + workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" + + response_xml = REVISION_XML.read_text() + with requests_mock.mock() as m: + m.get(f"{server.workbooks.baseurl}/{workbook.id}/revisions", text=response_xml) + server.workbooks.populate_revisions(workbook) + revisions = workbook.revisions + + assert len(revisions) == 3 + assert "2016-07-26T20:34:56Z" == format_datetime(revisions[0].created_at) + assert "2016-07-27T20:34:56Z" == format_datetime(revisions[1].created_at) + assert "2016-07-28T20:34:56Z" == format_datetime(revisions[2].created_at) + + assert not revisions[0].deleted + assert not revisions[0].current + assert not revisions[1].deleted + assert not revisions[1].current + assert not revisions[2].deleted + assert revisions[2].current + + assert "Cassie" == revisions[0].user_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == revisions[0].user_id + assert revisions[1].user_name is None + assert revisions[1].user_id is None + assert "Cassie" == revisions[2].user_name + assert "5de011f8-5aa9-4d5b-b991-f462c8dd6bb7" == revisions[2].user_id + + +def test_delete_revision(server: TSC.Server) -> None: + server.workbooks.baseurl + workbook = TSC.WorkbookItem("project", "test") + workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" + + with requests_mock.mock() as m: + m.delete(f"{server.workbooks.baseurl}/{workbook.id}/revisions/3") + server.workbooks.delete_revision(workbook.id, "3") + + +def test_download_revision(server: TSC.Server) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + server.workbooks.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/revisions/3/content", + headers={"Content-Disposition": 'name="tableau_datasource"; filename="Sample datasource.tds"'}, + ) + file_path = server.workbooks.download_revision("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", "3", td) + assert os.path.exists(file_path) + + +def test_bad_download_response(server: TSC.Server) -> None: + with requests_mock.mock() as m, tempfile.TemporaryDirectory() as td: + m.get( + server.workbooks.baseurl + "/9dbd2263-16b5-46e1-9c43-a76bb8ab65fb/content", + headers={"Content-Disposition": '''name="tableau_workbook"; filename*=UTF-8''"Sample workbook.twb"'''}, + ) + file_path = server.workbooks.download("9dbd2263-16b5-46e1-9c43-a76bb8ab65fb", td) + assert os.path.exists(file_path) + + +def test_odata_connection(server: TSC.Server) -> None: + server.workbooks.baseurl + workbook = TSC.WorkbookItem("project", "test") + workbook._id = "06b944d2-959d-4604-9305-12323c95e70e" + connection = TSC.ConnectionItem() + url = "https://odata.website.com/TestODataEndpoint" + connection.server_address = url + connection._connection_type = "odata" + connection._id = "17376070-64d1-4d17-acb4-a56e4b5b1768" + + creds = TSC.ConnectionCredentials("", "", True) + connection.connection_credentials = creds + response_xml = ODATA_XML.read_text() + + with requests_mock.mock() as m: + m.put(f"{server.workbooks.baseurl}/{workbook.id}/connections/{connection.id}", text=response_xml) + server.workbooks.update_connection(workbook, connection) + + history = m.request_history + + request = history[0] + xml = fromstring(request.body) + xml_connection = xml.find(".//connection") + + assert xml_connection is not None + assert xml_connection.get("serverAddress") == url + + +def test_update_workbook_connections(server: TSC.Server) -> None: + populate_xml = POPULATE_CONNECTIONS_XML.read_text() + response_xml = UPDATE_CONNECTIONS_XML.read_text() + + with requests_mock.Mocker() as m: + workbook_id = "1a2b3c4d-5e6f-7a8b-9c0d-112233445566" + connection_luids = ["abc12345-def6-7890-gh12-ijklmnopqrst", "1234abcd-5678-efgh-ijkl-0987654321mn"] + + workbook = TSC.WorkbookItem(workbook_id) + workbook._id = workbook_id + server.version = "3.26" + url = f"{server.baseurl}/{workbook_id}/connections" + m.get( + "http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks/1a2b3c4d-5e6f-7a8b-9c0d-112233445566/connections", + text=populate_xml, + ) + m.put( + "http://test/api/3.26/sites/dad65087-b08b-4603-af4e-2887b8aafc67/workbooks/1a2b3c4d-5e6f-7a8b-9c0d-112233445566/connections", + text=response_xml, + ) + + connection_items = server.workbooks.update_connections( + workbook_item=workbook, + connection_luids=connection_luids, + authentication_type="AD Service Principal", + username="svc-client", + password="secret-token", + embed_password=True, + ) + updated_ids = [conn.id for conn in connection_items] + + assert updated_ids == connection_luids + assert "AD Service Principal" == connection_items[0].auth_type + + +def test_get_workbook_all_fields(server: TSC.Server) -> None: + server.version = "3.21" + baseurl = server.workbooks.baseurl + + response = GET_XML_ALL_FIELDS.read_text() + + ro = TSC.RequestOptions() + ro.all_fields = True + + with requests_mock.mock() as m: + m.get(f"{baseurl}?fields=_all_", text=response) + workbooks, _ = server.workbooks.get(req_options=ro) + + assert workbooks[0].id == "9df3e2d1-070e-497a-9578-8cc557ced9df" + assert workbooks[0].name == "Superstore" + assert workbooks[0].content_url == "Superstore" + assert workbooks[0].webpage_url == "https://10ax.online.tableau.com/#/site/exampledev/workbooks/265605" + assert workbooks[0].show_tabs + assert workbooks[0].size == 2 + assert workbooks[0].created_at == parse_datetime("2024-02-14T04:42:09Z") + assert workbooks[0].updated_at == parse_datetime("2024-02-14T04:42:10Z") + assert workbooks[0].sheet_count == 9 + assert not workbooks[0].has_extracts + assert not workbooks[0].encrypt_extracts + assert workbooks[0].default_view_id == "2bdcd787-dcc6-4a5d-bc61-2846f1ef4534" + assert workbooks[0].share_description == "Superstore" + assert workbooks[0].last_published_at == parse_datetime("2024-02-14T04:42:09Z") + assert isinstance(workbooks[0].project, TSC.ProjectItem) + assert workbooks[0].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert workbooks[0].project.name == "Samples" + assert workbooks[0].project.description == "This project includes automatically uploaded samples." + assert isinstance(workbooks[0].location, TSC.LocationItem) + assert workbooks[0].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert workbooks[0].location.type == "Project" + assert workbooks[0].location.name == "Samples" + assert isinstance(workbooks[0].owner, TSC.UserItem) + assert workbooks[0].owner.email == "bob@example.com" + assert workbooks[0].owner.fullname == "Bob Smith" + assert workbooks[0].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert workbooks[0].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert workbooks[0].owner.name == "bob@example.com" + assert workbooks[0].owner.site_role == "SiteAdministratorCreator" + assert workbooks[1].id == "6693cb26-9507-4174-ad3e-9de81a18c971" + assert workbooks[1].name == "World Indicators" + assert workbooks[1].content_url == "WorldIndicators" + assert workbooks[1].webpage_url == "https://10ax.online.tableau.com/#/site/exampledev/workbooks/265606" + assert workbooks[1].show_tabs + assert workbooks[1].size == 1 + assert workbooks[1].created_at == parse_datetime("2024-02-14T04:42:11Z") + assert workbooks[1].updated_at == parse_datetime("2024-02-14T04:42:12Z") + assert workbooks[1].sheet_count == 8 + assert not workbooks[1].has_extracts + assert not workbooks[1].encrypt_extracts + assert workbooks[1].default_view_id == "3d10dbcf-a206-47c7-91ba-ebab3ab33d7c" + assert workbooks[1].share_description == "World Indicators" + assert workbooks[1].last_published_at == parse_datetime("2024-02-14T04:42:11Z") + assert isinstance(workbooks[1].project, TSC.ProjectItem) + assert workbooks[1].project.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert workbooks[1].project.name == "Samples" + assert workbooks[1].project.description == "This project includes automatically uploaded samples." + assert isinstance(workbooks[1].location, TSC.LocationItem) + assert workbooks[1].location.id == "669ca36b-492e-4ccf-bca1-3614fe6a9d7a" + assert workbooks[1].location.type == "Project" + assert workbooks[1].location.name == "Samples" + assert isinstance(workbooks[1].owner, TSC.UserItem) + assert workbooks[1].owner.email == "bob@example.com" + assert workbooks[1].owner.fullname == "Bob Smith" + assert workbooks[1].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert workbooks[1].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert workbooks[1].owner.name == "bob@example.com" + assert workbooks[1].owner.site_role == "SiteAdministratorCreator" + assert workbooks[2].id == "dbc0f162-909f-4edf-8392-0d12a80af955" + assert workbooks[2].name == "Superstore" + assert workbooks[2].description == "This is a superstore workbook" + assert workbooks[2].content_url == "Superstore_17078880698360" + assert workbooks[2].webpage_url == "https://10ax.online.tableau.com/#/site/exampledev/workbooks/265621" + assert not workbooks[2].show_tabs + assert workbooks[2].size == 1 + assert workbooks[2].created_at == parse_datetime("2024-02-14T05:21:09Z") + assert workbooks[2].updated_at == parse_datetime("2024-07-02T02:19:59Z") + assert workbooks[2].sheet_count == 7 + assert workbooks[2].has_extracts + assert not workbooks[2].encrypt_extracts + assert workbooks[2].default_view_id == "8c4b1d3e-3f31-4d2a-8b9f-492b92f27987" + assert workbooks[2].share_description == "Superstore" + assert workbooks[2].last_published_at == parse_datetime("2024-07-02T02:19:58Z") + assert isinstance(workbooks[2].project, TSC.ProjectItem) + assert workbooks[2].project.id == "9836791c-9468-40f0-b7f3-d10b9562a046" + assert workbooks[2].project.name == "default" + assert workbooks[2].project.description == "The default project that was automatically created by Tableau." + assert isinstance(workbooks[2].location, TSC.LocationItem) + assert workbooks[2].location.id == "9836791c-9468-40f0-b7f3-d10b9562a046" + assert workbooks[2].location.type == "Project" + assert workbooks[2].location.name == "default" + assert isinstance(workbooks[2].owner, TSC.UserItem) + assert workbooks[2].owner.email == "bob@example.com" + assert workbooks[2].owner.fullname == "Bob Smith" + assert workbooks[2].owner.id == "ee8bc9ca-77fe-4ae0-8093-cf77f0ee67a9" + assert workbooks[2].owner.last_login == parse_datetime("2025-02-04T06:39:20Z") + assert workbooks[2].owner.name == "bob@example.com" + assert workbooks[2].owner.site_role == "SiteAdministratorCreator" diff --git a/test/test_workbook_model.py b/test/test_workbook_model.py index fc6423564..3d6f31a7a 100644 --- a/test/test_workbook_model.py +++ b/test/test_workbook_model.py @@ -1,13 +1,12 @@ -import unittest +import pytest import tableauserverclient as TSC -class WorkbookModelTests(unittest.TestCase): - def test_invalid_show_tabs(self): - workbook = TSC.WorkbookItem("10") - with self.assertRaises(ValueError): - workbook.show_tabs = "Hello" +def test_invalid_show_tabs(): + workbook = TSC.WorkbookItem("10") + with pytest.raises(ValueError): + workbook.show_tabs = "Hello" - with self.assertRaises(ValueError): - workbook.show_tabs = None + with pytest.raises(ValueError): + workbook.show_tabs = None