mirror of
https://github.com/Sea-Haven-Industries/open-swe.git
synced 2026-09-30 08:03:15 +00:00
feat: gate @open-swe mentions on public repos to org members (#1273)
Adds a webhook-level check so only members of $PUBLIC_REPO_ORG_GATE (e.g. langchain-ai) can trigger Open SWE via mentions or review requests on public repositories. Private repos remain governed by the existing org/repo allowlists. Internal bots bypass the gate. Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com> Co-authored-by: Johannes du Plessis <51395795+johannes117@users.noreply.github.com>
This commit is contained in:
parent
5d1020e8d7
commit
dc9a0b98da
3 changed files with 546 additions and 0 deletions
65
agent/utils/github_org_membership.py
Normal file
65
agent/utils/github_org_membership.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""GitHub organization membership checks for webhook gating."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
import httpx
|
||||
|
||||
from .github_app import get_github_app_installation_token
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
INTERNAL_BOT_LOGINS: frozenset[str] = frozenset({"open-swe[bot]", "openswe-dev[bot]"})
|
||||
|
||||
|
||||
async def is_user_active_org_member(username: str, org: str) -> bool:
|
||||
"""Return True if ``username`` is an *active* member of ``org``.
|
||||
|
||||
Uses the GitHub App installation token so that private organization
|
||||
memberships are visible (the same approach as the reference
|
||||
``tag-external-contributions.yml`` workflow). On any API error, returns
|
||||
``False`` — fail-closed for security.
|
||||
"""
|
||||
if not username or not org:
|
||||
return False
|
||||
|
||||
token = await get_github_app_installation_token()
|
||||
if not token:
|
||||
logger.warning(
|
||||
"GitHub App token unavailable; cannot verify org membership for %s", username
|
||||
)
|
||||
return False
|
||||
|
||||
url = f"https://api.github.com/orgs/{org}/memberships/{username}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=10.0) as client:
|
||||
response = await client.get(
|
||||
url,
|
||||
headers={
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Accept": "application/vnd.github+json",
|
||||
"X-GitHub-Api-Version": "2022-11-28",
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Error calling GitHub org membership API for %s/%s", org, username)
|
||||
return False
|
||||
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
if response.status_code != 200:
|
||||
logger.warning(
|
||||
"Unexpected status %s checking %s membership for %s",
|
||||
response.status_code,
|
||||
org,
|
||||
username,
|
||||
)
|
||||
return False
|
||||
|
||||
try:
|
||||
state = response.json().get("state")
|
||||
except ValueError:
|
||||
logger.warning("Failed to parse org membership response for %s/%s", org, username)
|
||||
return False
|
||||
return state == "active"
|
||||
|
|
@ -43,6 +43,7 @@ from .utils.github_comments import (
|
|||
sanitize_github_comment_body,
|
||||
verify_github_signature,
|
||||
)
|
||||
from .utils.github_org_membership import INTERNAL_BOT_LOGINS, is_user_active_org_member
|
||||
from .utils.github_token import get_github_token_from_thread
|
||||
from .utils.github_user_email_map import GITHUB_USER_EMAIL_MAP
|
||||
from .utils.linear import post_linear_trace_comment
|
||||
|
|
@ -115,6 +116,9 @@ ALLOWED_REVIEWER_GITHUB_REPOS: frozenset[str] = frozenset(
|
|||
for repo in os.environ.get("ALLOWED_REVIEWER_GITHUB_REPOS", "").split(",")
|
||||
if repo.strip()
|
||||
)
|
||||
# Org whose members are allowed to tag @open-swe on public repos. When empty,
|
||||
# the public-repo gate is disabled (back-compat).
|
||||
PUBLIC_REPO_ORG_GATE: str = os.environ.get("PUBLIC_REPO_ORG_GATE", "").strip()
|
||||
|
||||
LINEAR_API_KEY = os.environ.get("LINEAR_API_KEY", "")
|
||||
|
||||
|
|
@ -363,6 +367,57 @@ def _is_repo_allowed_for_reviewer(repo_config: dict[str, str]) -> bool:
|
|||
return owner in ALLOWED_REVIEWER_GITHUB_ORGS
|
||||
|
||||
|
||||
_PUBLIC_REPO_GATE_REJECTION = {
|
||||
"status": "ignored",
|
||||
"reason": "Sender is not a member of the allowed organization for public-repo triggers",
|
||||
}
|
||||
|
||||
|
||||
async def _is_sender_allowed_for_public_repo(payload: dict[str, Any]) -> bool:
|
||||
"""Public-repo gate: only ``PUBLIC_REPO_ORG_GATE`` org members may trigger.
|
||||
|
||||
Returns True (allowed) when:
|
||||
- The gate is disabled (``PUBLIC_REPO_ORG_GATE`` empty), OR
|
||||
- The repo is private (gate only applies to public repos), OR
|
||||
- The sender is a known internal bot, OR
|
||||
- The sender is an active member of ``PUBLIC_REPO_ORG_GATE``.
|
||||
"""
|
||||
if not PUBLIC_REPO_ORG_GATE:
|
||||
return True
|
||||
|
||||
repository = payload.get("repository") or {}
|
||||
if repository.get("private", False):
|
||||
return True
|
||||
|
||||
sender = payload.get("sender") or {}
|
||||
sender_login = sender.get("login", "") or ""
|
||||
if sender_login in INTERNAL_BOT_LOGINS:
|
||||
return True
|
||||
|
||||
if not sender_login:
|
||||
return False
|
||||
|
||||
return await is_user_active_org_member(sender_login, PUBLIC_REPO_ORG_GATE)
|
||||
|
||||
|
||||
async def _enforce_public_repo_org_gate(
|
||||
payload: dict[str, Any], event_type: str
|
||||
) -> dict[str, str] | None:
|
||||
"""Return a rejection response if the public-repo org gate blocks this event."""
|
||||
if await _is_sender_allowed_for_public_repo(payload):
|
||||
return None
|
||||
sender_login = (payload.get("sender") or {}).get("login", "")
|
||||
repo = payload.get("repository") or {}
|
||||
logger.warning(
|
||||
"Blocking GitHub %s from non-org-member sender '%s' on public repo '%s/%s'",
|
||||
event_type,
|
||||
sender_login,
|
||||
(repo.get("owner") or {}).get("login", ""),
|
||||
repo.get("name", ""),
|
||||
)
|
||||
return _PUBLIC_REPO_GATE_REJECTION
|
||||
|
||||
|
||||
async def _upsert_slack_thread_repo_metadata(
|
||||
thread_id: str, repo_config: dict[str, str], langgraph_client: LangGraphClient
|
||||
) -> None:
|
||||
|
|
@ -2201,6 +2256,10 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
|
|||
reason = "Repository org not in allowlist"
|
||||
return {"status": "ignored", "reason": reason}
|
||||
|
||||
gate_rejection = await _enforce_public_repo_org_gate(payload, "pull_request")
|
||||
if gate_rejection is not None:
|
||||
return gate_rejection
|
||||
|
||||
logger.info("Accepted GitHub PR review request webhook, scheduling reviewer task")
|
||||
background_tasks.add_task(process_github_pr_review_request, payload)
|
||||
return {"status": "accepted", "message": "Processing GitHub PR review request"}
|
||||
|
|
@ -2235,6 +2294,10 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
|
|||
logger.info("Ignoring issue that does not mention @openswe or @open-swe")
|
||||
return {"status": "ignored", "reason": "Issue does not mention @openswe or @open-swe"}
|
||||
|
||||
gate_rejection = await _enforce_public_repo_org_gate(payload, event_type)
|
||||
if gate_rejection is not None:
|
||||
return gate_rejection
|
||||
|
||||
logger.info("Accepted GitHub issue webhook, scheduling background task")
|
||||
background_tasks.add_task(process_github_issue, payload, event_type)
|
||||
return {"status": "accepted", "message": "Processing GitHub issue event"}
|
||||
|
|
@ -2258,6 +2321,10 @@ async def github_webhook(request: Request, background_tasks: BackgroundTasks) ->
|
|||
)
|
||||
return {"status": "ignored", "reason": "Comment does not mention @openswe or @open-swe"}
|
||||
|
||||
gate_rejection = await _enforce_public_repo_org_gate(payload, event_type)
|
||||
if gate_rejection is not None:
|
||||
return gate_rejection
|
||||
|
||||
logger.info("Accepted GitHub webhook: event=%s, scheduling background task", event_type)
|
||||
if is_pull_request_comment or event_type in {
|
||||
"pull_request_review_comment",
|
||||
|
|
|
|||
414
tests/test_public_repo_org_gate.py
Normal file
414
tests/test_public_repo_org_gate.py
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
"""Tests for the public-repo org-membership gate on GitHub webhooks."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from agent import webapp
|
||||
|
||||
_TEST_WEBHOOK_SECRET = "test-secret-for-webhook"
|
||||
|
||||
|
||||
def _sign_body(body: bytes, secret: str = _TEST_WEBHOOK_SECRET) -> str:
|
||||
sig = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
|
||||
return f"sha256={sig}"
|
||||
|
||||
|
||||
def _post_github_webhook(client: TestClient, event_type: str, payload: dict) -> object:
|
||||
body = json.dumps(payload, separators=(",", ":")).encode()
|
||||
return client.post(
|
||||
"/webhooks/github",
|
||||
content=body,
|
||||
headers={
|
||||
"X-GitHub-Event": event_type,
|
||||
"X-Hub-Signature-256": _sign_body(body),
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _install_membership_stub(monkeypatch, members: set[str]) -> dict[str, list[str]]:
|
||||
"""Stub the org-membership lookup to return True only for ``members``."""
|
||||
seen: dict[str, list[str]] = {"calls": []}
|
||||
|
||||
async def fake_is_user_active_org_member(username: str, org: str) -> bool:
|
||||
seen["calls"].append(username)
|
||||
return username in members
|
||||
|
||||
monkeypatch.setattr(webapp, "is_user_active_org_member", fake_is_user_active_org_member)
|
||||
return seen
|
||||
|
||||
|
||||
def _common_setup(monkeypatch, *, gate: str = "langchain-ai") -> None:
|
||||
monkeypatch.setattr(webapp, "GITHUB_WEBHOOK_SECRET", _TEST_WEBHOOK_SECRET)
|
||||
monkeypatch.setattr(webapp, "PUBLIC_REPO_ORG_GATE", gate)
|
||||
monkeypatch.setattr(webapp, "ALLOWED_GITHUB_ORGS", frozenset())
|
||||
monkeypatch.setattr(webapp, "ALLOWED_REVIEWER_GITHUB_ORGS", frozenset())
|
||||
monkeypatch.setattr(webapp, "ALLOWED_REVIEWER_GITHUB_REPOS", frozenset())
|
||||
|
||||
|
||||
def test_gate_blocks_non_member_on_public_pr_comment(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
seen = _install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
async def fake_process_github_pr_review_command(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
async def fake_process_github_pr_comment(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_command", fake_process_github_pr_review_command
|
||||
)
|
||||
monkeypatch.setattr(webapp, "process_github_pr_comment", fake_process_github_pr_comment)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"issue_comment",
|
||||
{
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 1,
|
||||
"number": 7,
|
||||
"title": "PR title",
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/langchain-ai/open-swe/pulls/7"
|
||||
},
|
||||
},
|
||||
"comment": {"body": "@open-swe review"},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": False,
|
||||
},
|
||||
"sender": {"login": "stranger", "type": "User"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["status"] == "ignored"
|
||||
assert "not a member" in body["reason"]
|
||||
assert seen["calls"] == ["stranger"]
|
||||
|
||||
|
||||
def test_gate_allows_org_member_on_public_pr_comment(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
_install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
called: dict[str, object] = {}
|
||||
|
||||
async def fake_process_github_pr_review_command(payload, event_type, pr_url_override) -> None:
|
||||
called["event"] = event_type
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_command", fake_process_github_pr_review_command
|
||||
)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"issue_comment",
|
||||
{
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 1,
|
||||
"number": 7,
|
||||
"title": "PR title",
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/langchain-ai/open-swe/pulls/7"
|
||||
},
|
||||
},
|
||||
"comment": {"body": "@open-swe review"},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": False,
|
||||
},
|
||||
"sender": {"login": "insider", "type": "User"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "accepted"
|
||||
assert called["event"] == "issue_comment"
|
||||
|
||||
|
||||
def test_gate_skipped_on_private_repo(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
seen = _install_membership_stub(monkeypatch, members=set())
|
||||
|
||||
called: dict[str, object] = {}
|
||||
|
||||
async def fake_process_github_pr_comment(payload, event_type) -> None:
|
||||
called["event"] = event_type
|
||||
|
||||
monkeypatch.setattr(webapp, "process_github_pr_comment", fake_process_github_pr_comment)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"issue_comment",
|
||||
{
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 1,
|
||||
"number": 7,
|
||||
"title": "PR title",
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/langchain-ai/open-swe/pulls/7"
|
||||
},
|
||||
},
|
||||
"comment": {"body": "@open-swe please look at this"},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": True,
|
||||
},
|
||||
"sender": {"login": "stranger", "type": "User"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "accepted"
|
||||
assert called["event"] == "issue_comment"
|
||||
assert seen["calls"] == []
|
||||
|
||||
|
||||
def test_gate_disabled_when_env_unset(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch, gate="")
|
||||
seen = _install_membership_stub(monkeypatch, members=set())
|
||||
|
||||
called: dict[str, object] = {}
|
||||
|
||||
async def fake_process_github_pr_comment(payload, event_type) -> None:
|
||||
called["event"] = event_type
|
||||
|
||||
monkeypatch.setattr(webapp, "process_github_pr_comment", fake_process_github_pr_comment)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"issue_comment",
|
||||
{
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 1,
|
||||
"number": 7,
|
||||
"title": "PR title",
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/langchain-ai/open-swe/pulls/7"
|
||||
},
|
||||
},
|
||||
"comment": {"body": "@open-swe please look at this"},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": False,
|
||||
},
|
||||
"sender": {"login": "stranger", "type": "User"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "accepted"
|
||||
assert called["event"] == "issue_comment"
|
||||
assert seen["calls"] == []
|
||||
|
||||
|
||||
def test_gate_blocks_non_member_on_public_issue(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
_install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
async def fake_process_github_issue(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
monkeypatch.setattr(webapp, "process_github_issue", fake_process_github_issue)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"issues",
|
||||
{
|
||||
"action": "opened",
|
||||
"issue": {
|
||||
"id": 1,
|
||||
"number": 7,
|
||||
"title": "@openswe please help",
|
||||
"body": "x",
|
||||
},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": False,
|
||||
},
|
||||
"sender": {"login": "stranger", "type": "User"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["status"] == "ignored"
|
||||
assert "not a member" in body["reason"]
|
||||
|
||||
|
||||
def test_gate_blocks_non_member_on_public_review_requested(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
_install_membership_stub(monkeypatch, members={"insider"})
|
||||
|
||||
async def fake_process_github_pr_review_request(*_args, **_kwargs) -> None:
|
||||
raise AssertionError("should not be called")
|
||||
|
||||
monkeypatch.setattr(
|
||||
webapp, "process_github_pr_review_request", fake_process_github_pr_review_request
|
||||
)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"pull_request",
|
||||
{
|
||||
"action": "review_requested",
|
||||
"requested_reviewer": {"login": "open-swe[bot]"},
|
||||
"pull_request": {
|
||||
"number": 1244,
|
||||
"html_url": "https://github.com/langchain-ai/open-swe/pull/1244",
|
||||
"base": {"sha": "base-sha"},
|
||||
"head": {"sha": "head-sha", "ref": "feature-branch"},
|
||||
},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": False,
|
||||
},
|
||||
"sender": {"login": "stranger", "type": "User"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
body = response.json()
|
||||
assert body["status"] == "ignored"
|
||||
assert "not a member" in body["reason"]
|
||||
|
||||
|
||||
def test_gate_allows_internal_bot_sender(monkeypatch) -> None:
|
||||
_common_setup(monkeypatch)
|
||||
seen = _install_membership_stub(monkeypatch, members=set())
|
||||
|
||||
called: dict[str, object] = {}
|
||||
|
||||
async def fake_process_github_pr_comment(payload, event_type) -> None:
|
||||
called["event"] = event_type
|
||||
|
||||
monkeypatch.setattr(webapp, "process_github_pr_comment", fake_process_github_pr_comment)
|
||||
|
||||
client = TestClient(webapp.app)
|
||||
response = _post_github_webhook(
|
||||
client,
|
||||
"issue_comment",
|
||||
{
|
||||
"action": "created",
|
||||
"issue": {
|
||||
"id": 1,
|
||||
"number": 7,
|
||||
"title": "PR title",
|
||||
"pull_request": {
|
||||
"url": "https://api.github.com/repos/langchain-ai/open-swe/pulls/7"
|
||||
},
|
||||
},
|
||||
"comment": {"body": "@open-swe please look at this"},
|
||||
"repository": {
|
||||
"owner": {"login": "langchain-ai"},
|
||||
"name": "open-swe",
|
||||
"private": False,
|
||||
},
|
||||
"sender": {"login": "open-swe[bot]", "type": "Bot"},
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["status"] == "accepted"
|
||||
assert called["event"] == "issue_comment"
|
||||
assert seen["calls"] == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_user_active_org_member_returns_false_when_no_token(monkeypatch) -> None:
|
||||
from agent.utils import github_org_membership
|
||||
|
||||
async def fake_token() -> None:
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(github_org_membership, "get_github_app_installation_token", fake_token)
|
||||
|
||||
assert await github_org_membership.is_user_active_org_member("alice", "langchain-ai") is False
|
||||
|
||||
|
||||
class _FakeAsyncClient:
|
||||
def __init__(self, response) -> None:
|
||||
self._response = response
|
||||
|
||||
async def __aenter__(self) -> _FakeAsyncClient:
|
||||
return self
|
||||
|
||||
async def __aexit__(self, *_args) -> None:
|
||||
return None
|
||||
|
||||
async def get(self, *_args, **_kwargs):
|
||||
return self._response
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
def __init__(self, status_code: int, payload: dict | None = None) -> None:
|
||||
self.status_code = status_code
|
||||
self._payload = payload or {}
|
||||
|
||||
def json(self) -> dict:
|
||||
return self._payload
|
||||
|
||||
|
||||
def _patch_membership_http(monkeypatch, response: _FakeResponse) -> None:
|
||||
from agent.utils import github_org_membership
|
||||
|
||||
async def fake_token() -> str:
|
||||
return "x"
|
||||
|
||||
monkeypatch.setattr(github_org_membership, "get_github_app_installation_token", fake_token)
|
||||
|
||||
def factory(*_args, **_kwargs) -> _FakeAsyncClient:
|
||||
return _FakeAsyncClient(response)
|
||||
|
||||
monkeypatch.setattr(github_org_membership.httpx, "AsyncClient", factory)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_user_active_org_member_handles_404(monkeypatch) -> None:
|
||||
from agent.utils import github_org_membership
|
||||
|
||||
_patch_membership_http(monkeypatch, _FakeResponse(404))
|
||||
|
||||
assert await github_org_membership.is_user_active_org_member("alice", "langchain-ai") is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_user_active_org_member_active(monkeypatch) -> None:
|
||||
from agent.utils import github_org_membership
|
||||
|
||||
_patch_membership_http(monkeypatch, _FakeResponse(200, {"state": "active"}))
|
||||
|
||||
assert await github_org_membership.is_user_active_org_member("alice", "langchain-ai") is True
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_is_user_active_org_member_pending_returns_false(monkeypatch) -> None:
|
||||
from agent.utils import github_org_membership
|
||||
|
||||
_patch_membership_http(monkeypatch, _FakeResponse(200, {"state": "pending"}))
|
||||
|
||||
assert await github_org_membership.is_user_active_org_member("alice", "langchain-ai") is False
|
||||
Loading…
Add table
Reference in a new issue