Skip to content

Commit 034dee9

Browse files
test: add integration tests for test owner attribution (CORE-196)
Co-Authored-By: Yosef Arbiv <[email protected]>
1 parent dd3b9fe commit 034dee9

File tree

1 file changed

+328
-0
lines changed

1 file changed

+328
-0
lines changed
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
"""
2+
Integration tests for test owner attribution in dbt_tests artifact table.
3+
Tests that test ownership is correctly extracted from the primary model only,
4+
not aggregated from all parent models (CORE-196).
5+
"""
6+
import contextlib
7+
import json
8+
import uuid
9+
10+
import pytest
11+
from dbt_project import DbtProject
12+
13+
14+
@contextlib.contextmanager
15+
def cleanup_file(path):
16+
"""Context manager to clean up a file after the test."""
17+
try:
18+
yield
19+
finally:
20+
if path.exists():
21+
path.unlink()
22+
23+
24+
def _parse_model_owners(model_owners_value):
25+
"""
26+
Parse the model_owners column value which may be a JSON string or list.
27+
Returns a list of owner strings.
28+
"""
29+
if model_owners_value is None:
30+
return []
31+
if isinstance(model_owners_value, list):
32+
return model_owners_value
33+
if isinstance(model_owners_value, str):
34+
if not model_owners_value or model_owners_value == "[]":
35+
return []
36+
try:
37+
parsed = json.loads(model_owners_value)
38+
return parsed if isinstance(parsed, list) else [model_owners_value]
39+
except json.JSONDecodeError:
40+
return [model_owners_value]
41+
return []
42+
43+
44+
def test_single_parent_test_owner_attribution(dbt_project: DbtProject, tmp_path):
45+
"""
46+
Test that a test on a single model correctly inherits the owner from that model.
47+
This is the baseline case - single parent tests should have the parent's owner.
48+
"""
49+
unique_id = str(uuid.uuid4()).replace("-", "_")
50+
model_name = f"model_single_owner_{unique_id}"
51+
owner_name = "Alice"
52+
53+
model_sql = (
54+
"""
55+
{{ config(meta={'owner': '"""
56+
+ owner_name
57+
+ """'}) }}
58+
select 1 as id
59+
"""
60+
)
61+
62+
schema_yaml = {
63+
"version": 2,
64+
"models": [
65+
{
66+
"name": model_name,
67+
"description": "A model with a single owner for testing",
68+
"columns": [{"name": "id", "tests": ["unique"]}],
69+
}
70+
],
71+
}
72+
73+
dbt_model_path = dbt_project.models_dir_path / "tmp" / f"{model_name}.sql"
74+
with cleanup_file(dbt_model_path):
75+
with dbt_project.write_yaml(
76+
schema_yaml, name=f"schema_single_owner_{unique_id}.yml"
77+
):
78+
dbt_model_path.parent.mkdir(parents=True, exist_ok=True)
79+
dbt_model_path.write_text(model_sql)
80+
81+
dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False
82+
dbt_project.dbt_runner.run(select=model_name)
83+
84+
tests = dbt_project.read_table(
85+
"dbt_tests",
86+
where=f"parent_model_unique_id LIKE '%{model_name}'",
87+
raise_if_empty=True,
88+
)
89+
90+
assert len(tests) == 1, f"Expected 1 test, got {len(tests)}"
91+
test_row = tests[0]
92+
model_owners = _parse_model_owners(test_row.get("model_owners"))
93+
94+
assert model_owners == [
95+
owner_name
96+
], f"Expected model_owners to be ['{owner_name}'], got {model_owners}"
97+
98+
99+
@pytest.mark.skip_targets(["dremio"])
100+
def test_relationship_test_uses_primary_model_owner_only(
101+
dbt_project: DbtProject, tmp_path
102+
):
103+
"""
104+
Test that a relationship test between two models with different owners
105+
only uses the owner from the PRIMARY model (the one being tested),
106+
not from the referenced model.
107+
108+
This is the key test for CORE-196 - previously owners were aggregated
109+
from all parent models, now only the primary model's owner should be used.
110+
"""
111+
unique_id = str(uuid.uuid4()).replace("-", "_")
112+
primary_model_name = f"model_primary_{unique_id}"
113+
referenced_model_name = f"model_referenced_{unique_id}"
114+
primary_owner = "Alice"
115+
referenced_owner = "Bob"
116+
117+
primary_model_sql = f"""
118+
{{{{ config(meta={{'owner': '{primary_owner}'}}) }}}}
119+
select 1 as id, 1 as ref_id
120+
"""
121+
122+
referenced_model_sql = f"""
123+
{{{{ config(meta={{'owner': '{referenced_owner}'}}) }}}}
124+
select 1 as id
125+
"""
126+
127+
schema_yaml = {
128+
"version": 2,
129+
"models": [
130+
{
131+
"name": primary_model_name,
132+
"description": "Primary model with owner Alice",
133+
"columns": [
134+
{"name": "id"},
135+
{
136+
"name": "ref_id",
137+
"tests": [
138+
{
139+
"relationships": {
140+
"to": f"ref('{referenced_model_name}')",
141+
"field": "id",
142+
}
143+
}
144+
],
145+
},
146+
],
147+
},
148+
{
149+
"name": referenced_model_name,
150+
"description": "Referenced model with owner Bob",
151+
"columns": [{"name": "id"}],
152+
},
153+
],
154+
}
155+
156+
primary_model_path = (
157+
dbt_project.models_dir_path / "tmp" / f"{primary_model_name}.sql"
158+
)
159+
referenced_model_path = (
160+
dbt_project.models_dir_path / "tmp" / f"{referenced_model_name}.sql"
161+
)
162+
163+
with cleanup_file(primary_model_path), cleanup_file(referenced_model_path):
164+
with dbt_project.write_yaml(
165+
schema_yaml, name=f"schema_relationship_{unique_id}.yml"
166+
):
167+
primary_model_path.parent.mkdir(parents=True, exist_ok=True)
168+
primary_model_path.write_text(primary_model_sql)
169+
referenced_model_path.write_text(referenced_model_sql)
170+
171+
dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False
172+
dbt_project.dbt_runner.run(
173+
select=f"{primary_model_name} {referenced_model_name}"
174+
)
175+
176+
tests = dbt_project.read_table(
177+
"dbt_tests",
178+
where=f"name LIKE '%relationships%' AND name LIKE '%{primary_model_name}%'",
179+
raise_if_empty=True,
180+
)
181+
182+
assert len(tests) == 1, f"Expected 1 relationship test, got {len(tests)}"
183+
test_row = tests[0]
184+
model_owners = _parse_model_owners(test_row.get("model_owners"))
185+
186+
assert model_owners == [
187+
primary_owner
188+
], f"Expected model_owners to be ['{primary_owner}'] (primary model only), got {model_owners}. Referenced model owner '{referenced_owner}' should NOT be included."
189+
190+
191+
@pytest.mark.skip_targets(["dremio"])
192+
def test_relationship_test_no_owner_on_primary_model(dbt_project: DbtProject, tmp_path):
193+
"""
194+
Test that when the primary model has no owner but the referenced model does,
195+
the test should have empty model_owners (not inherit from referenced model).
196+
"""
197+
unique_id = str(uuid.uuid4()).replace("-", "_")
198+
primary_model_name = f"model_no_owner_{unique_id}"
199+
referenced_model_name = f"model_with_owner_{unique_id}"
200+
referenced_owner = "Bob"
201+
202+
primary_model_sql = """
203+
select 1 as id, 1 as ref_id
204+
"""
205+
206+
referenced_model_sql = f"""
207+
{{{{ config(meta={{'owner': '{referenced_owner}'}}) }}}}
208+
select 1 as id
209+
"""
210+
211+
schema_yaml = {
212+
"version": 2,
213+
"models": [
214+
{
215+
"name": primary_model_name,
216+
"description": "Primary model with NO owner",
217+
"columns": [
218+
{"name": "id"},
219+
{
220+
"name": "ref_id",
221+
"tests": [
222+
{
223+
"relationships": {
224+
"to": f"ref('{referenced_model_name}')",
225+
"field": "id",
226+
}
227+
}
228+
],
229+
},
230+
],
231+
},
232+
{
233+
"name": referenced_model_name,
234+
"description": "Referenced model with owner Bob",
235+
"columns": [{"name": "id"}],
236+
},
237+
],
238+
}
239+
240+
primary_model_path = (
241+
dbt_project.models_dir_path / "tmp" / f"{primary_model_name}.sql"
242+
)
243+
referenced_model_path = (
244+
dbt_project.models_dir_path / "tmp" / f"{referenced_model_name}.sql"
245+
)
246+
247+
with cleanup_file(primary_model_path), cleanup_file(referenced_model_path):
248+
with dbt_project.write_yaml(
249+
schema_yaml, name=f"schema_no_owner_{unique_id}.yml"
250+
):
251+
primary_model_path.parent.mkdir(parents=True, exist_ok=True)
252+
primary_model_path.write_text(primary_model_sql)
253+
referenced_model_path.write_text(referenced_model_sql)
254+
255+
dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False
256+
dbt_project.dbt_runner.run(
257+
select=f"{primary_model_name} {referenced_model_name}"
258+
)
259+
260+
tests = dbt_project.read_table(
261+
"dbt_tests",
262+
where=f"name LIKE '%relationships%' AND name LIKE '%{primary_model_name}%'",
263+
raise_if_empty=True,
264+
)
265+
266+
assert len(tests) == 1, f"Expected 1 relationship test, got {len(tests)}"
267+
test_row = tests[0]
268+
model_owners = _parse_model_owners(test_row.get("model_owners"))
269+
270+
assert (
271+
model_owners == []
272+
), f"Expected model_owners to be empty (primary model has no owner), got {model_owners}. Referenced model owner '{referenced_owner}' should NOT be inherited."
273+
274+
275+
def test_owner_deduplication(dbt_project: DbtProject, tmp_path):
276+
"""
277+
Test that duplicate owners in a model's owner field are deduplicated.
278+
For example, if owner is "Alice,Bob,Alice", the result should be ["Alice", "Bob"].
279+
"""
280+
unique_id = str(uuid.uuid4()).replace("-", "_")
281+
model_name = f"model_dup_owner_{unique_id}"
282+
283+
model_sql = """
284+
{{ config(meta={'owner': 'Alice,Bob,Alice'}) }}
285+
select 1 as id
286+
"""
287+
288+
schema_yaml = {
289+
"version": 2,
290+
"models": [
291+
{
292+
"name": model_name,
293+
"description": "A model with duplicate owners for testing deduplication",
294+
"columns": [{"name": "id", "tests": ["unique"]}],
295+
}
296+
],
297+
}
298+
299+
dbt_model_path = dbt_project.models_dir_path / "tmp" / f"{model_name}.sql"
300+
with cleanup_file(dbt_model_path):
301+
with dbt_project.write_yaml(
302+
schema_yaml, name=f"schema_dup_owner_{unique_id}.yml"
303+
):
304+
dbt_model_path.parent.mkdir(parents=True, exist_ok=True)
305+
dbt_model_path.write_text(model_sql)
306+
307+
dbt_project.dbt_runner.vars["disable_dbt_artifacts_autoupload"] = False
308+
dbt_project.dbt_runner.run(select=model_name)
309+
310+
tests = dbt_project.read_table(
311+
"dbt_tests",
312+
where=f"parent_model_unique_id LIKE '%{model_name}'",
313+
raise_if_empty=True,
314+
)
315+
316+
assert len(tests) == 1, f"Expected 1 test, got {len(tests)}"
317+
test_row = tests[0]
318+
model_owners = _parse_model_owners(test_row.get("model_owners"))
319+
320+
assert (
321+
len(model_owners) == 2
322+
), f"Expected 2 unique owners, got {len(model_owners)}: {model_owners}"
323+
assert (
324+
"Alice" in model_owners
325+
), f"Expected 'Alice' in model_owners, got {model_owners}"
326+
assert (
327+
"Bob" in model_owners
328+
), f"Expected 'Bob' in model_owners, got {model_owners}"

0 commit comments

Comments
 (0)