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.
@@ -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"]