Skip to content

Commit baf7a56

Browse files
Simplify permission checking and remove optional return types
1 parent 9ef1e05 commit baf7a56

File tree

4 files changed

+115
-98
lines changed

4 files changed

+115
-98
lines changed

src/django_github_app/mentions.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def from_event(cls, event: sansio.Event) -> Comment:
121121
class MentionContext:
122122
comment: Comment
123123
triggered_by: Mention
124-
user_permission: Permission | None
124+
user_permission: Permission
125125
scope: MentionScope | None
126126

127127

src/django_github_app/permissions.py

Lines changed: 37 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,33 @@ class PermissionCacheKey(NamedTuple):
4848
username: str
4949

5050

51-
async def aget_user_permission(
52-
gh: AsyncGitHubAPI, owner: str, repo: str, username: str
51+
class EventInfo(NamedTuple):
52+
author: str | None
53+
owner: str | None
54+
repo: str | None
55+
56+
@classmethod
57+
def from_event(cls, event: sansio.Event) -> EventInfo:
58+
comment = event.data.get("comment", {})
59+
repository = event.data.get("repository", {})
60+
61+
author = comment.get("user", {}).get("login")
62+
owner = repository.get("owner", {}).get("login")
63+
repo = repository.get("name")
64+
65+
return cls(author=author, owner=owner, repo=repo)
66+
67+
68+
async def aget_user_permission_from_event(
69+
event: sansio.Event, gh: AsyncGitHubAPI
5370
) -> Permission:
54-
cache_key = PermissionCacheKey(owner, repo, username)
71+
author, owner, repo = EventInfo.from_event(event)
72+
73+
if not (author and owner and repo):
74+
return Permission.NONE
75+
76+
# Inline the logic from aget_user_permission
77+
cache_key = PermissionCacheKey(owner, repo, author)
5578

5679
if cache_key in cache:
5780
return cache[cache_key]
@@ -61,7 +84,7 @@ async def aget_user_permission(
6184
try:
6285
# Check if user is a collaborator and get their permission
6386
data = await gh.getitem(
64-
f"/repos/{owner}/{repo}/collaborators/{username}/permission"
87+
f"/repos/{owner}/{repo}/collaborators/{author}/permission"
6588
)
6689
permission_str = data.get("permission", "none")
6790
permission = Permission.from_string(permission_str)
@@ -80,10 +103,16 @@ async def aget_user_permission(
80103
return permission
81104

82105

83-
def get_user_permission(
84-
gh: SyncGitHubAPI, owner: str, repo: str, username: str
106+
def get_user_permission_from_event(
107+
event: sansio.Event, gh: SyncGitHubAPI
85108
) -> Permission:
86-
cache_key = PermissionCacheKey(owner, repo, username)
109+
author, owner, repo = EventInfo.from_event(event)
110+
111+
if not (author and owner and repo):
112+
return Permission.NONE
113+
114+
# Inline the logic from get_user_permission
115+
cache_key = PermissionCacheKey(owner, repo, author)
87116

88117
if cache_key in cache:
89118
return cache[cache_key]
@@ -92,7 +121,7 @@ def get_user_permission(
92121

93122
try:
94123
# Check if user is a collaborator and get their permission
95-
data = gh.getitem(f"/repos/{owner}/{repo}/collaborators/{username}/permission")
124+
data = gh.getitem(f"/repos/{owner}/{repo}/collaborators/{author}/permission")
96125
permission_str = data.get("permission", "none") # type: ignore[attr-defined]
97126
permission = Permission.from_string(permission_str)
98127
except gidgethub.HTTPException as e:
@@ -108,72 +137,3 @@ def get_user_permission(
108137

109138
cache[cache_key] = permission
110139
return permission
111-
112-
113-
class EventInfo(NamedTuple):
114-
comment_author: str | None
115-
owner: str | None
116-
repo: str | None
117-
118-
@classmethod
119-
def from_event(cls, event: sansio.Event) -> EventInfo:
120-
comment_author = None
121-
owner = None
122-
repo = None
123-
124-
if "comment" in event.data:
125-
comment_author = event.data["comment"]["user"]["login"]
126-
127-
if "repository" in event.data:
128-
owner = event.data["repository"]["owner"]["login"]
129-
repo = event.data["repository"]["name"]
130-
131-
return cls(comment_author=comment_author, owner=owner, repo=repo)
132-
133-
134-
class PermissionCheck(NamedTuple):
135-
has_permission: bool
136-
137-
138-
async def aget_user_permission_from_event(
139-
event: sansio.Event, gh: AsyncGitHubAPI
140-
) -> Permission | None:
141-
comment_author, owner, repo = EventInfo.from_event(event)
142-
143-
if not (comment_author and owner and repo):
144-
return None
145-
146-
return await aget_user_permission(gh, owner, repo, comment_author)
147-
148-
149-
async def acheck_mention_permission(
150-
event: sansio.Event, gh: AsyncGitHubAPI, required_permission: Permission
151-
) -> PermissionCheck:
152-
user_permission = await aget_user_permission_from_event(event, gh)
153-
154-
if user_permission is None:
155-
return PermissionCheck(has_permission=False)
156-
157-
return PermissionCheck(has_permission=user_permission >= required_permission)
158-
159-
160-
def get_user_permission_from_event(
161-
event: sansio.Event, gh: SyncGitHubAPI
162-
) -> Permission | None:
163-
comment_author, owner, repo = EventInfo.from_event(event)
164-
165-
if not (comment_author and owner and repo):
166-
return None
167-
168-
return get_user_permission(gh, owner, repo, comment_author)
169-
170-
171-
def check_mention_permission(
172-
event: sansio.Event, gh: SyncGitHubAPI, required_permission: Permission
173-
) -> PermissionCheck:
174-
user_permission = get_user_permission_from_event(event, gh)
175-
176-
if user_permission is None:
177-
return PermissionCheck(has_permission=False)
178-
179-
return PermissionCheck(has_permission=user_permission >= required_permission)

src/django_github_app/routing.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,13 @@ async def async_wrapper(
9898
if not mentions:
9999
return
100100

101-
user_permission = await aget_user_permission_from_event(event, gh)
102101
if permission is not None:
102+
user_permission = await aget_user_permission_from_event(event, gh)
103103
required_perm = Permission[permission.upper()]
104-
if user_permission is None or user_permission < required_perm:
104+
if user_permission < required_perm:
105105
return
106+
else:
107+
user_permission = Permission.NONE
106108

107109
comment = Comment.from_event(event)
108110
comment.mentions = mentions
@@ -135,11 +137,13 @@ def sync_wrapper(
135137
if not mentions:
136138
return
137139

138-
user_permission = get_user_permission_from_event(event, gh)
139140
if permission is not None:
141+
user_permission = get_user_permission_from_event(event, gh)
140142
required_perm = Permission[permission.upper()]
141-
if user_permission is None or user_permission < required_perm:
143+
if user_permission < required_perm:
142144
return
145+
else:
146+
user_permission = Permission.NONE
143147

144148
comment = Comment.from_event(event)
145149
comment.mentions = mentions

tests/test_permissions.py

Lines changed: 69 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66

77
import gidgethub
88
import pytest
9+
from gidgethub import sansio
910

1011
from django_github_app.github import AsyncGitHubAPI
1112
from django_github_app.github import SyncGitHubAPI
1213
from django_github_app.permissions import Permission
13-
from django_github_app.permissions import aget_user_permission
14+
from django_github_app.permissions import aget_user_permission_from_event
1415
from django_github_app.permissions import cache
15-
from django_github_app.permissions import get_user_permission
16+
from django_github_app.permissions import get_user_permission_from_event
1617

1718

1819
@pytest.fixture(autouse=True)
@@ -22,6 +23,18 @@ def clear_cache():
2223
cache.clear()
2324

2425

26+
def create_test_event(username: str, owner: str, repo: str) -> sansio.Event:
27+
"""Create a test event with comment author and repository info."""
28+
return sansio.Event(
29+
{
30+
"comment": {"user": {"login": username}},
31+
"repository": {"owner": {"login": owner}, "name": repo},
32+
},
33+
event="issue_comment",
34+
delivery_id="test",
35+
)
36+
37+
2538
class TestPermission:
2639
def test_permission_ordering(self):
2740
assert Permission.NONE < Permission.READ
@@ -64,8 +77,9 @@ class TestGetUserPermission:
6477
async def test_collaborator_with_admin_permission(self):
6578
gh = create_autospec(AsyncGitHubAPI, instance=True)
6679
gh.getitem = AsyncMock(return_value={"permission": "admin"})
80+
event = create_test_event("user", "owner", "repo")
6781

68-
permission = await aget_user_permission(gh, "owner", "repo", "user")
82+
permission = await aget_user_permission_from_event(event, gh)
6983

7084
assert permission == Permission.ADMIN
7185
gh.getitem.assert_called_once_with(
@@ -75,8 +89,9 @@ async def test_collaborator_with_admin_permission(self):
7589
async def test_collaborator_with_write_permission(self):
7690
gh = create_autospec(AsyncGitHubAPI, instance=True)
7791
gh.getitem = AsyncMock(return_value={"permission": "write"})
92+
event = create_test_event("user", "owner", "repo")
7893

79-
permission = await aget_user_permission(gh, "owner", "repo", "user")
94+
permission = await aget_user_permission_from_event(event, gh)
8095

8196
assert permission == Permission.WRITE
8297

@@ -90,7 +105,8 @@ async def test_non_collaborator_public_repo(self):
90105
]
91106
)
92107

93-
permission = await aget_user_permission(gh, "owner", "repo", "user")
108+
event = create_test_event("user", "owner", "repo")
109+
permission = await aget_user_permission_from_event(event, gh)
94110

95111
assert permission == Permission.READ
96112
assert gh.getitem.call_count == 2
@@ -106,8 +122,9 @@ async def test_non_collaborator_private_repo(self):
106122
{"private": True}, # Repo is private
107123
]
108124
)
125+
event = create_test_event("user", "owner", "repo")
109126

110-
permission = await aget_user_permission(gh, "owner", "repo", "user")
127+
permission = await aget_user_permission_from_event(event, gh)
111128

112129
assert permission == Permission.NONE
113130

@@ -116,16 +133,18 @@ async def test_api_error_returns_none_permission(self):
116133
gh.getitem = AsyncMock(
117134
side_effect=gidgethub.HTTPException(500, "Server error", {})
118135
)
136+
event = create_test_event("user", "owner", "repo")
119137

120-
permission = await aget_user_permission(gh, "owner", "repo", "user")
138+
permission = await aget_user_permission_from_event(event, gh)
121139

122140
assert permission == Permission.NONE
123141

124142
async def test_missing_permission_field(self):
125143
gh = create_autospec(AsyncGitHubAPI, instance=True)
126144
gh.getitem = AsyncMock(return_value={}) # No permission field
145+
event = create_test_event("user", "owner", "repo")
127146

128-
permission = await aget_user_permission(gh, "owner", "repo", "user")
147+
permission = await aget_user_permission_from_event(event, gh)
129148

130149
assert permission == Permission.NONE
131150

@@ -134,8 +153,9 @@ class TestGetUserPermissionSync:
134153
def test_collaborator_with_permission(self):
135154
gh = create_autospec(SyncGitHubAPI, instance=True)
136155
gh.getitem = Mock(return_value={"permission": "maintain"})
156+
event = create_test_event("user", "owner", "repo")
137157

138-
permission = get_user_permission(gh, "owner", "repo", "user")
158+
permission = get_user_permission_from_event(event, gh)
139159

140160
assert permission == Permission.MAINTAIN
141161
gh.getitem.assert_called_once_with(
@@ -151,8 +171,9 @@ def test_non_collaborator_public_repo(self):
151171
{"private": False}, # Repo is public
152172
]
153173
)
174+
event = create_test_event("user", "owner", "repo")
154175

155-
permission = get_user_permission(gh, "owner", "repo", "user")
176+
permission = get_user_permission_from_event(event, gh)
156177

157178
assert permission == Permission.READ
158179

@@ -162,14 +183,15 @@ class TestPermissionCaching:
162183
async def test_cache_hit(self):
163184
gh = create_autospec(AsyncGitHubAPI, instance=True)
164185
gh.getitem = AsyncMock(return_value={"permission": "write"})
186+
event = create_test_event("user", "owner", "repo")
165187

166188
# First call should hit the API
167-
perm1 = await aget_user_permission(gh, "owner", "repo", "user")
189+
perm1 = await aget_user_permission_from_event(event, gh)
168190
assert perm1 == Permission.WRITE
169191
assert gh.getitem.call_count == 1
170192

171193
# Second call should use cache
172-
perm2 = await aget_user_permission(gh, "owner", "repo", "user")
194+
perm2 = await aget_user_permission_from_event(event, gh)
173195
assert perm2 == Permission.WRITE
174196
assert gh.getitem.call_count == 1 # No additional API call
175197

@@ -182,9 +204,11 @@ async def test_cache_different_users(self):
182204
{"permission": "admin"},
183205
]
184206
)
207+
event1 = create_test_event("user1", "owner", "repo")
208+
event2 = create_test_event("user2", "owner", "repo")
185209

186-
perm1 = await aget_user_permission(gh, "owner", "repo", "user1")
187-
perm2 = await aget_user_permission(gh, "owner", "repo", "user2")
210+
perm1 = await aget_user_permission_from_event(event1, gh)
211+
perm2 = await aget_user_permission_from_event(event2, gh)
188212

189213
assert perm1 == Permission.WRITE
190214
assert perm2 == Permission.ADMIN
@@ -194,13 +218,42 @@ def test_sync_cache_hit(self):
194218
"""Test that sync version uses cache."""
195219
gh = create_autospec(SyncGitHubAPI, instance=True)
196220
gh.getitem = Mock(return_value={"permission": "read"})
221+
event = create_test_event("user", "owner", "repo")
197222

198223
# First call should hit the API
199-
perm1 = get_user_permission(gh, "owner", "repo", "user")
224+
perm1 = get_user_permission_from_event(event, gh)
200225
assert perm1 == Permission.READ
201226
assert gh.getitem.call_count == 1
202227

203228
# Second call should use cache
204-
perm2 = get_user_permission(gh, "owner", "repo", "user")
229+
perm2 = get_user_permission_from_event(event, gh)
205230
assert perm2 == Permission.READ
206231
assert gh.getitem.call_count == 1 # No additional API call
232+
233+
234+
class TestPermissionFromEvent:
235+
@pytest.mark.asyncio
236+
async def test_missing_comment_data(self):
237+
"""Test when event has no comment data."""
238+
gh = create_autospec(AsyncGitHubAPI, instance=True)
239+
event = sansio.Event({}, event="issue_comment", delivery_id="test")
240+
241+
permission = await aget_user_permission_from_event(event, gh)
242+
243+
assert permission == Permission.NONE
244+
assert gh.getitem.called is False
245+
246+
@pytest.mark.asyncio
247+
async def test_missing_repository_data(self):
248+
"""Test when event has no repository data."""
249+
gh = create_autospec(AsyncGitHubAPI, instance=True)
250+
event = sansio.Event(
251+
{"comment": {"user": {"login": "user"}}},
252+
event="issue_comment",
253+
delivery_id="test",
254+
)
255+
256+
permission = await aget_user_permission_from_event(event, gh)
257+
258+
assert permission == Permission.NONE
259+
assert gh.getitem.called is False

0 commit comments

Comments
 (0)