open-swe/tests/test_slack_oauth.py
Johannes du Plessis cb4c643e43
feat: open Slack-triggered PRs as the triggering user (#1375)
* feat: open Slack-triggered PRs as the triggering user

Route the Slack per-user GitHub token through the dashboard OAuth store
(the backend the self-service link prompt populates) and block runs that
lack a valid user token, prompting the user to (re-)link. Per-user OAuth
now wins over bot-token-only mode for mapped Slack/dashboard users.

Flip commit/PR authorship across all sources: the triggering user is the
commit author (via repo-local git identity using their resolvable GitHub
noreply email) and open-swe[bot] is the Co-authored-by collaborator.

* fix: address PR review — shell-escape commit identity, fix token cache impersonation

- Shell-escape the triggering user's name/email with shlex.quote before
  embedding them in the repo-setup `git config` command, so a name like
  O'Connor (or a crafted one) can't break or inject into the command.
- Stop consulting the shared thread-metadata token cache in
  _resolve_dashboard_user_token. Slack thread ids are shared across the
  conversation, so a cached token from a prior triggering user could be
  returned for the current github_login. Always resolve by login from the
  dashboard OAuth store instead.

* feat: dashboard self-service user mapping + UI cleanup

- Add session-scoped GET/PUT /dashboard/api/my-mapping so users can set their
  own work email / Slack member ID (keyed by their GitHub login, source=self).
- Slack account-link prompt now redirects to Profile Settings after auth.
- Rename "My Settings" -> "Profile Settings" and "Cloud Agents" -> "Open SWE
  Agent"; remove the Integrations tab/section (folded out, low value for now)
  and redirect /integrations to Profile Settings.
- Add a "User mapping" section to Profile Settings (work email used by Slack
  and Linear, optional Slack member ID).
- Make dashboard auth cookies scheme-aware: Secure;SameSite=None over HTTPS,
  non-Secure;SameSite=Lax over http://localhost so local login works.

* feat: self-service Slack account linking via Sign in with Slack (OIDC)

Replace the spoofable manual work-email/Slack-ID form with a verified
"Sign in with Slack" flow so a logged-in GitHub user can only ever link
their own Slack identity.

- New agent/dashboard/slack_oauth.py: OIDC authorize URL, code exchange,
  userInfo identity parse, optional workspace gate, configured check.
- routes.py: session-gated GET /slack/login and /slack/callback that upsert
  the mapping from Slack-verified user_id + email (source=slack_oauth).
  Remove the spoofable PUT /my-mapping; expose slack_oauth_enabled on /me.
- UI: drop the editable inputs; add a Connect Slack button + status to the
  User mapping section.

Admin-managed mappings are unaffected and still resolve at trigger time.
2026-06-02 15:04:20 -07:00

86 lines
3.1 KiB
Python

from __future__ import annotations
from urllib.parse import parse_qs, urlparse
import pytest
from fastapi import HTTPException
from agent.dashboard import slack_oauth
def test_slack_oauth_configured(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(slack_oauth, "SLACK_CLIENT_ID", "cid")
monkeypatch.setattr(slack_oauth, "SLACK_CLIENT_SECRET", "secret")
assert slack_oauth.slack_oauth_configured() is True
monkeypatch.setattr(slack_oauth, "SLACK_CLIENT_SECRET", "")
assert slack_oauth.slack_oauth_configured() is False
def test_build_authorize_url(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(slack_oauth, "SLACK_CLIENT_ID", "cid")
monkeypatch.setattr(slack_oauth, "SLACK_TEAM_ID", "")
url = slack_oauth.build_authorize_url(
redirect_uri="http://localhost:2024/dashboard/api/slack/callback", state="ST8"
)
parsed = urlparse(url)
q = parse_qs(parsed.query)
assert parsed.netloc == "slack.com"
assert q["response_type"] == ["code"]
assert q["scope"] == ["openid email profile"]
assert q["client_id"] == ["cid"]
assert q["redirect_uri"] == ["http://localhost:2024/dashboard/api/slack/callback"]
assert q["state"] == ["ST8"]
assert "team" not in q
def test_build_authorize_url_includes_team_when_configured(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(slack_oauth, "SLACK_CLIENT_ID", "cid")
monkeypatch.setattr(slack_oauth, "SLACK_TEAM_ID", "T123")
url = slack_oauth.build_authorize_url(redirect_uri="https://x/cb", state="S")
assert parse_qs(urlparse(url).query)["team"] == ["T123"]
def test_parse_slack_identity_success() -> None:
identity = slack_oauth.parse_slack_identity(
{
"ok": True,
"https://slack.com/user_id": "U999",
"https://slack.com/team_id": "T123",
"email": "dev@example.com",
"email_verified": True,
"name": "Dev",
}
)
assert identity.user_id == "U999"
assert identity.team_id == "T123"
assert identity.email == "dev@example.com"
assert identity.email_verified is True
assert identity.name == "Dev"
def test_parse_slack_identity_missing_user_id() -> None:
with pytest.raises(HTTPException):
slack_oauth.parse_slack_identity({"ok": True, "email": "x@y.com"})
def test_parse_slack_identity_not_ok() -> None:
with pytest.raises(HTTPException):
slack_oauth.parse_slack_identity({"ok": False, "error": "bad"})
def test_verify_team(monkeypatch: pytest.MonkeyPatch) -> None:
ident = slack_oauth.SlackIdentity(
user_id="U1", team_id="T1", email="a@b.com", email_verified=True, name=None
)
# No workspace restriction configured → always allowed.
monkeypatch.setattr(slack_oauth, "SLACK_TEAM_ID", "")
slack_oauth.verify_team(ident)
# Matching workspace → allowed.
monkeypatch.setattr(slack_oauth, "SLACK_TEAM_ID", "T1")
slack_oauth.verify_team(ident)
# Different workspace → rejected.
monkeypatch.setattr(slack_oauth, "SLACK_TEAM_ID", "T2")
with pytest.raises(HTTPException):
slack_oauth.verify_team(ident)