Skip to content

Commit d07a36d

Browse files
fix: Improve exception handling
1 parent baf361f commit d07a36d

File tree

3 files changed

+140
-9
lines changed

3 files changed

+140
-9
lines changed

django_postgres_anon/management/commands/anon_fix_permissions.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
from django.core.management.base import BaseCommand
6+
from django.db import DatabaseError, OperationalError
67

78
from django_postgres_anon.models import MaskedRole
89
from django_postgres_anon.utils import create_masked_role
@@ -55,6 +56,6 @@ def fix_role_permissions(self, role_name):
5556
try:
5657
# Use the create_masked_role function which now includes permission fixing
5758
return create_masked_role(role_name)
58-
except Exception as e:
59+
except (DatabaseError, OperationalError) as e:
5960
self.stdout.write(self.style.ERROR(f"Error fixing permissions for {role_name}: {e}"))
6061
return False

django_postgres_anon/utils.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from typing import Any, Dict, List, Optional
33

44
from django.conf import settings
5-
from django.db import connection
5+
from django.db import DatabaseError, OperationalError, connection
66

77
from django_postgres_anon.constants import DEFAULT_POSTGRES_PORT
88

@@ -148,13 +148,13 @@ def create_masked_role(role_name, inherit_from=None):
148148
logger.debug(
149149
f"Granted INSERT, UPDATE, and USAGE on sequence for {table} to {role_name}"
150150
)
151-
except Exception as write_error:
151+
except (DatabaseError, OperationalError) as write_error:
152152
logger.warning(f"Failed to grant write permissions on {table}: {write_error}")
153153

154154
logger.debug(f"Granted SELECT on {table} to {role_name}")
155155
else:
156156
logger.debug(f"Table {table} does not exist, skipping permission grant")
157-
except Exception as table_error:
157+
except (DatabaseError, OperationalError) as table_error:
158158
logger.warning(f"Failed to grant permissions on {table} to {role_name}: {table_error}")
159159

160160
# Grant CONNECT permission on database
@@ -163,18 +163,18 @@ def create_masked_role(role_name, inherit_from=None):
163163
f"GRANT CONNECT ON DATABASE {connection.ops.quote_name(connection.settings_dict['NAME'])} TO {connection.ops.quote_name(role_name)}"
164164
)
165165
logger.debug(f"Granted CONNECT on database to {role_name}")
166-
except Exception as db_error:
166+
except (DatabaseError, OperationalError) as db_error:
167167
logger.warning(f"Failed to grant CONNECT permission: {db_error}")
168168

169169
# Grant USAGE on schema
170170
try:
171171
cursor.execute(f"GRANT USAGE ON SCHEMA public TO {connection.ops.quote_name(role_name)}")
172172
logger.debug(f"Granted USAGE on schema public to {role_name}")
173-
except Exception as schema_error:
173+
except (DatabaseError, OperationalError) as schema_error:
174174
logger.warning(f"Failed to grant USAGE on schema: {schema_error}")
175175

176176
return True
177-
except Exception as e:
177+
except (DatabaseError, OperationalError) as e:
178178
logger.debug(f"Failed to create role {role_name}: {e}")
179179
return False
180180

@@ -379,7 +379,7 @@ def switch_to_role(role_name: str, auto_create: bool = True):
379379
logger.debug(f"Set search_path to 'mask, public' for role {role_name}")
380380

381381
return True
382-
except Exception as e:
382+
except (DatabaseError, OperationalError) as e:
383383
logger.debug(f"Failed to switch to role {role_name}: {e}")
384384
if auto_create:
385385
return create_masked_role(role_name)

tests/test_commands.py

Lines changed: 131 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -565,7 +565,7 @@ def test_anon_drop_requires_confirmation_for_dangerous_ops():
565565

566566
@pytest.mark.django_db
567567
@patch("builtins.input", return_value="yes")
568-
def test_anon_drop_interactive_confirmation_accepted(mock_input):
568+
def test_anon_drop_interactive_confirmation_accepted(_mock_input):
569569
"""Test drop command with interactive confirmation accepted"""
570570
baker.make(MaskingRule, table_name="auth_user", column_name="email")
571571

@@ -789,5 +789,135 @@ def test_anon_fix_permissions_command():
789789
output = str(mock_stdout.write.call_args_list)
790790
assert "Failed" in output or "failed" in output.lower()
791791

