|
| 1 | +from datetime import time |
1 | 2 | from pathlib import Path |
2 | 3 |
|
3 | 4 | import pytest |
@@ -275,6 +276,164 @@ def test_parse_response_with_inline_schedule_no_id(server: TSC.Server) -> None: |
275 | 276 | assert subs[0].schedule is not None |
276 | 277 |
|
277 | 278 |
|
| 279 | +def test_update_manually_built_subscription_emits_flag_false(server: TSC.Server) -> None: |
| 280 | + """Manual-build update() footgun coverage. A fresh SubscriptionItem |
| 281 | + constructed locally, with _id assigned to point at an existing |
| 282 | + subscription, must round-trip refresh_extract_triggered=False on the wire |
| 283 | + when the caller has not touched the flag. This is the exact case the |
| 284 | + property docstring warns callers away from -- pin the behavior so we |
| 285 | + notice if the emit changes. |
| 286 | + """ |
| 287 | + from tableauserverclient.server.request_factory import RequestFactory |
| 288 | + |
| 289 | + response_xml = ( |
| 290 | + '<tsResponse xmlns="http://tableau.com/api">' |
| 291 | + ' <subscription id="existing-sub-id" subject="Updated subject" attachImage="true" attachPdf="false"' |
| 292 | + ' suspended="false" refreshExtractTriggered="false">' |
| 293 | + ' <content id="view-id" type="View" sendIfViewEmpty="true" />' |
| 294 | + ' <schedule id="sched-id" name="Weekly" />' |
| 295 | + ' <user id="user-id" />' |
| 296 | + " </subscription>" |
| 297 | + "</tsResponse>" |
| 298 | + ) |
| 299 | + target = TSC.Target("view-id", "view") |
| 300 | + sub = TSC.SubscriptionItem("Updated subject", "sched-id", "user-id", target) |
| 301 | + sub._id = "existing-sub-id" # type: ignore[assignment] |
| 302 | + |
| 303 | + with requests_mock.mock() as m: |
| 304 | + m.put(server.subscriptions.baseurl + "/existing-sub-id", text=response_xml) |
| 305 | + server.subscriptions.update(sub) |
| 306 | + body = m.last_request.text or "" |
| 307 | + |
| 308 | + assert 'refreshExtractTriggered="false"' in body |
| 309 | + # Sanity: also confirm update_req produces the same thing without a server |
| 310 | + body_direct = RequestFactory.Subscription.update_req(sub).decode("utf-8") |
| 311 | + assert 'refreshExtractTriggered="false"' in body_direct |
| 312 | + |
| 313 | + |
| 314 | +def test_update_manually_built_extract_refresh_subscription_emits_flag_true(server: TSC.Server) -> None: |
| 315 | + """Manual-build update() coverage for the on_extract_refresh() variant. A |
| 316 | + fresh SubscriptionItem constructed via on_extract_refresh(), with _id |
| 317 | + assigned, must emit refreshExtractTriggered='true' when sent through |
| 318 | + update(). |
| 319 | + """ |
| 320 | + response_xml = ( |
| 321 | + '<tsResponse xmlns="http://tableau.com/api">' |
| 322 | + ' <subscription id="existing-sub-id" subject="On refresh" attachImage="true" attachPdf="false"' |
| 323 | + ' suspended="false" refreshExtractTriggered="true">' |
| 324 | + ' <content id="view-id" type="View" sendIfViewEmpty="true" />' |
| 325 | + ' <schedule id="refresh-sched-id" name="Nightly refresh" />' |
| 326 | + ' <user id="user-id" />' |
| 327 | + " </subscription>" |
| 328 | + "</tsResponse>" |
| 329 | + ) |
| 330 | + target = TSC.Target("view-id", "view") |
| 331 | + sub = TSC.SubscriptionItem.on_extract_refresh( |
| 332 | + subject="On refresh", |
| 333 | + extract_refresh_schedule_id="refresh-sched-id", |
| 334 | + user_id="user-id", |
| 335 | + target=target, |
| 336 | + ) |
| 337 | + sub._id = "existing-sub-id" # type: ignore[assignment] |
| 338 | + |
| 339 | + with requests_mock.mock() as m: |
| 340 | + m.put(server.subscriptions.baseurl + "/existing-sub-id", text=response_xml) |
| 341 | + server.subscriptions.update(sub) |
| 342 | + body = m.last_request.text or "" |
| 343 | + |
| 344 | + assert 'refreshExtractTriggered="true"' in body |
| 345 | + |
| 346 | + |
| 347 | +def test_update_round_trip_preserves_refresh_extract_triggered(server: TSC.Server) -> None: |
| 348 | + """Fetch-then-mutate-then-update round-trip preserves the flag. Parse a |
| 349 | + subscription XML with refreshExtractTriggered='true', mutate an unrelated |
| 350 | + field (subject), send through update(), and confirm the emitted body |
| 351 | + still carries refreshExtractTriggered='true'. This is the safe pattern |
| 352 | + the property docstring recommends and it deserves an explicit test. |
| 353 | + """ |
| 354 | + parsed = TSC.SubscriptionItem.from_response( |
| 355 | + ( |
| 356 | + b'<tsResponse xmlns="http://tableau.com/api">' |
| 357 | + b" <subscriptions>" |
| 358 | + b' <subscription id="existing-sub-id" subject="Original subject"' |
| 359 | + b' attachImage="true" attachPdf="false" suspended="false"' |
| 360 | + b' refreshExtractTriggered="true">' |
| 361 | + b' <content id="view-id" type="View" sendIfViewEmpty="false" />' |
| 362 | + b' <schedule id="refresh-sched-id" name="Nightly refresh" />' |
| 363 | + b' <user id="user-id" />' |
| 364 | + b" </subscription>" |
| 365 | + b" </subscriptions>" |
| 366 | + b"</tsResponse>" |
| 367 | + ), |
| 368 | + {"t": "http://tableau.com/api"}, |
| 369 | + ) |
| 370 | + assert len(parsed) == 1 |
| 371 | + sub = parsed[0] |
| 372 | + assert sub.refresh_extract_triggered is True |
| 373 | + sub.subject = "Mutated subject" |
| 374 | + |
| 375 | + response_xml = ( |
| 376 | + '<tsResponse xmlns="http://tableau.com/api">' |
| 377 | + ' <subscription id="existing-sub-id" subject="Mutated subject" attachImage="true" attachPdf="false"' |
| 378 | + ' suspended="false" refreshExtractTriggered="true">' |
| 379 | + ' <content id="view-id" type="View" sendIfViewEmpty="false" />' |
| 380 | + ' <schedule id="refresh-sched-id" name="Nightly refresh" />' |
| 381 | + ' <user id="user-id" />' |
| 382 | + " </subscription>" |
| 383 | + "</tsResponse>" |
| 384 | + ) |
| 385 | + with requests_mock.mock() as m: |
| 386 | + m.put(server.subscriptions.baseurl + "/existing-sub-id", text=response_xml) |
| 387 | + server.subscriptions.update(sub) |
| 388 | + body = m.last_request.text or "" |
| 389 | + |
| 390 | + assert 'refreshExtractTriggered="true"' in body |
| 391 | + assert 'subject="Mutated subject"' in body |
| 392 | + |
| 393 | + |
| 394 | +def test_update_falls_back_to_schedule_object_id_on_cloud(server: TSC.Server) -> None: |
| 395 | + """Cloud fetch-then-update guard. When a SubscriptionItem arrives with |
| 396 | + schedule_id=None but a parsed schedule object whose id is populated |
| 397 | + (a shape a future _parse_element could produce, or a shape a caller can |
| 398 | + build directly), update() must fall back to schedule.id instead of |
| 399 | + rejecting the item. This unblocks the fetch-then-update pattern the |
| 400 | + refresh_extract_triggered property docstring recommends on Cloud. |
| 401 | + """ |
| 402 | + target = TSC.Target("view-id", "view") |
| 403 | + sub = TSC.SubscriptionItem("Cloud sub", None, "user-id", target) |
| 404 | + sub._id = "existing-sub-id" # type: ignore[assignment] |
| 405 | + # Build a schedule object with an id but leave schedule_id None -- the |
| 406 | + # Cloud inline-schedule case the endpoint fallback exists to handle. |
| 407 | + schedule = TSC.ScheduleItem( |
| 408 | + "Nightly refresh", |
| 409 | + 50, |
| 410 | + TSC.ScheduleItem.Type.Extract, |
| 411 | + "Parallel", |
| 412 | + TSC.DailyInterval(time(2, 0)), |
| 413 | + ) |
| 414 | + schedule._id = "inline-sched-id" |
| 415 | + sub.schedule = schedule # type: ignore[assignment] |
| 416 | + assert sub.schedule_id is None |
| 417 | + assert sub.schedule.id == "inline-sched-id" # type: ignore[attr-defined] |
| 418 | + |
| 419 | + response_xml = ( |
| 420 | + '<tsResponse xmlns="http://tableau.com/api">' |
| 421 | + ' <subscription id="existing-sub-id" subject="Cloud sub" attachImage="true" attachPdf="false"' |
| 422 | + ' suspended="false" refreshExtractTriggered="false">' |
| 423 | + ' <content id="view-id" type="View" sendIfViewEmpty="true" />' |
| 424 | + ' <schedule id="inline-sched-id" name="Nightly refresh" />' |
| 425 | + ' <user id="user-id" />' |
| 426 | + " </subscription>" |
| 427 | + "</tsResponse>" |
| 428 | + ) |
| 429 | + with requests_mock.mock() as m: |
| 430 | + m.put(server.subscriptions.baseurl + "/existing-sub-id", text=response_xml) |
| 431 | + server.subscriptions.update(sub) |
| 432 | + body = m.last_request.text or "" |
| 433 | + |
| 434 | + assert 'id="inline-sched-id"' in body |
| 435 | + |
| 436 | + |
278 | 437 | def test_parse_response_missing_refresh_extract_triggered_defaults_false(server: TSC.Server) -> None: |
279 | 438 | """Backward compatibility: a subscription XML element without the attribute |
280 | 439 | parses back to refresh_extract_triggered=False. |
|
0 commit comments