|
10 | 10 |
|
11 | 11 | @pytest.fixture |
12 | 12 | def command(): |
13 | | - """Return a command instance.""" |
14 | | - return Command() |
| 13 | + """Return a command instance with a mocked stdout.""" |
| 14 | + cmd = Command() |
| 15 | + cmd.stdout = MagicMock() |
| 16 | + return cmd |
15 | 17 |
|
16 | 18 |
|
17 | 19 | class TestGithubMatchUsersCommand: |
18 | | - """Test suite for the github_match_users command.""" |
| 20 | + """Test suite for the command's setup and helper methods.""" |
19 | 21 |
|
20 | 22 | def test_command_help_text(self, command): |
21 | | - """Test that the command has the correct help text.""" |
| 23 | + """Test that the command has the new, correct help text.""" |
22 | 24 | assert ( |
23 | 25 | command.help |
24 | | - == "Match leaders or Slack members with GitHub users using exact and fuzzy matching." |
| 26 | + == "Matches entity leader names with GitHub Users and creates EntityMember records." |
25 | 27 | ) |
26 | 28 |
|
27 | 29 | def test_command_inheritance(self, command): |
28 | 30 | """Test that the command inherits from BaseCommand.""" |
29 | 31 | assert isinstance(command, BaseCommand) |
30 | 32 |
|
31 | 33 | def test_add_arguments(self, command): |
32 | | - """Test that the command adds the correct arguments.""" |
| 34 | + """Test that the command adds the correct arguments for the new version.""" |
33 | 35 | parser = MagicMock() |
34 | 36 | command.add_arguments(parser) |
35 | 37 |
|
36 | 38 | assert parser.add_argument.call_count == 2 |
37 | 39 | parser.add_argument.assert_any_call( |
38 | 40 | "model_name", |
39 | 41 | type=str, |
40 | | - choices=("chapter", "committee", "member", "project"), |
41 | | - help="Model name to process: chapter, committee, project, or member", |
| 42 | + choices=("chapter", "committee", "project", "all"), |
| 43 | + help="Model to process: chapter, committee, project, or all.", |
42 | 44 | ) |
43 | 45 | parser.add_argument.assert_any_call( |
44 | 46 | "--threshold", |
@@ -67,246 +69,39 @@ def test_is_valid_user(self, command, login, name, expected): |
67 | 69 | assert command._is_valid_user(login, name) == expected |
68 | 70 |
|
69 | 71 |
|
70 | | -class TestProcessLeaders: |
71 | | - """Test suite for the process_leaders method.""" |
72 | | - |
73 | | - @pytest.fixture |
74 | | - def command(self): |
75 | | - """Return a command instance.""" |
76 | | - command = Command() |
77 | | - command.stdout = MagicMock() |
78 | | - |
79 | | - return command |
| 72 | +class TestFindUserMatches: |
| 73 | + """Test suite for the _find_user_matches helper method.""" |
80 | 74 |
|
81 | 75 | @pytest.fixture |
82 | 76 | def mock_users(self): |
83 | 77 | """Return a dictionary of mock users.""" |
84 | | - return { |
85 | | - 1: {"id": 1, "login": "john_doe", "name": "John Doe"}, |
86 | | - 2: {"id": 2, "login": "jane_doe", "name": "Jane Doe"}, |
87 | | - 3: {"id": 3, "login": "peter_jones", "name": "Peter Jones"}, |
88 | | - 4: {"id": 4, "login": "testuser", "name": "Test User"}, |
89 | | - } |
90 | | - |
91 | | - def test_no_leaders(self, command): |
92 | | - """Test with no leaders provided.""" |
93 | | - exact, fuzzy, unmatched = command.process_leaders([], 75, {}) |
94 | | - |
95 | | - assert exact == [] |
96 | | - assert fuzzy == [] |
97 | | - assert unmatched == [] |
| 78 | + return [ |
| 79 | + {"id": 1, "login": "john_doe", "name": "John Doe"}, |
| 80 | + {"id": 2, "login": "jane_doe", "name": "Jane Doe"}, |
| 81 | + {"id": 3, "login": "peter_jones", "name": "Peter Jones"}, |
| 82 | + ] |
98 | 83 |
|
99 | 84 | def test_exact_match(self, command, mock_users): |
100 | | - """Test exact matching.""" |
| 85 | + """Test exact matching by login and name.""" |
101 | 86 | leaders_raw = ["john_doe", "Jane Doe"] |
102 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 75, mock_users) |
| 87 | + matches = command._find_user_matches(leaders_raw, mock_users, 90) |
103 | 88 |
|
104 | | - assert len(exact) == 2 |
105 | | - assert mock_users[1] in exact |
106 | | - assert mock_users[2] in exact |
107 | | - assert fuzzy == [] |
108 | | - assert unmatched == [] |
| 89 | + assert len(matches) == 2 |
| 90 | + assert any(u["id"] == 1 for u in matches) |
| 91 | + assert any(u["id"] == 2 for u in matches) |
109 | 92 |
|
110 | 93 | @patch("apps.github.management.commands.github_match_users.fuzz") |
111 | 94 | def test_fuzzy_match(self, mock_fuzz, command, mock_users): |
112 | 95 | """Test fuzzy matching.""" |
113 | | - mock_fuzz.token_sort_ratio.side_effect = ( |
114 | | - lambda left, right: 90 if "peter" in right.lower() or "peter" in left.lower() else 10 |
115 | | - ) |
116 | | - |
| 96 | + mock_fuzz.token_sort_ratio.side_effect = lambda _, s2: 90 if "peter" in s2.lower() else 10 |
117 | 97 | leaders_raw = ["pete_jones"] |
118 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 80, mock_users) |
| 98 | + matches = command._find_user_matches(leaders_raw, mock_users, 80) |
119 | 99 |
|
120 | | - assert exact == [] |
121 | | - assert len(fuzzy) == 1 |
122 | | - assert mock_users[3] in fuzzy |
123 | | - assert unmatched == [] |
| 100 | + assert len(matches) == 1 |
| 101 | + assert matches[0]["id"] == 3 |
124 | 102 |
|
125 | 103 | def test_unmatched_leader(self, command, mock_users): |
126 | | - """Test unmatched leader.""" |
| 104 | + """Test that an unknown leader returns no matches.""" |
127 | 105 | leaders_raw = ["unknown_leader"] |
128 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 100, mock_users) |
129 | | - |
130 | | - assert exact == [] |
131 | | - assert fuzzy == [] |
132 | | - assert unmatched == ["unknown_leader"] |
133 | | - |
134 | | - def test_mixed_matches(self, command, mock_users): |
135 | | - """Test a mix of exact, fuzzy, and unmatched leaders.""" |
136 | | - leaders_raw = ["john_doe", "pete_jones", "unknown_leader"] |
137 | | - |
138 | | - with patch("apps.github.management.commands.github_match_users.fuzz") as mock_fuzz: |
139 | | - |
140 | | - def ratio(s1, s2): |
141 | | - return 85 if "peter" in s2.lower() and "pete" in s1.lower() else 50 |
142 | | - |
143 | | - mock_fuzz.token_sort_ratio.side_effect = ratio |
144 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 80, mock_users) |
145 | | - |
146 | | - assert len(exact) == 1 |
147 | | - assert mock_users[1] in exact |
148 | | - assert len(fuzzy) == 1 |
149 | | - assert mock_users[3] in fuzzy |
150 | | - assert unmatched == ["unknown_leader"] |
151 | | - |
152 | | - def test_duplicate_leaders(self, command, mock_users): |
153 | | - """Test with duplicate leaders in raw list.""" |
154 | | - leaders_raw = ["john_doe", "john_doe"] |
155 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 75, mock_users) |
156 | | - |
157 | | - assert len(exact) == 1 |
158 | | - assert mock_users[1] in exact |
159 | | - assert fuzzy == [] |
160 | | - assert unmatched == [] |
161 | | - |
162 | | - def test_empty_and_none_leaders(self, command, mock_users): |
163 | | - """Test with empty string and None in leaders raw list.""" |
164 | | - leaders_raw = ["", None, "john_doe"] |
165 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 75, mock_users) |
166 | | - |
167 | | - assert len(exact) == 1 |
168 | | - assert mock_users[1] in exact |
169 | | - assert fuzzy == [] |
170 | | - assert unmatched == [] |
171 | | - |
172 | | - def test_multiple_exact_matches_for_one_leader(self, command): |
173 | | - """Test when one leader name matches multiple users.""" |
174 | | - users = { |
175 | | - 1: {"id": 1, "login": "johndoe", "name": "Johnathan Doe"}, |
176 | | - 2: {"id": 2, "login": "JohnDoe", "name": "John Doe"}, |
177 | | - } |
178 | | - leaders_raw = ["JohnDoe"] |
179 | | - exact, fuzzy, unmatched = command.process_leaders(leaders_raw, 75, users) |
180 | | - |
181 | | - assert len(exact) == 2 |
182 | | - assert users[1] in exact |
183 | | - assert users[2] in exact |
184 | | - assert fuzzy == [] |
185 | | - assert unmatched == [] |
186 | | - |
187 | | - |
188 | | -@patch("apps.github.management.commands.github_match_users.User") |
189 | | -@patch("apps.github.management.commands.github_match_users.Chapter") |
190 | | -@patch("apps.github.management.commands.github_match_users.Committee") |
191 | | -@patch("apps.github.management.commands.github_match_users.Project") |
192 | | -@patch("apps.github.management.commands.github_match_users.Member") |
193 | | -class TestHandleMethod: |
194 | | - """Test suite for the handle method of the command.""" |
195 | | - |
196 | | - @pytest.fixture |
197 | | - def command(self): |
198 | | - """Return a command instance with mocked stdout.""" |
199 | | - command = Command() |
200 | | - command.stdout = MagicMock() |
201 | | - |
202 | | - return command |
203 | | - |
204 | | - def test_invalid_model_name( |
205 | | - self, mock_member, mock_project, mock_committee, mock_chapter, mock_user, command |
206 | | - ): |
207 | | - """Test handle with an invalid model name.""" |
208 | | - command.handle(model_name="invalid", threshold=75) |
209 | | - command.stdout.write.assert_called_with( |
210 | | - command.style.ERROR( |
211 | | - "Invalid model name! Choose from: chapter, committee, project, member" |
212 | | - ) |
213 | | - ) |
214 | | - |
215 | | - @pytest.mark.parametrize( |
216 | | - ("model_name", "model_class_str", "relation_field"), |
217 | | - [ |
218 | | - ("chapter", "Chapter", "suggested_leaders"), |
219 | | - ("committee", "Committee", "suggested_leaders"), |
220 | | - ("project", "Project", "suggested_leaders"), |
221 | | - ("member", "Member", "suggested_users"), |
222 | | - ], |
223 | | - ) |
224 | | - def test_handle_with_valid_models( |
225 | | - self, |
226 | | - mock_member, |
227 | | - mock_project, |
228 | | - mock_committee, |
229 | | - mock_chapter, |
230 | | - mock_user, |
231 | | - command, |
232 | | - model_name, |
233 | | - model_class_str, |
234 | | - relation_field, |
235 | | - ): |
236 | | - """Test handle with different valid models.""" |
237 | | - mock_models = { |
238 | | - "Chapter": mock_chapter, |
239 | | - "Committee": mock_committee, |
240 | | - "Project": mock_project, |
241 | | - "Member": mock_member, |
242 | | - } |
243 | | - model_class = mock_models[model_class_str] |
244 | | - |
245 | | - mock_user.objects.values.return_value = [ |
246 | | - {"id": 1, "login": "leader_one", "name": "Leader One"}, |
247 | | - {"id": 2, "login": "leader_two", "name": "Leader Two"}, |
248 | | - ] |
249 | | - |
250 | | - mock_instance = MagicMock() |
251 | | - mock_instance.id = 1 |
252 | | - |
253 | | - if model_name == "member": |
254 | | - mock_instance.username = "leader_one" |
255 | | - mock_instance.real_name = "Leader Two" |
256 | | - else: |
257 | | - mock_instance.leaders_raw = ["leader_one", "leader_two"] |
258 | | - |
259 | | - model_class.objects.prefetch_related.return_value = [mock_instance] |
260 | | - |
261 | | - command.handle(model_name=model_name, threshold=90) |
262 | | - |
263 | | - model_class.objects.prefetch_related.assert_called_once_with(relation_field) |
264 | | - |
265 | | - relation = getattr(mock_instance, relation_field) |
266 | | - relation.set.assert_called_once_with({1, 2}) |
267 | | - |
268 | | - command.stdout.write.assert_any_call(f"Processing {model_name} 1...") |
269 | | - command.stdout.write.assert_any_call("Exact match found for leader_one: leader_one") |
270 | | - |
271 | | - def test_handle_with_no_users( |
272 | | - self, mock_member, mock_project, mock_committee, mock_chapter, mock_user, command |
273 | | - ): |
274 | | - """Test handle when there are no users in the database.""" |
275 | | - mock_user.objects.values.return_value = [] |
276 | | - mock_chapter_instance = MagicMock(id=1, leaders_raw=["some_leader"]) |
277 | | - mock_chapter.objects.prefetch_related.return_value = [mock_chapter_instance] |
278 | | - |
279 | | - command.handle(model_name="chapter", threshold=75) |
280 | | - |
281 | | - command.stdout.write.assert_any_call("Processing chapter 1...") |
282 | | - |
283 | | - unmatched_call = [ |
284 | | - c for c in command.stdout.write.call_args_list if "Unmatched" in c.args[0] |
285 | | - ] |
286 | | - |
287 | | - assert len(unmatched_call) == 1 |
288 | | - assert "['some_leader']" in unmatched_call[0].args[0] |
289 | | - |
290 | | - mock_chapter_instance.suggested_leaders.set.assert_called_once_with(set()) |
291 | | - |
292 | | - def test_handle_with_no_leaders_in_instance( |
293 | | - self, mock_member, mock_project, mock_committee, mock_chapter, mock_user, command |
294 | | - ): |
295 | | - """Test handle when an instance has no leaders.""" |
296 | | - mock_user.objects.values.return_value = [ |
297 | | - {"id": 1, "login": "user1", "name": "User One"}, |
298 | | - ] |
299 | | - mock_chapter_instance = MagicMock(id=1, leaders_raw=[]) |
300 | | - mock_chapter.objects.prefetch_related.return_value = [mock_chapter_instance] |
301 | | - |
302 | | - command.handle(model_name="chapter", threshold=75) |
303 | | - |
304 | | - command.stdout.write.assert_any_call("Processing chapter 1...") |
305 | | - |
306 | | - unmatched_call = [ |
307 | | - c for c in command.stdout.write.call_args_list if "Unmatched" in c.args[0] |
308 | | - ] |
309 | | - |
310 | | - assert len(unmatched_call) == 0 |
311 | | - |
312 | | - mock_chapter_instance.suggested_leaders.set.assert_called_once_with(set()) |
| 106 | + matches = command._find_user_matches(leaders_raw, mock_users, 100) |
| 107 | + assert matches == [] |
0 commit comments