Skip to content

Commit b9db799

Browse files
Tighten AuthFederation tests + require update() lifetime arg
Three review nits from Wayne Xie's second pass on PR #1894: - Realism in the "never-enabled" GET fixture: use the real server defaults — token lifetime 7200 (AuthFederationService.java:86) and the actual issuer URL shape %s/api/%s/sites/<luid>/auth-federation (:748-766), not the fabricated /sites/mySite/... URL and 3600 lifetime. - UUID-guard matrix: add None to the bad-target list and parametrize over {raw, item} so the item-wrapped None path (connector.luid=None / key.kid=None) is also covered. Widens bad_target to object. - update() token_lifetime_seconds is now required: there is no other mutable field on the config, so a no-arg call is not a meaningful operation. Tightens the matching request_factory signature and drops the test that exercised the empty-patch wire shape. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 4778874 commit b9db799

4 files changed

Lines changed: 43 additions & 34 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@
1111
- `server.auth_federation.enable(token_lifetime_seconds=None)` ->
1212
`AuthFederationItem` — idempotent PUT. Server auto-generates one active
1313
signing key on first enable.
14-
- `server.auth_federation.update(token_lifetime_seconds=None)` ->
14+
- `server.auth_federation.update(token_lifetime_seconds)` ->
1515
`AuthFederationItem` — merge-patch; only `token_lifetime_seconds` is
16-
mutable. `enabled` is never sent on PATCH; use `enable()` / `delete()`
17-
to change WIF state.
16+
mutable (required), so `update()` always changes it. `enabled` is never
17+
sent on PATCH; use `enable()` / `delete()` to change WIF state.
1818
- `server.auth_federation.delete()` — permanently deletes the WIF config
1919
along with every signing key and connector on the site. A subsequent
2020
`enable()` starts from a clean slate; prior connectors and keys are

‎tableauserverclient/server/endpoint/auth_federation_endpoint.py‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ def enable(self, token_lifetime_seconds: int | None = None) -> AuthFederationIte
130130
return AuthFederationItem.from_response(response.content, self.parent_srv.namespace)
131131

