|
3 | 3 | Covers models, tests, seeds, and snapshots group assignment and artifact table correctness. |
4 | 4 | """ |
5 | 5 | import contextlib |
| 6 | +import json |
6 | 7 | import uuid |
| 8 | +from typing import List, Optional, Union |
7 | 9 |
|
8 | 10 | import pytest |
9 | 11 | from dbt_project import DbtProject |
|
26 | 28 | } |
27 | 29 |
|
28 | 30 |
|
| 31 | +def _parse_owners(owners_value: Optional[Union[str, List[str]]]) -> List[str]: |
| 32 | + """Parse an owner column value which may be a JSON string or a list.""" |
| 33 | + if owners_value is None: |
| 34 | + return [] |
| 35 | + if isinstance(owners_value, list): |
| 36 | + return owners_value |
| 37 | + if isinstance(owners_value, str): |
| 38 | + if not owners_value or owners_value == "[]": |
| 39 | + return [] |
| 40 | + try: |
| 41 | + parsed = json.loads(owners_value) |
| 42 | + return parsed if isinstance(parsed, list) else [parsed] |
| 43 | + except json.JSONDecodeError: |
| 44 | + return [owners_value] |
| 45 | + return [] |
| 46 | + |
| 47 | + |
29 | 48 | @contextlib.contextmanager |
30 | 49 | def _write_group_config(dbt_project: DbtProject, group_config: dict, name: str): |
31 | 50 | """Context manager to write a group config YAML file in the dbt project and clean up after.""" |
@@ -540,3 +559,171 @@ def test_snapshot_group_attribute(dbt_project: DbtProject, tmp_path): |
540 | 559 | assert_group_name_in_run_results_view( |
541 | 560 | dbt_project, "snapshot_run_results", snapshot_name, group_name |
542 | 561 | ) |
| 562 | + |
| 563 | + |
| 564 | +@pytest.mark.skip_for_dbt_fusion |
| 565 | +def test_model_owner_derived_from_group(dbt_project: DbtProject, tmp_path): |
| 566 | + """ |
| 567 | + A model assigned to a group but WITHOUT a direct owner should inherit the |
| 568 | + group's owner (email preferred) in the dbt_models artifact table. |
| 569 | + """ |
| 570 | + unique_id = str(uuid.uuid4()).replace("-", "_") |
| 571 | + model_name = f"model_group_owner_{unique_id}" |
| 572 | + group_name = f"test_group_{unique_id}" |
| 573 | + model_sql = """ |
| 574 | + select 1 as col |
| 575 | + """ |
| 576 | + schema_yaml = { |
| 577 | + "version": 2, |
| 578 | + "models": [ |
| 579 | + { |
| 580 | + "name": model_name, |
| 581 | + "config": {"group": group_name}, |
| 582 | + "description": "A model assigned to a group without a direct owner", |
| 583 | + } |
| 584 | + ], |
| 585 | + } |
| 586 | + group_config = { |
| 587 | + "groups": [ |
| 588 | + { |
| 589 | + "name": group_name, |
| 590 | + "owner": {"name": OWNER_NAME, "email": OWNER_EMAIL}, |
| 591 | + } |
| 592 | + ] |
| 593 | + } |
| 594 | + with _write_group_config( |
| 595 | + dbt_project, group_config, name=f"groups_model_owner_{unique_id}.yml" |
| 596 | + ), dbt_project.write_yaml( |
| 597 | + schema_yaml, name=f"schema_model_group_owner_{unique_id}.yml" |
| 598 | + ): |
| 599 | + dbt_model_path = dbt_project.models_dir_path / "tmp" / f"{model_name}.sql" |
| 600 | + dbt_model_path.parent.mkdir(parents=True, exist_ok=True) |
| 601 | + dbt_model_path.write_text(model_sql) |
| 602 | + try: |
| 603 | + dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False |
| 604 | + dbt_project.dbt_runner.vars["disable_run_results"] = False |
| 605 | + dbt_project.dbt_runner.run(select=model_name) |
| 606 | + |
| 607 | + models = dbt_project.read_table( |
| 608 | + "dbt_models", where=f"name = '{model_name}'", raise_if_empty=True |
| 609 | + ) |
| 610 | + assert len(models) == 1, f"Expected 1 model, got {len(models)}" |
| 611 | + owners = _parse_owners(models[0].get("owner")) |
| 612 | + assert owners == [ |
| 613 | + OWNER_EMAIL |
| 614 | + ], f"Expected owner ['{OWNER_EMAIL}'] derived from group, got {owners}" |
| 615 | + finally: |
| 616 | + if dbt_model_path.exists(): |
| 617 | + dbt_model_path.unlink() |
| 618 | + |
| 619 | + |
| 620 | +def test_test_owner_derived_from_group(dbt_project: DbtProject, tmp_path): |
| 621 | + """ |
| 622 | + A test on a grouped model WITHOUT a direct owner should inherit the group's |
| 623 | + owner as model_owners in the dbt_tests artifact table (this feeds |
| 624 | + elementary_test_results.owners and alert routing). |
| 625 | + """ |
| 626 | + unique_id = str(uuid.uuid4()).replace("-", "_") |
| 627 | + model_name = f"model_group_test_owner_{unique_id}" |
| 628 | + group_name = f"test_group_{unique_id}" |
| 629 | + model_sql = """ |
| 630 | + select 1 as col |
| 631 | + """ |
| 632 | + schema_yaml = { |
| 633 | + "version": 2, |
| 634 | + "models": [ |
| 635 | + { |
| 636 | + "name": model_name, |
| 637 | + "config": {"group": group_name}, |
| 638 | + "description": "A grouped model without a direct owner", |
| 639 | + "columns": [{"name": "col", "tests": ["unique"]}], |
| 640 | + } |
| 641 | + ], |
| 642 | + } |
| 643 | + group_config = { |
| 644 | + "groups": [ |
| 645 | + { |
| 646 | + "name": group_name, |
| 647 | + "owner": {"name": OWNER_NAME, "email": OWNER_EMAIL}, |
| 648 | + } |
| 649 | + ] |
| 650 | + } |
| 651 | + with _write_group_config( |
| 652 | + dbt_project, group_config, name=f"groups_test_owner_{unique_id}.yml" |
| 653 | + ), dbt_project.write_yaml( |
| 654 | + schema_yaml, name=f"schema_test_group_owner_{unique_id}.yml" |
| 655 | + ): |
| 656 | + dbt_model_path = dbt_project.models_dir_path / "tmp" / f"{model_name}.sql" |
| 657 | + dbt_model_path.parent.mkdir(parents=True, exist_ok=True) |
| 658 | + dbt_model_path.write_text(model_sql) |
| 659 | + try: |
| 660 | + dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False |
| 661 | + dbt_project.dbt_runner.run(select=model_name) |
| 662 | + tests = dbt_project.read_table( |
| 663 | + "dbt_tests", |
| 664 | + where=f"parent_model_unique_id LIKE '%{model_name}'", |
| 665 | + raise_if_empty=True, |
| 666 | + ) |
| 667 | + assert len(tests) == 1, f"Expected 1 test, got {len(tests)}" |
| 668 | + model_owners = _parse_owners(tests[0].get("model_owners")) |
| 669 | + assert model_owners == [ |
| 670 | + OWNER_EMAIL |
| 671 | + ], f"Expected model_owners ['{OWNER_EMAIL}'] derived from group, got {model_owners}" |
| 672 | + finally: |
| 673 | + if dbt_model_path.exists(): |
| 674 | + dbt_model_path.unlink() |
| 675 | + |
| 676 | + |
| 677 | +@pytest.mark.skip_for_dbt_fusion |
| 678 | +def test_direct_owner_takes_precedence_over_group(dbt_project: DbtProject, tmp_path): |
| 679 | + """ |
| 680 | + When a model has BOTH a direct owner and a group, the direct owner wins and |
| 681 | + the group owner is not added (mirrors dbt's own config precedence). |
| 682 | + """ |
| 683 | + unique_id = str(uuid.uuid4()).replace("-", "_") |
| 684 | + model_name = f"model_direct_owner_{unique_id}" |
| 685 | + group_name = f"test_group_{unique_id}" |
| 686 | + direct_owner = "Alice" |
| 687 | + model_sql = f""" |
| 688 | + {{{{ config(meta={{'owner': '{direct_owner}'}}) }}}} |
| 689 | + select 1 as col |
| 690 | + """ |
| 691 | + schema_yaml = { |
| 692 | + "version": 2, |
| 693 | + "models": [ |
| 694 | + { |
| 695 | + "name": model_name, |
| 696 | + "config": {"group": group_name}, |
| 697 | + "description": "A grouped model with a direct owner", |
| 698 | + } |
| 699 | + ], |
| 700 | + } |
| 701 | + group_config = { |
| 702 | + "groups": [ |
| 703 | + { |
| 704 | + "name": group_name, |
| 705 | + "owner": {"name": OWNER_NAME, "email": OWNER_EMAIL}, |
| 706 | + } |
| 707 | + ] |
| 708 | + } |
| 709 | + with _write_group_config( |
| 710 | + dbt_project, group_config, name=f"groups_direct_owner_{unique_id}.yml" |
| 711 | + ), dbt_project.write_yaml(schema_yaml, name=f"schema_direct_owner_{unique_id}.yml"): |
| 712 | + dbt_model_path = dbt_project.models_dir_path / "tmp" / f"{model_name}.sql" |
| 713 | + dbt_model_path.parent.mkdir(parents=True, exist_ok=True) |
| 714 | + dbt_model_path.write_text(model_sql) |
| 715 | + try: |
| 716 | + dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False |
| 717 | + dbt_project.dbt_runner.run(select=model_name) |
| 718 | + |
| 719 | + models = dbt_project.read_table( |
| 720 | + "dbt_models", where=f"name = '{model_name}'", raise_if_empty=True |
| 721 | + ) |
| 722 | + assert len(models) == 1, f"Expected 1 model, got {len(models)}" |
| 723 | + owners = _parse_owners(models[0].get("owner")) |
| 724 | + assert owners == [ |
| 725 | + direct_owner |
| 726 | + ], f"Expected direct owner ['{direct_owner}'] to win over group, got {owners}" |
| 727 | + finally: |
| 728 | + if dbt_model_path.exists(): |
| 729 | + dbt_model_path.unlink() |
0 commit comments