Skip to content

Commit cf59ca4

Browse files
committed
add more e2e tests
1 parent d1567a3 commit cf59ca4

6 files changed

Lines changed: 641 additions & 20 deletions

File tree

76.3 KB
Binary file not shown.
62.6 KB
Binary file not shown.

test_e2e/conftest.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
def pytest_configure(config):
77
config.addinivalue_line("markers", "e2e: mark test as end-to-end (requires a real Tableau server)")
8+
config.addinivalue_line("markers", "site_admin: mark test as requiring SiteAdmin permissions")
89

910

1011
@pytest.fixture(scope="session")
@@ -13,10 +14,14 @@ def server():
1314
Authenticated TSC server session for e2e tests.
1415
1516
Required environment variables:
16-
TABLEAU_SERVER — server URL, e.g. https://10ax.online.tableau.com
17-
TABLEAU_SITE — site content URL
18-
TABLEAU_TOKEN — personal access token value
19-
TABLEAU_TOKEN_NAME — personal access token name
17+
TABLEAU_SERVER -- server URL, e.g. https://10ax.online.tableau.com
18+
TABLEAU_SITE -- site content URL
19+
TABLEAU_TOKEN -- personal access token value
20+
TABLEAU_TOKEN_NAME -- personal access token name
21+
22+
Optional:
23+
TABLEAU_IS_ADMIN -- set to "1" or "true" if the token belongs to a SiteAdmin account.
24+
Tests marked with @pytest.mark.site_admin are skipped when not set.
2025
"""
2126
url = os.environ.get("TABLEAU_SERVER")
2227
site = os.environ.get("TABLEAU_SITE", "")
@@ -30,3 +35,28 @@ def server():
3035
auth = TSC.PersonalAccessTokenAuth(token_name, token, site)
3136
with server.auth.sign_in(auth):
3237
yield server
38+
39+
40+
@pytest.fixture(scope="session")
41+
def is_admin():
42+
val = os.environ.get("TABLEAU_IS_ADMIN", "").strip().lower()
43+
return val in ("1", "true", "yes")
44+
45+
46+
def pytest_runtest_setup(item):
47+
if item.get_closest_marker("site_admin"):
48+
val = os.environ.get("TABLEAU_IS_ADMIN", "").strip().lower()
49+
if val not in ("1", "true", "yes"):
50+
pytest.skip("Skipping site_admin test: set TABLEAU_IS_ADMIN=1 to run")
51+
52+
53+
@pytest.fixture(scope="session")
54+
def project_id(server):
55+
"""Return the ID of the project named by TABLEAU_PROJECT (default 'Default')."""
56+
project_name = os.environ.get("TABLEAU_PROJECT", "Sandbox")
57+
opts = TSC.RequestOptions()
58+
opts.filter.add(TSC.Filter(TSC.RequestOptions.Field.Name, TSC.RequestOptions.Operator.Equals, project_name))
59+
projects, _ = server.projects.get(opts)
60+
if not projects:
61+
pytest.skip(f"Project {project_name!r} not found -- set TABLEAU_PROJECT env var")
62+
return projects[0].id

test_e2e/test_publisher.py

Lines changed: 311 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
1+
"""
2+
E2E tests for Publisher-level operations against a real Tableau server.
3+
4+
Requires: TABLEAU_SERVER, TABLEAU_SITE, TABLEAU_TOKEN, TABLEAU_TOKEN_NAME
5+
Optional: TABLEAU_PROJECT (defaults to "Default")
6+
7+
Run with:
8+
pytest test_e2e/test_publisher.py -v -m e2e
9+
"""
10+
import os
11+
from pathlib import Path
12+
13+
import pytest
14+
import tableauserverclient as TSC
15+
from tableauserverclient.models import Resource
16+
17+
ASSETS_DIR = Path(__file__).parent / "assets"
18+
SAMPLE_WORKBOOK = ASSETS_DIR / "WorkbookWithoutExtract.twbx"
19+
EXTRACT_WORKBOOK = ASSETS_DIR / "WorkbookWithExtract.twbx"
20+
SAMPLE_DATASOURCE = ASSETS_DIR / "WorldIndicators.tdsx"
21+
22+
pytestmark = pytest.mark.e2e
23+
24+
25+
# ---------------------------------------------------------------------------
26+
# Shared fixtures
27+
# ---------------------------------------------------------------------------
28+
29+
30+
@pytest.fixture(scope="module")
31+
def workbook(server, project_id):
32+
"""Publish a workbook for this module's tests, clean up after."""
33+
wb = TSC.WorkbookItem(name="tsc-e2e-publisher-wb", project_id=project_id)
34+
wb = server.workbooks.publish(wb, SAMPLE_WORKBOOK, TSC.Server.PublishMode.Overwrite)
35+
yield wb
36+
server.workbooks.delete(wb.id)
37+
38+
39+
# ---------------------------------------------------------------------------
40+
# Workbook CRUD
41+
# ---------------------------------------------------------------------------
42+
43+
44+
def test_workbook_publish_and_get(server, project_id):
45+
"""Published workbook is retrievable by id and has correct name/project."""
46+
wb = TSC.WorkbookItem(name="tsc-e2e-publish-test", project_id=project_id)
47+
wb = server.workbooks.publish(wb, SAMPLE_WORKBOOK, TSC.Server.PublishMode.Overwrite)
48+
try:
49+
fetched = server.workbooks.get_by_id(wb.id)
50+
assert fetched.id == wb.id
51+
assert fetched.name == "tsc-e2e-publish-test"
52+
assert fetched.project_id == project_id
53+
finally:
54+
server.workbooks.delete(wb.id)
55+
56+
57+
def test_workbook_update(server, workbook):
58+
"""Updating a workbook's name and description persists on the server."""
59+
original_name = workbook.name
60+
workbook.name = "tsc-e2e-publisher-wb-renamed"
61+
workbook.description = "updated by e2e test"
62+
updated = server.workbooks.update(workbook)
63+
try:
64+
assert updated.name == "tsc-e2e-publisher-wb-renamed"
65+
assert updated.description == "updated by e2e test"
66+
finally:
67+
workbook.name = original_name
68+
workbook.description = ""
69+
server.workbooks.update(workbook)
70+
71+
72+
def test_workbook_download(server, workbook, tmp_path):
73+
"""Downloaded workbook file exists and is non-empty."""
74+
path = server.workbooks.download(workbook.id, str(tmp_path))
75+
assert Path(path).exists()
76+
assert Path(path).stat().st_size > 0
77+
78+
79+
def test_workbook_populate_views(server, workbook):
80+
"""populate_views returns at least one view for the test workbook."""
81+
server.workbooks.populate_views(workbook)
82+
assert workbook.views is not None
83+
assert len(workbook.views) > 0
84+
85+
86+
def test_workbook_populate_connections(server, workbook):
87+
"""populate_connections returns a list (may be empty for extract-only wb)."""
88+
server.workbooks.populate_connections(workbook)
89+
assert workbook.connections is not None
90+
91+
92+
def test_workbook_preview_image(server, workbook):
93+
"""populate_preview_image succeeds without error (image may be empty for freshly-published workbooks)."""
94+
server.workbooks.populate_preview_image(workbook)
95+
assert workbook.preview_image is not None
96+
97+
98+
def test_workbook_tags(server, workbook):
99+
"""Tags added to a workbook round-trip correctly and can be removed."""
100+
server.workbooks.add_tags(workbook, ["e2e-tag-a", "e2e-tag-b"])
101+
fetched = server.workbooks.get_by_id(workbook.id)
102+
try:
103+
assert "e2e-tag-a" in fetched.tags
104+
assert "e2e-tag-b" in fetched.tags
105+
finally:
106+
server.workbooks.delete_tags(workbook, ["e2e-tag-a", "e2e-tag-b"])
107+
fetched = server.workbooks.get_by_id(workbook.id)
108+
assert "e2e-tag-a" not in fetched.tags
109+
110+
111+
# ---------------------------------------------------------------------------
112+
# View exports
113+
# ---------------------------------------------------------------------------
114+
115+
116+
def test_view_export_png(server, workbook, tmp_path):
117+
"""A view can be exported as a PNG image."""
118+
server.workbooks.populate_views(workbook)
119+
view = workbook.views[0]
120+
server.views.populate_image(view)
121+
assert view.image is not None
122+
assert len(view.image) > 0
123+
124+
125+
def test_view_export_pdf(server, workbook, tmp_path):
126+
"""A view can be exported as a PDF."""
127+
server.workbooks.populate_views(workbook)
128+
view = workbook.views[0]
129+
opts = TSC.PDFRequestOptions()
130+
server.views.populate_pdf(view, opts)
131+
assert view.pdf is not None
132+
assert len(view.pdf) > 0
133+
134+
135+
def test_view_export_csv(server, workbook):
136+
"""A view can be exported as CSV data."""
137+
server.workbooks.populate_views(workbook)
138+
view = workbook.views[0]
139+
server.views.populate_csv(view)
140+
assert view.csv is not None
141+
142+
143+
# ---------------------------------------------------------------------------
144+
# Datasource CRUD
145+
# ---------------------------------------------------------------------------
146+
147+
148+
@pytest.fixture(scope="module")
149+
def extract_workbook(server, project_id):
150+
"""Publish a workbook with an extract for refresh/extract tests, clean up after."""
151+
wb = TSC.WorkbookItem(name="tsc-e2e-extract-wb", project_id=project_id)
152+
wb = server.workbooks.publish(wb, EXTRACT_WORKBOOK, TSC.Server.PublishMode.Overwrite)
153+
yield wb
154+
server.workbooks.delete(wb.id)
155+
156+
157+
@pytest.fixture(scope="module")
158+
def datasource(server, project_id):
159+
"""Publish a datasource for this module's tests, clean up after."""
160+
ds = TSC.DatasourceItem(project_id=project_id, name="tsc-e2e-publisher-ds")
161+
ds = server.datasources.publish(ds, str(SAMPLE_DATASOURCE), TSC.Server.PublishMode.Overwrite)
162+
yield ds
163+
server.datasources.delete(ds.id)
164+
165+
166+
def test_datasource_publish_and_get(server, datasource):
167+
"""Published datasource is retrievable by id."""
168+
fetched = server.datasources.get_by_id(datasource.id)
169+
assert fetched.id == datasource.id
170+
assert fetched.name == "tsc-e2e-publisher-ds"
171+
172+
173+
def test_datasource_update(server, datasource):
174+
"""Updating datasource description persists."""
175+
datasource.description = "updated by e2e test"
176+
updated = server.datasources.update(datasource)
177+
assert updated.description == "updated by e2e test"
178+
179+
180+
def test_datasource_download(server, datasource, tmp_path):
181+
"""Downloaded datasource file exists and is non-empty."""
182+
path = server.datasources.download(datasource.id, str(tmp_path))
183+
assert Path(path).exists()
184+
assert Path(path).stat().st_size > 0
185+
186+
187+
def test_datasource_populate_connections(server, datasource):
188+
"""populate_connections returns a list for a published datasource."""
189+
server.datasources.populate_connections(datasource)
190+
assert datasource.connections is not None
191+
192+
193+
def test_datasource_tags(server, datasource):
194+
"""Tags added to a datasource round-trip correctly."""
195+
server.datasources.add_tags(datasource, ["e2e-ds-tag"])
196+
fetched = server.datasources.get_by_id(datasource.id)
197+
try:
198+
assert "e2e-ds-tag" in fetched.tags
199+
finally:
200+
server.datasources.delete_tags(datasource, ["e2e-ds-tag"])
201+
202+
203+
# ---------------------------------------------------------------------------
204+
# Favorites
205+
# ---------------------------------------------------------------------------
206+
207+
208+
def test_favorites_workbook(server, workbook):
209+
"""A workbook can be added to and removed from favorites."""
210+
user = TSC.UserItem()
211+
user.id = server.user_id
212+
server.favorites.add_favorite(user, Resource.Workbook, workbook)
213+
server.favorites.get(user)
214+
assert any(f.id == workbook.id for f in user.favorites.get("workbooks", []))
215+
server.favorites.delete_favorite_workbook(user, workbook)
216+
217+
218+
def test_favorites_view(server, workbook):
219+
"""A view can be added to and removed from favorites."""
220+
server.workbooks.populate_views(workbook)
221+
view = workbook.views[0]
222+
user = TSC.UserItem()
223+
user.id = server.user_id
224+
server.favorites.add_favorite_view(user, view)
225+
server.favorites.get(user)
226+
assert any(f.id == view.id for f in user.favorites.get("views", []))
227+
server.favorites.delete_favorite_view(user, view)
228+
229+
230+
def test_favorites_datasource(server, datasource):
231+
"""A datasource can be added to and removed from favorites."""
232+
user = TSC.UserItem()
233+
user.id = server.user_id
234+
server.favorites.add_favorite_datasource(user, datasource)
235+
server.favorites.get(user)
236+
assert any(f.id == datasource.id for f in user.favorites.get("datasources", []))
237+
server.favorites.delete_favorite_datasource(user, datasource)
238+
239+
240+
# ---------------------------------------------------------------------------
241+
# Pagination
242+
# ---------------------------------------------------------------------------
243+
244+
245+
def test_pager_workbooks(server):
246+
"""Pager iterates over all workbooks without error."""
247+
count = sum(1 for _ in TSC.Pager(server.workbooks))
248+
assert count >= 0
249+
250+
251+
def test_queryset_filter(server):
252+
"""QuerySet filter returns only matching workbooks."""
253+
results = list(server.workbooks.filter(name="tsc-e2e-publisher-wb"))
254+
assert all(wb.name == "tsc-e2e-publisher-wb" for wb in results)
255+
256+
257+
# ---------------------------------------------------------------------------
258+
# Workbook refresh and extract
259+
# ---------------------------------------------------------------------------
260+
261+
262+
def test_workbook_refresh(server, extract_workbook):
263+
"""Triggering a workbook refresh returns a job item."""
264+
job = server.workbooks.refresh(extract_workbook)
265+
assert job.id is not None
266+
267+
268+
def test_workbook_create_and_delete_extract(server, extract_workbook):
269+
"""An extract can be deleted from a workbook and then recreated.
270+
271+
Requires a workbook asset whose datasource is supported on the target server OS.
272+
WorkbookWithExtract.twbx uses MS Access which fails on Linux servers -- replace
273+
the asset with a compatible .twbx to enable this test.
274+
"""
275+
pytest.skip("WorkbookWithExtract.twbx uses MS Access (not supported on Linux) -- replace asset to enable")
276+
277+
278+
# ---------------------------------------------------------------------------
279+
# Datasource refresh
280+
# ---------------------------------------------------------------------------
281+
282+
283+
def test_datasource_refresh(server, datasource):
284+
"""Triggering a datasource refresh returns a job item."""
285+
job = server.datasources.refresh(datasource)
286+
assert job.id is not None
287+
288+
289+
# ---------------------------------------------------------------------------
290+
# Metadata API
291+
# ---------------------------------------------------------------------------
292+
293+
294+
def test_metadata_query(server):
295+
"""Metadata GraphQL API returns a valid response structure."""
296+
result = server.metadata.query(
297+
"""
298+
{
299+
publishedDatasourcesConnection(first: 5) {
300+
nodes {
301+
luid
302+
name
303+
}
304+
}
305+
}
306+
"""
307+
)
308+
assert "data" in result
309+
assert "publishedDatasourcesConnection" in result["data"]
310+
311+

0 commit comments

Comments
 (0)