132132
@api(version="3.30")
133-
def update(self, token_lifetime_seconds: int | None = None) -> AuthFederationItem:
133+
def update(self, token_lifetime_seconds: int) -> AuthFederationItem:
134134
"""
135135
Merge-patch the mutable fields of the WIF admin config. Only
136136
``token_lifetime_seconds`` is mutable; ``enabled``, ``issuer_url``,
@@ -141,9 +141,10 @@ def update(self, token_lifetime_seconds: int | None = None) -> AuthFederationIte
141141
142142
Parameters
143143
----------
144-
token_lifetime_seconds : int | None
145-
New JWT lifetime in seconds. ``None`` sends an empty patch (no
146-
fields changed) — rarely useful, but accepted by the server.
144+
token_lifetime_seconds : int
145+
New JWT lifetime in seconds. Required — there is nothing else to
146+
patch, so a call with no fields to change is not a meaningful
147+
operation.
147148
148149
Returns
149150
-------

‎tableauserverclient/server/request_factory.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -117,12 +117,11 @@ def enable_req(self, token_lifetime_seconds: int | None = None) -> bytes:
117117
elem.attrib["tokenLifetimeSeconds"] = str(token_lifetime_seconds)
118118
return ET.tostring(elem)
119119

120-
def update_req(self, token_lifetime_seconds: int | None = None) -> bytes:
120+
def update_req(self, token_lifetime_seconds: int) -> bytes:
121121
# ``enabled`` must never be sent on PATCH — the server rejects a merge
122122
# patch with ``enabled`` present. Enable/disable goes through PUT/DELETE.
123123
elem = ET.Element("authFederationAdmin")
124-
if token_lifetime_seconds is not None:
125-
elem.attrib["tokenLifetimeSeconds"] = str(token_lifetime_seconds)
124+
elem.attrib["tokenLifetimeSeconds"] = str(token_lifetime_seconds)
126125
return ET.tostring(elem)
127126

128127
def create_connector_req(self, connector: "AuthFederationConnectorItem") -> bytes:

‎test/test_auth_federation.py‎

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,22 +50,27 @@ def test_auth_federation_get(server: TSC.Server) -> None:
5050
def test_auth_federation_get_defaults_when_never_enabled(server: TSC.Server) -> None:
5151
# Server GET on a never-enabled site still returns ``issuerUrl`` and the
5252
# default ``tokenLifetimeSeconds``; only ``activeKeyId`` and the
53-
# ``<connectors>`` wrapper are absent.
53+
# ``<connectors>`` wrapper are absent. Values mirror the real server:
54+
# DEFAULT_TOKEN_LIFETIME_SECONDS=7200 (AuthFederationService.java:86) and
55+
# ``<base>/api/<version>/sites/<siteLuid>/auth-federation`` (:748-766).
56+
default_issuer_url = (
57+
"https://10ax.online.tableau.com/api/3.30/sites/" "dad65087-b08b-4603-af4e-2887b8aafc67/auth-federation"
58+
)
5459
default_body = (
5560
'<tsResponse xmlns="http://tableau.com/api">'
5661
'<authFederationAdmin enabled="false" '
57-
'issuerUrl="https://10ax.online.tableau.com/sites/mySite/auth-federation" '
58-
'tokenLifetimeSeconds="3600"/>'
62+
f'issuerUrl="{default_issuer_url}" '
63+
'tokenLifetimeSeconds="7200"/>'
5964
"</tsResponse>"
6065
)
6166
with requests_mock.mock() as m:
6267
m.get(server.auth_federation.baseurl, text=default_body)
6368
config = server.auth_federation.get()
6469

6570
assert config.enabled is False
66-
assert config.issuer_url == "https://10ax.online.tableau.com/sites/mySite/auth-federation"
71+
assert config.issuer_url == default_issuer_url
6772
assert config.active_key_id is None
68-
assert config.token_lifetime_seconds == 3600
73+
assert config.token_lifetime_seconds == 7200
6974
assert config.connectors == []
7075

7176

@@ -119,22 +124,6 @@ def test_auth_federation_update(server: TSC.Server) -> None:
119124
assert config_out.token_lifetime_seconds == 7200
120125

121126

122-
def test_auth_federation_update_empty_patch(server: TSC.Server) -> None:
123-
# This exercises the client wire shape only. Whether the server binds an
124-
# empty ``<authFederationAdmin/>`` as a non-null ``AuthFederationConfigType``
125-
# with every field null (a no-op merge-patch) or as a null body (400) must
126-
# be covered by a server-side integration test — not here.
127-
with requests_mock.mock() as m:
128-
m.patch(server.auth_federation.baseurl, text=UPDATE.read_text())
129-
server.auth_federation.update()
130-
body = m.request_history[0].text
131-
132-
elem = fromstring(body)
133-
assert elem.tag == "authFederationAdmin"
134-
assert "enabled" not in elem.attrib
135-
assert "tokenLifetimeSeconds" not in elem.attrib
136-
137-
138127
def test_auth_federation_delete(server: TSC.Server) -> None:
139128
with requests_mock.mock() as m:
140129
m.delete(server.auth_federation.baseurl, status_code=204)
@@ -330,16 +319,36 @@ def test_auth_federation_delete_key_by_item(server: TSC.Server) -> None:
330319
"deactivate_key",
331320
"delete_key",
332321
]
322+
_KEY_METHODS = {"activate_key", "deactivate_key", "delete_key"}
333323

334324

325+
@pytest.mark.parametrize("wrap_as_item", [False, True], ids=["raw", "item"])
335326
@pytest.mark.parametrize("method_name", _UUID_METHODS)
336-
@pytest.mark.parametrize("bad_target", ["../../admin", "not-a-uuid", "a4b9c2d7", "", "d3a8e5b2-7c01-4e13-9c82"])
337-
def test_auth_federation_rejects_non_uuid_target(server: TSC.Server, method_name: str, bad_target: str) -> None:
327+
@pytest.mark.parametrize(
328+
"bad_target",
329+
[None, "../../admin", "not-a-uuid", "a4b9c2d7", "", "d3a8e5b2-7c01-4e13-9c82"],
330+
ids=["None", "path", "noise", "short", "empty", "truncated"],
331+
)
332+
def test_auth_federation_rejects_non_uuid_target(
333+
server: TSC.Server, method_name: str, bad_target: object, wrap_as_item: bool
334+
) -> None:
335+
if wrap_as_item:
336+
if method_name in _KEY_METHODS:
337+
item = TSC.AuthFederationKeyItem()
338+
item.kid = bad_target # type: ignore[assignment]
339+
target: object = item
340+
else:
341+
connector = TSC.AuthFederationConnectorItem()
342+
connector.luid = bad_target # type: ignore[assignment]
343+
target = connector
344+
else:
345+
target = bad_target
346+
338347
method = getattr(server.auth_federation, method_name)
339348
with requests_mock.mock() as m:
340349
m.register_uri(requests_mock.ANY, requests_mock.ANY)
341350
with pytest.raises(ValueError, match="Invalid UUID"):
342-
method(bad_target)
351+
method(target)
343352
assert not m.called # must reject before any HTTP call
344353

345354

0 commit comments

Comments
 (0)