792+
# Test with database error in permission fix
793+
from django.db import DatabaseError
794+
795+
with patch("django_postgres_anon.management.commands.anon_fix_permissions.create_masked_role") as mock_create:
796+
mock_create.side_effect = DatabaseError("permission denied")
797+
with patch("sys.stdout", new_callable=MagicMock) as mock_stdout:
798+
call_command("anon_fix_permissions", "--role", "error_role")
799+
output = str(mock_stdout.write.call_args_list)
800+
assert "Error fixing permissions" in output or "permission denied" in output
801+
792802
# Clean up
793803
MaskedRole.objects.all().delete()
804+
805+
806+
@pytest.mark.django_db
807+
def test_create_masked_role_permission_failures():
808+
"""Test permission failure scenarios in create_masked_role to improve coverage"""
809+
from unittest.mock import MagicMock, patch
810+
811+
from django.db import DatabaseError, OperationalError
812+
813+
from django_postgres_anon.utils import create_masked_role
814+
815+
# Test write permission failure
816+
with patch("django.db.connection.cursor") as mock_cursor_ctx:
817+
mock_cursor = MagicMock()
818+
mock_cursor_ctx.return_value.__enter__.return_value = mock_cursor
819+
820+
# Mock table exists check to return True, but write permissions fail
821+
def mock_execute_side_effect(sql, params=None):
822+
if "INSERT, UPDATE ON TABLE" in sql:
823+
raise DatabaseError("permission denied for table test_table")
824+
elif "SELECT table_name FROM information_schema.tables" in sql:
825+
return None # Query for tables
826+
# Let other queries pass through (CREATE ROLE, etc.)
827+
828+
mock_cursor.execute.side_effect = mock_execute_side_effect
829+
mock_cursor.fetchall.return_value = [("auth_user",), ("django_content_type",)] # Mock tables
830+
831+
result = create_masked_role("test_role")
832+
assert result is True # Should still succeed despite write permission failure
833+
834+
# Test table permission failure
835+
with patch("django.db.connection.cursor") as mock_cursor_ctx:
836+
mock_cursor = MagicMock()
837+
mock_cursor_ctx.return_value.__enter__.return_value = mock_cursor
838+
839+
def mock_execute_side_effect(sql, params=None):
840+
if "GRANT SELECT ON" in sql and "TO" in sql:
841+
raise OperationalError("permission denied for table test_table")
842+
843+
mock_cursor.execute.side_effect = mock_execute_side_effect
844+
mock_cursor.fetchall.return_value = [("auth_user",), ("django_content_type",)]
845+
846+
result = create_masked_role("test_role")
847+
assert result is True # Should still succeed despite table permission failure
848+
849+
# Test database CONNECT permission failure
850+
with patch("django.db.connection.cursor") as mock_cursor_ctx:
851+
mock_cursor = MagicMock()
852+
mock_cursor_ctx.return_value.__enter__.return_value = mock_cursor
853+
854+
def mock_execute_side_effect(sql, params=None):
855+
if "GRANT CONNECT ON DATABASE" in sql:
856+
raise DatabaseError("permission denied for database")
857+
858+
mock_cursor.execute.side_effect = mock_execute_side_effect
859+
mock_cursor.fetchall.return_value = [] # No tables
860+
861+
result = create_masked_role("test_role")
862+
assert result is True # Should still succeed despite CONNECT failure
863+
864+
# Test schema USAGE permission failure
865+
with patch("django.db.connection.cursor") as mock_cursor_ctx:
866+
mock_cursor = MagicMock()
867+
mock_cursor_ctx.return_value.__enter__.return_value = mock_cursor
868+
869+
def mock_execute_side_effect(sql, params=None):
870+
if "GRANT USAGE ON SCHEMA" in sql:
871+
raise OperationalError("permission denied for schema public")
872+
873+
mock_cursor.execute.side_effect = mock_execute_side_effect
874+
mock_cursor.fetchall.return_value = [] # No tables
875+
876+
result = create_masked_role("test_role")
877+
assert result is True # Should still succeed despite schema failure
878+
879+
880+
@pytest.mark.django_db
881+
def test_database_role_permission_failures():
882+
"""Test database role switching permission failures"""
883+
from unittest.mock import MagicMock, patch
884+
885+
from django.db import OperationalError
886+
887+
from django_postgres_anon.context_managers import database_role
888+
889+
# Test role switching failure - database_role doesn't have auto_create
890+
with patch("django_postgres_anon.utils.switch_to_role") as mock_switch:
891+
mock_switch.return_value = False # Role switch fails
892+
893+
try:
894+
with database_role("nonexistent_role"):
895+
# Should handle the role switch failure gracefully
896+
pass
897+
except RuntimeError as e:
898+
assert "does not exist" in str(e)
899+
900+
# Test role switching permission failure in switch_to_role itself
901+
with patch("django.db.connection.cursor") as mock_cursor_ctx:
902+
mock_cursor = MagicMock()
903+
mock_cursor_ctx.return_value.__enter__.return_value = mock_cursor
904+
905+
def mock_execute_side_effect(sql, params=None):
906+
if "SET ROLE" in sql:
907+
raise OperationalError("permission denied to set role")
908+
909+
mock_cursor.execute.side_effect = mock_execute_side_effect
910+
911+
from django_postgres_anon.utils import switch_to_role
912+
913+
# Test with auto_create=True
914+
with patch("django_postgres_anon.utils.create_masked_role") as mock_create:
915+
mock_create.return_value = True
916+
917+
result = switch_to_role("test_role", auto_create=True)
918+
mock_create.assert_called_once_with("test_role")
919+
assert result is True
920+
921+
# Test with auto_create=False
922+
result = switch_to_role("test_role", auto_create=False)
923+
assert result is False

0 commit comments

Comments
 (0)