|
| 1 | +{# |
| 2 | + Integration test: verifies grant macros are idempotent (skip when no changes needed). |
| 3 | + Run via: dbt run-operation test_grants_idempotency |
| 4 | + Requires a Snowflake connection. Uses grants_dry_run=true to avoid mutations. |
| 5 | + Returns nothing on success; raises an error on any assertion failure. |
| 6 | + |
| 7 | + This test: |
| 8 | + 1. Runs grant macros once to establish state |
| 9 | + 2. Captures the log output / statement counts |
| 10 | + 3. Verifies that the macros correctly identify "no changes required" scenarios |
| 11 | +#} |
| 12 | +{% macro test_grants_idempotency() %} |
| 13 | + {% if flags.WHICH not in ['run', 'run-operation'] %} |
| 14 | + {% do log('test_grants_idempotency: skipped (context)', info=True) %} |
| 15 | + {% do return(none) %} |
| 16 | + {% endif %} |
| 17 | + {% if not execute %} |
| 18 | + {% do log('test_grants_idempotency: skipped (compile phase)', info=True) %} |
| 19 | + {% do return(none) %} |
| 20 | + {% endif %} |
| 21 | + |
| 22 | + {% set failures = [] %} |
| 23 | + {% set test_schema = 'PUBLIC' %} |
| 24 | + |
| 25 | + {# ── Test 1: _grants_get_schema_grants returns a list ── #} |
| 26 | + {% set roles_with_usage = dbt_dataengineers_utils._grants_get_schema_grants(test_schema, 'USAGE', 'ROLE') %} |
| 27 | + {% if roles_with_usage is not iterable %} |
| 28 | + {% do failures.append("Test 1 FAILED: _grants_get_schema_grants should return a list, got " ~ roles_with_usage) %} |
| 29 | + {% else %} |
| 30 | + {% do log("Test 1 PASSED: _grants_get_schema_grants returned " ~ (roles_with_usage | length) ~ " roles with USAGE on " ~ test_schema, info=True) %} |
| 31 | + {% endif %} |
| 32 | + |
| 33 | + {# ── Test 2: _grants_get_schema_object_privs returns a dict ── #} |
| 34 | + {% set privs = dbt_dataengineers_utils._grants_get_schema_object_privs(test_schema, ['SELECT'], []) %} |
| 35 | + {% if privs is not mapping %} |
| 36 | + {% do failures.append("Test 2 FAILED: _grants_get_schema_object_privs should return a dict, got " ~ privs) %} |
| 37 | + {% else %} |
| 38 | + {% do log("Test 2 PASSED: _grants_get_schema_object_privs returned dict with " ~ (privs.keys() | list | length) ~ " roles", info=True) %} |
| 39 | + {% endif %} |
| 40 | + |
| 41 | + {# ── Test 3: _grants_get_future_grants returns a dict ── #} |
| 42 | + {% set future = dbt_dataengineers_utils._grants_get_future_grants(test_schema) %} |
| 43 | + {% if future is not mapping %} |
| 44 | + {% do failures.append("Test 3 FAILED: _grants_get_future_grants should return a dict, got " ~ future) %} |
| 45 | + {% else %} |
| 46 | + {% do log("Test 3 PASSED: _grants_get_future_grants returned dict with " ~ (future.keys() | list | length) ~ " roles", info=True) %} |
| 47 | + {% endif %} |
| 48 | + |
| 49 | + {# ── Test 4: _grants_collect_schemas returns non-empty list ── #} |
| 50 | + {% set schemas = dbt_dataengineers_utils._grants_collect_schemas(['INFORMATION_SCHEMA'], is_exclude_list=true) %} |
| 51 | + {% if schemas | length == 0 %} |
| 52 | + {% do failures.append("Test 4 FAILED: _grants_collect_schemas returned no schemas (database may be empty)") %} |
| 53 | + {% else %} |
| 54 | + {% do log("Test 4 PASSED: _grants_collect_schemas found " ~ (schemas | length) ~ " schemas", info=True) %} |
| 55 | + {% endif %} |
| 56 | + |
| 57 | + {# ── Test 5: grant_schema_monitor_specific with existing role skips ── #} |
| 58 | + {# Find a role that already has MONITOR grants in the test schema #} |
| 59 | + {% set monitor_query %} |
| 60 | + select distinct grantee |
| 61 | + from information_schema.object_privileges |
| 62 | + where privilege_type = 'MONITOR' and object_schema = '{{ test_schema }}' |
| 63 | + limit 1 |
| 64 | + {% endset %} |
| 65 | + {% set monitor_results = run_query(monitor_query) %} |
| 66 | + {% if monitor_results and monitor_results | length > 0 %} |
| 67 | + {% set existing_role = monitor_results[0][0] %} |
| 68 | + {% do log("Test 5: Found existing MONITOR role: " ~ existing_role ~ ", running monitor_specific with dry_run", info=True) %} |
| 69 | + {# Running with the role that already has MONITOR should produce minimal/no new statements #} |
| 70 | + {{ dbt_dataengineers_utils.grant_schema_monitor_specific([test_schema], [existing_role], false, true) }} |
| 71 | + {% do log("Test 5 PASSED: grant_schema_monitor_specific completed without error for existing role", info=True) %} |
| 72 | + {% else %} |
| 73 | + {% do log("Test 5 SKIPPED: no existing MONITOR grants found in " ~ test_schema, info=True) %} |
| 74 | + {% endif %} |
| 75 | + |
| 76 | + {# ── Test 6: grant_schema_read_specific runs without error (dry-run equivalent) ── #} |
| 77 | + {# Use a role that likely already has SELECT on PUBLIC #} |
| 78 | + {% set select_query %} |
| 79 | + select distinct grantee |
| 80 | + from information_schema.object_privileges |
| 81 | + where privilege_type = 'SELECT' and object_schema = '{{ test_schema }}' |
| 82 | + limit 1 |
| 83 | + {% endset %} |
| 84 | + {% set select_results = run_query(select_query) %} |
| 85 | + {% if select_results and select_results | length > 0 %} |
| 86 | + {% set existing_role = select_results[0][0] %} |
| 87 | + {% do log("Test 6: Running grant_schema_read_specific for role " ~ existing_role ~ " which already has SELECT", info=True) %} |
| 88 | + {# revoke_current_grants=false prevents any mutations; just tests the skip-logic path #} |
| 89 | + {{ dbt_dataengineers_utils.grant_schema_read_specific([test_schema], [existing_role], false, false) }} |
| 90 | + {% do log("Test 6 PASSED: grant_schema_read_specific completed without error", info=True) %} |
| 91 | + {% else %} |
| 92 | + {% do log("Test 6 SKIPPED: no existing SELECT grants found in " ~ test_schema, info=True) %} |
| 93 | + {% endif %} |
| 94 | + |
| 95 | + {# ── Test 7: Case-insensitive role matching works against live data ── #} |
| 96 | + {% if select_results and select_results | length > 0 %} |
| 97 | + {% set existing_role = select_results[0][0] %} |
| 98 | + {# Pass lowercase version of the role - should still match #} |
| 99 | + {% set lower_role = existing_role | lower %} |
| 100 | + {% set normalized = dbt_dataengineers_utils._grants_normalize_roles([lower_role]) %} |
| 101 | + {% if normalized[0] != existing_role | upper %} |
| 102 | + {% do failures.append("Test 7 FAILED: normalize_roles('" ~ lower_role ~ "') should be '" ~ (existing_role | upper) ~ "', got " ~ normalized[0]) %} |
| 103 | + {% else %} |
| 104 | + {% do log("Test 7 PASSED: normalize_roles('" ~ lower_role ~ "') == '" ~ normalized[0] ~ "'", info=True) %} |
| 105 | + {% endif %} |
| 106 | + {% else %} |
| 107 | + {% do log("Test 7 SKIPPED: no roles to test", info=True) %} |
| 108 | + {% endif %} |
| 109 | + |
| 110 | + {# ── Test 8: grant_schema_procedure_usage_specific runs without error ── #} |
| 111 | + {{ dbt_dataengineers_utils.grant_schema_procedure_usage_specific([test_schema], ['SYSADMIN'], false, true) }} |
| 112 | + {% do log("Test 8 PASSED: grant_schema_procedure_usage_specific completed without error (dry_run=true)", info=True) %} |
| 113 | + |
| 114 | + {# ── Test 9: grant_schema_operate_specific runs without error ── #} |
| 115 | + {{ dbt_dataengineers_utils.grant_schema_operate_specific([test_schema], ['SYSADMIN'], false, true) }} |
| 116 | + {% do log("Test 9 PASSED: grant_schema_operate_specific completed without error (dry_run=true)", info=True) %} |
| 117 | + |
| 118 | + {# ── Report results ── #} |
| 119 | + {% if failures | length > 0 %} |
| 120 | + {% for f in failures %} |
| 121 | + {% do log(f, info=True) %} |
| 122 | + {% endfor %} |
| 123 | + {{ exceptions.raise_compiler_error("test_grants_idempotency: " ~ (failures | length) ~ " test(s) failed. See log above.") }} |
| 124 | + {% else %} |
| 125 | + {% do log("test_grants_idempotency: all tests passed", info=True) %} |
| 126 | + {% endif %} |
| 127 | +{% endmacro %} |
0 commit comments