66import pytest
77from django .core .management import call_command
88
9+ OWASP_EMPLOYEE_BADGE_NAME = "OWASP Employee"
10+
911
1012@pytest .fixture
1113def mock_badge ():
1214 badge = MagicMock ()
13- badge .name = "OWASP Employee"
15+ badge .name = OWASP_EMPLOYEE_BADGE_NAME
1416 badge .css_class = "fa-user-shield"
1517 badge .id = 1
1618 return badge
@@ -24,32 +26,28 @@ class TestSyncUserBadgesCommand:
2426 def test_sync_owasp_employee_badge (self , mock_user_filter , mock_badge_get_or_create ):
2527 # Set up badge mock
2628 mock_badge = MagicMock ()
27- mock_badge .name = "OWASP Employee"
29+ mock_badge .name = OWASP_EMPLOYEE_BADGE_NAME
2830 mock_badge .id = 1
2931 mock_badge_get_or_create .return_value = (mock_badge , False )
3032
31- # Set up employee mocks
33+ # Set up employee mocks - with exclude() method properly mocked
3234 mock_employee = MagicMock ()
33- mock_employee .badges .filter .return_value .exists .return_value = False
3435 mock_employees = MagicMock ()
35- mock_employees .__iter__ .return_value = [mock_employee ]
36- mock_employees .count .return_value = 1
36+ mock_employees_without_badge = MagicMock ()
37+ mock_employees_without_badge .__iter__ .return_value = [mock_employee ]
38+ mock_employees_without_badge .count .return_value = 1
39+ mock_employees .exclude .return_value = mock_employees_without_badge
3740
3841 # Set up former employee mocks
3942 mock_former_employee = MagicMock ()
4043 mock_former_employees = MagicMock ()
4144 mock_former_employees .__iter__ .return_value = [mock_former_employee ]
4245 mock_former_employees .count .return_value = 1
4346
44- # Set up the double filter behavior
45- mock_non_employees_first_filter = MagicMock ()
46- mock_non_employees_first_filter .filter .return_value = mock_former_employees
47-
4847 # Configure filter side effects
4948 mock_user_filter .side_effect = [
5049 mock_employees , # is_owasp_employee=True
51- # is_owasp_employee=False, will need .filter(badges=badge)
52- mock_non_employees_first_filter ,
50+ mock_former_employees , # is_owasp_employee=False, badges=badge
5351 ]
5452
5553 # Call the command
@@ -71,7 +69,7 @@ def test_sync_owasp_employee_badge(self, mock_user_filter, mock_badge_get_or_cre
7169 def test_badge_creation (self , mock_user_filter , mock_badge_get_or_create ):
7270 # Set up badge creation mock
7371 mock_badge = MagicMock ()
74- mock_badge .name = "OWASP Employee"
72+ mock_badge .name = OWASP_EMPLOYEE_BADGE_NAME
7573 mock_badge_get_or_create .return_value = (mock_badge , True )
7674
7775 # Set up empty querysets
@@ -88,9 +86,67 @@ def test_badge_creation(self, mock_user_filter, mock_badge_get_or_create):
8886 out = StringIO ()
8987 call_command ("github_sync_user_badges" , stdout = out )
9088
91- # Verify badge creation
92- mock_badge_get_or_create .assert_called_once ()
89+ # Verify badge creation and defaults
90+
91+ mock_badge_get_or_create .assert_called_once_with (
92+ name = "OWASP Employee" ,
93+ defaults = {
94+ "description" : "Official OWASP Employee" ,
95+ "css_class" : "fa-user-shield" ,
96+ "weight" : 100 ,
97+ },
98+ )
9399
94100 # Check command output
95101 output = out .getvalue ()
96102 assert f"Created badge: { mock_badge .name } " in output
103+
104+ @patch ("apps.owasp.models.badge.Badge.objects.get_or_create" )
105+ @patch ("apps.github.models.user.User.objects.filter" )
106+ def test_command_idempotency (self , mock_user_filter , mock_badge_get_or_create ):
107+ """Test that running the command multiple times has the same effect as running it once."""
108+ # Set up badge mock
109+ mock_badge = MagicMock ()
110+ mock_badge .name = OWASP_EMPLOYEE_BADGE_NAME
111+ mock_badge .id = 1
112+ mock_badge_get_or_create .return_value = (mock_badge , False )
113+
114+ # Set up employee mock that already has the badge
115+ mock_employee_with_badge = MagicMock ()
116+ # This employee already has the badge
117+ mock_employee_with_badge .badges .filter .return_value .exists .return_value = True
118+ mock_employees = MagicMock ()
119+ mock_employees .__iter__ .return_value = [mock_employee_with_badge ]
120+ # Using exclude() would return 0 employees without the badge
121+ mock_employees .exclude .return_value = MagicMock ()
122+ mock_employees .exclude .return_value .count .return_value = 0
123+
124+ # No former employees have the badge
125+ mock_non_employees_filter = MagicMock ()
126+ mock_non_employees_filter .count .return_value = 0
127+ mock_non_employees_filter .__iter__ .return_value = []
128+
129+ # Configure filter side effects for two command runs
130+ mock_user_filter .side_effect = [
131+ mock_employees , # is_owasp_employee=True (first run)
132+ mock_non_employees_filter , # is_owasp_employee=False (first run)
133+ mock_employees , # is_owasp_employee=True (second run)
134+ mock_non_employees_filter , # is_owasp_employee=False (second run)
135+ ]
136+
137+ # First run
138+ out1 = StringIO ()
139+ call_command ("github_sync_user_badges" , stdout = out1 )
140+
141+ # Second run
142+ out2 = StringIO ()
143+ call_command ("github_sync_user_badges" , stdout = out2 )
144+
145+ # Verify no add/remove operations were performed
146+ mock_employee_with_badge .badges .add .assert_not_called ()
147+
148+ # Check both outputs contain zero-count messages
149+ assert "Added badge to 0 employees" in out1 .getvalue ()
150+ assert "Removed badge from 0 non-employees" in out1 .getvalue ()
151+ assert "Added badge to 0 employees" in out2 .getvalue ()
152+ assert "Removed badge from 0 non-employees" in out2 .getvalue ()
0 commit comments