gitmap-core 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gitmap_core/README.md +46 -0
- gitmap_core/__init__.py +100 -0
- gitmap_core/communication.py +346 -0
- gitmap_core/compat.py +408 -0
- gitmap_core/connection.py +232 -0
- gitmap_core/context.py +709 -0
- gitmap_core/diff.py +283 -0
- gitmap_core/maps.py +385 -0
- gitmap_core/merge.py +449 -0
- gitmap_core/models.py +332 -0
- gitmap_core/py.typed +0 -0
- gitmap_core/pyproject.toml +48 -0
- gitmap_core/remote.py +728 -0
- gitmap_core/repository.py +1632 -0
- gitmap_core/tests/__init__.py +1 -0
- gitmap_core/tests/test_communication.py +695 -0
- gitmap_core/tests/test_compat.py +310 -0
- gitmap_core/tests/test_connection.py +314 -0
- gitmap_core/tests/test_context.py +814 -0
- gitmap_core/tests/test_diff.py +567 -0
- gitmap_core/tests/test_init.py +153 -0
- gitmap_core/tests/test_maps.py +642 -0
- gitmap_core/tests/test_merge.py +694 -0
- gitmap_core/tests/test_models.py +410 -0
- gitmap_core/tests/test_remote.py +3014 -0
- gitmap_core/tests/test_repository.py +1639 -0
- gitmap_core/tests/test_visualize.py +902 -0
- gitmap_core/visualize.py +1217 -0
- gitmap_core-0.1.0.dist-info/METADATA +961 -0
- gitmap_core-0.1.0.dist-info/RECORD +32 -0
- gitmap_core-0.1.0.dist-info/WHEEL +4 -0
- gitmap_core-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GitMap core library tests."""
|
|
@@ -0,0 +1,695 @@
|
|
|
1
|
+
"""Tests for the communication module.
|
|
2
|
+
|
|
3
|
+
Tests cover Portal/AGOL notification helpers using mocked GIS connections.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
from unittest.mock import Mock, patch
|
|
9
|
+
|
|
10
|
+
from gitmap_core.communication import (
|
|
11
|
+
_ensure_gis,
|
|
12
|
+
_resolve_group,
|
|
13
|
+
get_group_member_usernames,
|
|
14
|
+
list_groups,
|
|
15
|
+
send_group_notification,
|
|
16
|
+
get_item_group_users,
|
|
17
|
+
notify_item_group_users,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestEnsureGis:
|
|
22
|
+
"""Tests for _ensure_gis validation function."""
|
|
23
|
+
|
|
24
|
+
def test_raises_when_gis_module_unavailable(self):
|
|
25
|
+
"""Should raise RuntimeError if arcgis module not installed."""
|
|
26
|
+
with patch("gitmap_core.communication.GIS", None):
|
|
27
|
+
with pytest.raises(RuntimeError, match="not installed"):
|
|
28
|
+
_ensure_gis(Mock())
|
|
29
|
+
|
|
30
|
+
def test_raises_when_gis_is_none(self):
|
|
31
|
+
"""Should raise RuntimeError if gis connection is None."""
|
|
32
|
+
with pytest.raises(RuntimeError, match="valid GIS connection"):
|
|
33
|
+
_ensure_gis(None)
|
|
34
|
+
|
|
35
|
+
def test_passes_with_valid_gis(self):
|
|
36
|
+
"""Should not raise when valid GIS object provided."""
|
|
37
|
+
mock_gis = Mock()
|
|
38
|
+
_ensure_gis(mock_gis) # Should not raise
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestResolveGroup:
|
|
42
|
+
"""Tests for _resolve_group helper function."""
|
|
43
|
+
|
|
44
|
+
def test_resolves_by_id_directly(self):
|
|
45
|
+
"""Should return group when found by ID."""
|
|
46
|
+
mock_gis = Mock()
|
|
47
|
+
mock_group = Mock()
|
|
48
|
+
mock_gis.groups.get.return_value = mock_group
|
|
49
|
+
|
|
50
|
+
result = _resolve_group(mock_gis, "group-123")
|
|
51
|
+
|
|
52
|
+
mock_gis.groups.get.assert_called_once_with("group-123")
|
|
53
|
+
assert result is mock_group
|
|
54
|
+
|
|
55
|
+
def test_resolves_by_title_search(self):
|
|
56
|
+
"""Should search by title when ID lookup fails."""
|
|
57
|
+
mock_gis = Mock()
|
|
58
|
+
mock_group = Mock()
|
|
59
|
+
mock_gis.groups.get.return_value = None
|
|
60
|
+
mock_gis.groups.search.return_value = [mock_group]
|
|
61
|
+
|
|
62
|
+
result = _resolve_group(mock_gis, "My Group")
|
|
63
|
+
|
|
64
|
+
mock_gis.groups.search.assert_called_once_with('title:"My Group"')
|
|
65
|
+
assert result is mock_group
|
|
66
|
+
|
|
67
|
+
def test_returns_none_when_not_found(self):
|
|
68
|
+
"""Should return None when group not found by ID or title."""
|
|
69
|
+
mock_gis = Mock()
|
|
70
|
+
mock_gis.groups.get.return_value = None
|
|
71
|
+
mock_gis.groups.search.return_value = []
|
|
72
|
+
|
|
73
|
+
result = _resolve_group(mock_gis, "nonexistent")
|
|
74
|
+
|
|
75
|
+
assert result is None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class TestGetGroupMemberUsernames:
|
|
79
|
+
"""Tests for get_group_member_usernames function."""
|
|
80
|
+
|
|
81
|
+
def test_collects_owner(self):
|
|
82
|
+
"""Should include group owner in usernames."""
|
|
83
|
+
mock_gis = Mock()
|
|
84
|
+
mock_group = Mock()
|
|
85
|
+
mock_gis.groups.get.return_value = mock_group
|
|
86
|
+
mock_group.get_members.return_value = {
|
|
87
|
+
"owner": "owner_user",
|
|
88
|
+
"admins": [],
|
|
89
|
+
"users": [],
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
93
|
+
|
|
94
|
+
assert "owner_user" in result
|
|
95
|
+
|
|
96
|
+
def test_collects_admins_and_users(self):
|
|
97
|
+
"""Should include admins and users in results."""
|
|
98
|
+
mock_gis = Mock()
|
|
99
|
+
mock_group = Mock()
|
|
100
|
+
mock_gis.groups.get.return_value = mock_group
|
|
101
|
+
mock_group.get_members.return_value = {
|
|
102
|
+
"owner": "owner",
|
|
103
|
+
"admins": ["admin1", "admin2"],
|
|
104
|
+
"users": ["user1", "user2"],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
108
|
+
|
|
109
|
+
assert set(result) == {"owner", "admin1", "admin2", "user1", "user2"}
|
|
110
|
+
|
|
111
|
+
def test_collects_invited_users(self):
|
|
112
|
+
"""Should include invited admins and users."""
|
|
113
|
+
mock_gis = Mock()
|
|
114
|
+
mock_group = Mock()
|
|
115
|
+
mock_gis.groups.get.return_value = mock_group
|
|
116
|
+
mock_group.get_members.return_value = {
|
|
117
|
+
"owner": "owner",
|
|
118
|
+
"admins": [],
|
|
119
|
+
"users": [],
|
|
120
|
+
"admins_invited": ["invited_admin"],
|
|
121
|
+
"users_invited": ["invited_user"],
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
125
|
+
|
|
126
|
+
assert "invited_admin" in result
|
|
127
|
+
assert "invited_user" in result
|
|
128
|
+
|
|
129
|
+
def test_deduplicates_usernames(self):
|
|
130
|
+
"""Should return unique usernames only."""
|
|
131
|
+
mock_gis = Mock()
|
|
132
|
+
mock_group = Mock()
|
|
133
|
+
mock_gis.groups.get.return_value = mock_group
|
|
134
|
+
mock_group.get_members.return_value = {
|
|
135
|
+
"owner": "shared_user",
|
|
136
|
+
"admins": ["shared_user"],
|
|
137
|
+
"users": ["shared_user", "unique_user"],
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
141
|
+
|
|
142
|
+
assert result == ["shared_user", "unique_user"]
|
|
143
|
+
|
|
144
|
+
def test_returns_sorted_usernames(self):
|
|
145
|
+
"""Should return usernames in sorted order."""
|
|
146
|
+
mock_gis = Mock()
|
|
147
|
+
mock_group = Mock()
|
|
148
|
+
mock_gis.groups.get.return_value = mock_group
|
|
149
|
+
mock_group.get_members.return_value = {
|
|
150
|
+
"owner": "zack",
|
|
151
|
+
"admins": [],
|
|
152
|
+
"users": ["alice", "bob"],
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
156
|
+
|
|
157
|
+
assert result == ["alice", "bob", "zack"]
|
|
158
|
+
|
|
159
|
+
def test_raises_when_group_not_found(self):
|
|
160
|
+
"""Should raise RuntimeError if group cannot be found."""
|
|
161
|
+
mock_gis = Mock()
|
|
162
|
+
mock_gis.groups.get.return_value = None
|
|
163
|
+
mock_gis.groups.search.return_value = []
|
|
164
|
+
|
|
165
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
166
|
+
get_group_member_usernames(mock_gis, "nonexistent")
|
|
167
|
+
|
|
168
|
+
def test_raises_when_no_members(self):
|
|
169
|
+
"""Should raise RuntimeError if group has no members."""
|
|
170
|
+
mock_gis = Mock()
|
|
171
|
+
mock_group = Mock()
|
|
172
|
+
mock_gis.groups.get.return_value = mock_group
|
|
173
|
+
mock_group.get_members.return_value = {
|
|
174
|
+
"owner": None,
|
|
175
|
+
"admins": [],
|
|
176
|
+
"users": [],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
with pytest.raises(RuntimeError, match="No members found"):
|
|
180
|
+
get_group_member_usernames(mock_gis, "empty-group")
|
|
181
|
+
|
|
182
|
+
def test_handles_none_values_in_member_lists(self):
|
|
183
|
+
"""Should skip None values in member lists."""
|
|
184
|
+
mock_gis = Mock()
|
|
185
|
+
mock_group = Mock()
|
|
186
|
+
mock_gis.groups.get.return_value = mock_group
|
|
187
|
+
mock_group.get_members.return_value = {
|
|
188
|
+
"owner": "owner",
|
|
189
|
+
"admins": [None, "admin1"],
|
|
190
|
+
"users": ["user1", None, ""],
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
194
|
+
|
|
195
|
+
assert "owner" in result
|
|
196
|
+
assert "admin1" in result
|
|
197
|
+
assert "user1" in result
|
|
198
|
+
assert None not in result
|
|
199
|
+
assert "" not in result
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class TestListGroups:
|
|
203
|
+
"""Tests for list_groups function."""
|
|
204
|
+
|
|
205
|
+
def test_returns_group_info(self):
|
|
206
|
+
"""Should return list of group dictionaries."""
|
|
207
|
+
mock_gis = Mock()
|
|
208
|
+
mock_group = Mock()
|
|
209
|
+
mock_group.id = "group-123"
|
|
210
|
+
mock_group.title = "Test Group"
|
|
211
|
+
mock_group.owner = "owner_user"
|
|
212
|
+
mock_gis.groups.search.return_value = [mock_group]
|
|
213
|
+
|
|
214
|
+
result = list_groups(mock_gis)
|
|
215
|
+
|
|
216
|
+
assert len(result) == 1
|
|
217
|
+
assert result[0]["id"] == "group-123"
|
|
218
|
+
assert result[0]["title"] == "Test Group"
|
|
219
|
+
assert result[0]["owner"] == "owner_user"
|
|
220
|
+
|
|
221
|
+
def test_respects_max_results(self):
|
|
222
|
+
"""Should limit results to max_results."""
|
|
223
|
+
mock_gis = Mock()
|
|
224
|
+
mock_groups = [Mock(id=f"g{i}", title=f"Group {i}", owner="owner") for i in range(10)]
|
|
225
|
+
mock_gis.groups.search.return_value = mock_groups
|
|
226
|
+
|
|
227
|
+
result = list_groups(mock_gis, max_results=3)
|
|
228
|
+
|
|
229
|
+
assert len(result) == 3
|
|
230
|
+
|
|
231
|
+
def test_passes_query_to_search(self):
|
|
232
|
+
"""Should pass query string to groups.search."""
|
|
233
|
+
mock_gis = Mock()
|
|
234
|
+
mock_gis.groups.search.return_value = []
|
|
235
|
+
|
|
236
|
+
list_groups(mock_gis, query="title:MyGroup")
|
|
237
|
+
|
|
238
|
+
mock_gis.groups.search.assert_called_once_with("title:MyGroup")
|
|
239
|
+
|
|
240
|
+
def test_handles_missing_attributes(self):
|
|
241
|
+
"""Should handle groups with missing attributes."""
|
|
242
|
+
mock_gis = Mock()
|
|
243
|
+
mock_group = Mock(spec=[]) # No attributes
|
|
244
|
+
mock_gis.groups.search.return_value = [mock_group]
|
|
245
|
+
|
|
246
|
+
result = list_groups(mock_gis)
|
|
247
|
+
|
|
248
|
+
assert result[0]["id"] == ""
|
|
249
|
+
assert result[0]["title"] == ""
|
|
250
|
+
assert result[0]["owner"] == ""
|
|
251
|
+
|
|
252
|
+
def test_raises_on_search_error(self):
|
|
253
|
+
"""Should raise RuntimeError if search fails."""
|
|
254
|
+
mock_gis = Mock()
|
|
255
|
+
mock_gis.groups.search.side_effect = Exception("Search failed")
|
|
256
|
+
|
|
257
|
+
with pytest.raises(RuntimeError, match="Failed to search groups"):
|
|
258
|
+
list_groups(mock_gis)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class TestSendGroupNotification:
|
|
262
|
+
"""Tests for send_group_notification function."""
|
|
263
|
+
|
|
264
|
+
def test_sends_notification_to_group(self):
|
|
265
|
+
"""Should call group.notify with correct parameters."""
|
|
266
|
+
mock_gis = Mock()
|
|
267
|
+
mock_group = Mock()
|
|
268
|
+
mock_gis.groups.get.return_value = mock_group
|
|
269
|
+
mock_group.get_members.return_value = {
|
|
270
|
+
"owner": "owner",
|
|
271
|
+
"admins": [],
|
|
272
|
+
"users": ["user1"],
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
result = send_group_notification(
|
|
276
|
+
mock_gis, "group-123", "Subject", "Body text"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
mock_group.notify.assert_called_once()
|
|
280
|
+
call_kwargs = mock_group.notify.call_args[1]
|
|
281
|
+
assert call_kwargs["subject"] == "Subject"
|
|
282
|
+
assert call_kwargs["message"] == "Body text"
|
|
283
|
+
assert set(call_kwargs["users"]) == {"owner", "user1"}
|
|
284
|
+
|
|
285
|
+
def test_sends_to_specific_users(self):
|
|
286
|
+
"""Should send to specified users when provided."""
|
|
287
|
+
mock_gis = Mock()
|
|
288
|
+
mock_group = Mock()
|
|
289
|
+
mock_gis.groups.get.return_value = mock_group
|
|
290
|
+
|
|
291
|
+
result = send_group_notification(
|
|
292
|
+
mock_gis, "group-123", "Subject", "Body",
|
|
293
|
+
users=["specific_user1", "specific_user2"]
|
|
294
|
+
)
|
|
295
|
+
|
|
296
|
+
call_kwargs = mock_group.notify.call_args[1]
|
|
297
|
+
assert set(call_kwargs["users"]) == {"specific_user1", "specific_user2"}
|
|
298
|
+
|
|
299
|
+
def test_returns_notified_users(self):
|
|
300
|
+
"""Should return list of users that were notified."""
|
|
301
|
+
mock_gis = Mock()
|
|
302
|
+
mock_group = Mock()
|
|
303
|
+
mock_gis.groups.get.return_value = mock_group
|
|
304
|
+
|
|
305
|
+
result = send_group_notification(
|
|
306
|
+
mock_gis, "group-123", "Subject", "Body",
|
|
307
|
+
users=["user1", "user2"]
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
assert set(result) == {"user1", "user2"}
|
|
311
|
+
|
|
312
|
+
def test_raises_when_group_not_found(self):
|
|
313
|
+
"""Should raise RuntimeError if group not found."""
|
|
314
|
+
mock_gis = Mock()
|
|
315
|
+
mock_gis.groups.get.return_value = None
|
|
316
|
+
mock_gis.groups.search.return_value = []
|
|
317
|
+
|
|
318
|
+
with pytest.raises(RuntimeError, match="not found"):
|
|
319
|
+
send_group_notification(mock_gis, "nonexistent", "Subject", "Body")
|
|
320
|
+
|
|
321
|
+
def test_raises_when_no_target_users(self):
|
|
322
|
+
"""Should raise RuntimeError if no users to notify."""
|
|
323
|
+
mock_gis = Mock()
|
|
324
|
+
mock_group = Mock()
|
|
325
|
+
mock_gis.groups.get.return_value = mock_group
|
|
326
|
+
mock_group.get_members.return_value = {
|
|
327
|
+
"owner": None,
|
|
328
|
+
"admins": [],
|
|
329
|
+
"users": [],
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
# Error comes from get_group_member_usernames (called by send_group_notification)
|
|
333
|
+
with pytest.raises(RuntimeError, match="No members found"):
|
|
334
|
+
send_group_notification(mock_gis, "group-123", "Subject", "Body")
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
class TestGetItemGroupUsers:
|
|
338
|
+
"""Tests for get_item_group_users function."""
|
|
339
|
+
|
|
340
|
+
def test_returns_empty_for_private_item(self):
|
|
341
|
+
"""Should return empty list for private items."""
|
|
342
|
+
mock_gis = Mock()
|
|
343
|
+
mock_item = Mock()
|
|
344
|
+
mock_item.access = "private"
|
|
345
|
+
|
|
346
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
347
|
+
|
|
348
|
+
assert result == []
|
|
349
|
+
|
|
350
|
+
def test_returns_empty_when_no_groups(self):
|
|
351
|
+
"""Should return empty list when item has no group shares."""
|
|
352
|
+
mock_gis = Mock()
|
|
353
|
+
mock_item = Mock()
|
|
354
|
+
mock_item.access = "public"
|
|
355
|
+
mock_item.properties = None
|
|
356
|
+
mock_gis.users.me = None
|
|
357
|
+
|
|
358
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
359
|
+
|
|
360
|
+
assert result == []
|
|
361
|
+
|
|
362
|
+
def test_collects_users_from_item_properties(self):
|
|
363
|
+
"""Should collect users from groups in item properties."""
|
|
364
|
+
mock_gis = Mock()
|
|
365
|
+
mock_item = Mock()
|
|
366
|
+
mock_item.access = "org"
|
|
367
|
+
mock_item.properties = {"sharing": {"groups": ["group-123"]}}
|
|
368
|
+
|
|
369
|
+
mock_group = Mock()
|
|
370
|
+
mock_gis.groups.get.return_value = mock_group
|
|
371
|
+
mock_group.get_members.return_value = {
|
|
372
|
+
"owner": "owner",
|
|
373
|
+
"admins": [],
|
|
374
|
+
"users": ["user1"],
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
378
|
+
|
|
379
|
+
assert set(result) == {"owner", "user1"}
|
|
380
|
+
|
|
381
|
+
def test_deduplicates_across_groups(self):
|
|
382
|
+
"""Should deduplicate users across multiple groups."""
|
|
383
|
+
mock_gis = Mock()
|
|
384
|
+
mock_item = Mock()
|
|
385
|
+
mock_item.access = "org"
|
|
386
|
+
mock_item.properties = {"sharing": {"groups": ["group-1", "group-2"]}}
|
|
387
|
+
|
|
388
|
+
mock_group1 = Mock()
|
|
389
|
+
mock_group1.get_members.return_value = {
|
|
390
|
+
"owner": "shared_user",
|
|
391
|
+
"admins": [],
|
|
392
|
+
"users": ["user1"],
|
|
393
|
+
}
|
|
394
|
+
mock_group2 = Mock()
|
|
395
|
+
mock_group2.get_members.return_value = {
|
|
396
|
+
"owner": "shared_user",
|
|
397
|
+
"admins": [],
|
|
398
|
+
"users": ["user2"],
|
|
399
|
+
}
|
|
400
|
+
mock_gis.groups.get.side_effect = [mock_group1, mock_group2]
|
|
401
|
+
|
|
402
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
403
|
+
|
|
404
|
+
# shared_user should appear only once
|
|
405
|
+
assert result.count("shared_user") == 1
|
|
406
|
+
assert set(result) == {"shared_user", "user1", "user2"}
|
|
407
|
+
|
|
408
|
+
def test_handles_inaccessible_groups(self):
|
|
409
|
+
"""Should skip groups that raise exceptions."""
|
|
410
|
+
mock_gis = Mock()
|
|
411
|
+
mock_item = Mock()
|
|
412
|
+
mock_item.access = "org"
|
|
413
|
+
mock_item.properties = {"sharing": {"groups": ["group-ok", "group-fail"]}}
|
|
414
|
+
|
|
415
|
+
mock_group_ok = Mock()
|
|
416
|
+
mock_group_ok.get_members.return_value = {
|
|
417
|
+
"owner": "owner",
|
|
418
|
+
"admins": [],
|
|
419
|
+
"users": [],
|
|
420
|
+
}
|
|
421
|
+
mock_gis.groups.get.side_effect = [mock_group_ok, Exception("Access denied")]
|
|
422
|
+
|
|
423
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
424
|
+
|
|
425
|
+
assert "owner" in result
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class TestNotifyItemGroupUsers:
|
|
429
|
+
"""Tests for notify_item_group_users function."""
|
|
430
|
+
|
|
431
|
+
def test_returns_empty_for_private_item(self):
|
|
432
|
+
"""Should return empty list for private items."""
|
|
433
|
+
mock_gis = Mock()
|
|
434
|
+
mock_item = Mock()
|
|
435
|
+
mock_item.access = "private"
|
|
436
|
+
|
|
437
|
+
result = notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
438
|
+
|
|
439
|
+
assert result == []
|
|
440
|
+
|
|
441
|
+
def test_returns_empty_when_no_groups(self):
|
|
442
|
+
"""Should return empty list when item has no group shares."""
|
|
443
|
+
mock_gis = Mock()
|
|
444
|
+
mock_item = Mock()
|
|
445
|
+
mock_item.access = "public"
|
|
446
|
+
mock_item.properties = None
|
|
447
|
+
mock_gis.users.me = None
|
|
448
|
+
|
|
449
|
+
result = notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
450
|
+
|
|
451
|
+
assert result == []
|
|
452
|
+
|
|
453
|
+
def test_notifies_all_group_users(self):
|
|
454
|
+
"""Should notify users from all groups sharing the item."""
|
|
455
|
+
mock_gis = Mock()
|
|
456
|
+
mock_item = Mock()
|
|
457
|
+
mock_item.access = "org"
|
|
458
|
+
mock_item.properties = {"sharing": {"groups": ["group-123"]}}
|
|
459
|
+
|
|
460
|
+
mock_group = Mock()
|
|
461
|
+
mock_gis.groups.get.return_value = mock_group
|
|
462
|
+
mock_group.get_members.return_value = {
|
|
463
|
+
"owner": "owner",
|
|
464
|
+
"admins": [],
|
|
465
|
+
"users": ["user1"],
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
result = notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
469
|
+
|
|
470
|
+
mock_group.notify.assert_called()
|
|
471
|
+
assert set(result) == {"owner", "user1"}
|
|
472
|
+
|
|
473
|
+
def test_continues_on_notify_failure(self):
|
|
474
|
+
"""Should continue notifying other groups if one fails."""
|
|
475
|
+
mock_gis = Mock()
|
|
476
|
+
mock_item = Mock()
|
|
477
|
+
mock_item.access = "org"
|
|
478
|
+
mock_item.properties = {"sharing": {"groups": ["group-1", "group-2"]}}
|
|
479
|
+
|
|
480
|
+
mock_group1 = Mock()
|
|
481
|
+
mock_group1.get_members.return_value = {
|
|
482
|
+
"owner": "owner1",
|
|
483
|
+
"admins": [],
|
|
484
|
+
"users": [],
|
|
485
|
+
}
|
|
486
|
+
mock_group1.notify.side_effect = Exception("Notify failed")
|
|
487
|
+
|
|
488
|
+
mock_group2 = Mock()
|
|
489
|
+
mock_group2.get_members.return_value = {
|
|
490
|
+
"owner": "owner2",
|
|
491
|
+
"admins": [],
|
|
492
|
+
"users": [],
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
mock_gis.groups.get.side_effect = [mock_group1, mock_group1, mock_group2, mock_group2]
|
|
496
|
+
|
|
497
|
+
result = notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
498
|
+
|
|
499
|
+
# Should still include users from both groups even if notify fails for one
|
|
500
|
+
assert "owner1" in result
|
|
501
|
+
assert "owner2" in result
|
|
502
|
+
|
|
503
|
+
def test_uses_user_groups_fallback(self):
|
|
504
|
+
"""Should fall back to user groups when item.properties lacks sharing data."""
|
|
505
|
+
mock_gis = Mock()
|
|
506
|
+
mock_item = Mock()
|
|
507
|
+
mock_item.access = "org"
|
|
508
|
+
mock_item.id = "item-123"
|
|
509
|
+
mock_item.properties = {} # No sharing data
|
|
510
|
+
|
|
511
|
+
# Set up user with groups
|
|
512
|
+
mock_user = Mock()
|
|
513
|
+
mock_group = Mock()
|
|
514
|
+
mock_group.id = "group-from-user"
|
|
515
|
+
mock_group_item = Mock()
|
|
516
|
+
mock_group_item.id = "item-123" # Matches our item
|
|
517
|
+
mock_group.content.return_value = [mock_group_item]
|
|
518
|
+
mock_group.get_members.return_value = {
|
|
519
|
+
"owner": "group_owner",
|
|
520
|
+
"admins": [],
|
|
521
|
+
"users": ["group_user"],
|
|
522
|
+
}
|
|
523
|
+
mock_user.groups = [mock_group]
|
|
524
|
+
mock_gis.users.me = mock_user
|
|
525
|
+
mock_gis.groups.get.return_value = mock_group
|
|
526
|
+
|
|
527
|
+
result = notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
528
|
+
|
|
529
|
+
assert set(result) == {"group_owner", "group_user"}
|
|
530
|
+
mock_group.notify.assert_called()
|
|
531
|
+
|
|
532
|
+
def test_raises_on_general_error(self):
|
|
533
|
+
"""Should raise RuntimeError on unexpected errors."""
|
|
534
|
+
mock_gis = Mock()
|
|
535
|
+
mock_item = Mock()
|
|
536
|
+
# Make access property raise an exception
|
|
537
|
+
type(mock_item).access = property(lambda self: (_ for _ in ()).throw(Exception("Unexpected")))
|
|
538
|
+
|
|
539
|
+
with pytest.raises(RuntimeError, match="Failed to notify item group users"):
|
|
540
|
+
notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
541
|
+
|
|
542
|
+
def test_skips_groups_with_content_error(self):
|
|
543
|
+
"""Should skip groups where content() raises an exception."""
|
|
544
|
+
mock_gis = Mock()
|
|
545
|
+
mock_item = Mock()
|
|
546
|
+
mock_item.access = "org"
|
|
547
|
+
mock_item.id = "item-123"
|
|
548
|
+
mock_item.properties = {}
|
|
549
|
+
|
|
550
|
+
mock_user = Mock()
|
|
551
|
+
mock_group_ok = Mock()
|
|
552
|
+
mock_group_ok.id = "group-ok"
|
|
553
|
+
mock_group_ok_item = Mock()
|
|
554
|
+
mock_group_ok_item.id = "item-123"
|
|
555
|
+
mock_group_ok.content.return_value = [mock_group_ok_item]
|
|
556
|
+
mock_group_ok.get_members.return_value = {
|
|
557
|
+
"owner": "owner_ok",
|
|
558
|
+
"admins": [],
|
|
559
|
+
"users": [],
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
mock_group_fail = Mock()
|
|
563
|
+
mock_group_fail.content.side_effect = Exception("Content error")
|
|
564
|
+
|
|
565
|
+
mock_user.groups = [mock_group_fail, mock_group_ok]
|
|
566
|
+
mock_gis.users.me = mock_user
|
|
567
|
+
mock_gis.groups.get.return_value = mock_group_ok
|
|
568
|
+
|
|
569
|
+
result = notify_item_group_users(mock_gis, mock_item, "Subject", "Body")
|
|
570
|
+
|
|
571
|
+
assert "owner_ok" in result
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
class TestGetItemGroupUsersFallback:
|
|
575
|
+
"""Tests for get_item_group_users fallback paths."""
|
|
576
|
+
|
|
577
|
+
def test_uses_user_groups_fallback(self):
|
|
578
|
+
"""Should fall back to user groups when item.properties lacks sharing data."""
|
|
579
|
+
mock_gis = Mock()
|
|
580
|
+
mock_item = Mock()
|
|
581
|
+
mock_item.access = "org"
|
|
582
|
+
mock_item.id = "item-456"
|
|
583
|
+
mock_item.properties = {} # No sharing data
|
|
584
|
+
|
|
585
|
+
# Set up user with groups
|
|
586
|
+
mock_user = Mock()
|
|
587
|
+
mock_group = Mock()
|
|
588
|
+
mock_group.id = "fallback-group"
|
|
589
|
+
mock_group_item = Mock()
|
|
590
|
+
mock_group_item.id = "item-456"
|
|
591
|
+
mock_group.content.return_value = [mock_group_item]
|
|
592
|
+
mock_group.get_members.return_value = {
|
|
593
|
+
"owner": "fallback_owner",
|
|
594
|
+
"admins": [],
|
|
595
|
+
"users": ["fallback_user"],
|
|
596
|
+
}
|
|
597
|
+
mock_user.groups = [mock_group]
|
|
598
|
+
mock_gis.users.me = mock_user
|
|
599
|
+
mock_gis.groups.get.return_value = mock_group
|
|
600
|
+
|
|
601
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
602
|
+
|
|
603
|
+
assert set(result) == {"fallback_owner", "fallback_user"}
|
|
604
|
+
|
|
605
|
+
def test_raises_on_general_error(self):
|
|
606
|
+
"""Should raise RuntimeError on unexpected errors."""
|
|
607
|
+
mock_gis = Mock()
|
|
608
|
+
mock_item = Mock()
|
|
609
|
+
type(mock_item).access = property(lambda self: (_ for _ in ()).throw(Exception("Unexpected")))
|
|
610
|
+
|
|
611
|
+
with pytest.raises(RuntimeError, match="Failed to get item group users"):
|
|
612
|
+
get_item_group_users(mock_gis, mock_item)
|
|
613
|
+
|
|
614
|
+
def test_skips_groups_with_content_error(self):
|
|
615
|
+
"""Should skip groups where content() raises an exception."""
|
|
616
|
+
mock_gis = Mock()
|
|
617
|
+
mock_item = Mock()
|
|
618
|
+
mock_item.access = "org"
|
|
619
|
+
mock_item.id = "item-789"
|
|
620
|
+
mock_item.properties = {}
|
|
621
|
+
|
|
622
|
+
mock_user = Mock()
|
|
623
|
+
mock_group_ok = Mock()
|
|
624
|
+
mock_group_ok.id = "group-ok"
|
|
625
|
+
mock_group_ok_item = Mock()
|
|
626
|
+
mock_group_ok_item.id = "item-789"
|
|
627
|
+
mock_group_ok.content.return_value = [mock_group_ok_item]
|
|
628
|
+
mock_group_ok.get_members.return_value = {
|
|
629
|
+
"owner": "ok_owner",
|
|
630
|
+
"admins": [],
|
|
631
|
+
"users": [],
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
mock_group_fail = Mock()
|
|
635
|
+
mock_group_fail.content.side_effect = Exception("Access denied")
|
|
636
|
+
|
|
637
|
+
mock_user.groups = [mock_group_fail, mock_group_ok]
|
|
638
|
+
mock_gis.users.me = mock_user
|
|
639
|
+
mock_gis.groups.get.return_value = mock_group_ok
|
|
640
|
+
|
|
641
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
642
|
+
|
|
643
|
+
assert "ok_owner" in result
|
|
644
|
+
|
|
645
|
+
def test_handles_non_dict_sharing_data(self):
|
|
646
|
+
"""Should handle non-dict sharing data gracefully."""
|
|
647
|
+
mock_gis = Mock()
|
|
648
|
+
mock_item = Mock()
|
|
649
|
+
mock_item.access = "org"
|
|
650
|
+
mock_item.id = "item-xxx"
|
|
651
|
+
mock_item.properties = {"sharing": "not-a-dict"} # Invalid format
|
|
652
|
+
mock_gis.users.me = None
|
|
653
|
+
|
|
654
|
+
result = get_item_group_users(mock_gis, mock_item)
|
|
655
|
+
|
|
656
|
+
assert result == []
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
class TestGetGroupMemberUsernamesFallback:
|
|
660
|
+
"""Tests for get_group_member_usernames edge cases."""
|
|
661
|
+
|
|
662
|
+
def test_handles_none_in_invited_lists(self):
|
|
663
|
+
"""Should handle None values in invited user lists."""
|
|
664
|
+
mock_gis = Mock()
|
|
665
|
+
mock_group = Mock()
|
|
666
|
+
mock_gis.groups.get.return_value = mock_group
|
|
667
|
+
mock_group.get_members.return_value = {
|
|
668
|
+
"owner": "owner",
|
|
669
|
+
"admins": [],
|
|
670
|
+
"users": [],
|
|
671
|
+
"admins_invited": None, # None instead of list
|
|
672
|
+
"users_invited": ["invited_user", None, ""], # Mixed valid/invalid
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
676
|
+
|
|
677
|
+
assert "owner" in result
|
|
678
|
+
assert "invited_user" in result
|
|
679
|
+
# Empty strings and None should be filtered
|
|
680
|
+
assert "" not in result
|
|
681
|
+
|
|
682
|
+
def test_handles_empty_lists_returned_as_none(self):
|
|
683
|
+
"""Should handle when members.get returns None for list keys."""
|
|
684
|
+
mock_gis = Mock()
|
|
685
|
+
mock_group = Mock()
|
|
686
|
+
mock_gis.groups.get.return_value = mock_group
|
|
687
|
+
mock_group.get_members.return_value = {
|
|
688
|
+
"owner": "sole_owner",
|
|
689
|
+
"admins": None, # None instead of empty list
|
|
690
|
+
"users": None,
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
result = get_group_member_usernames(mock_gis, "group-123")
|
|
694
|
+
|
|
695
|
+
assert result == ["sole_owner"]
|