open-swe/tests/test_account_link.py
Johannes du Plessis 427bfe4f56
feat: Store-backed GitHub/Slack user mapping (self-service + admin) (#1369)
* Replace hardcoded GitHub-email map with Store-backed user mapping

Move the static GITHUB_USER_EMAIL_MAP to a Store-backed bidirectional
mapping (GitHub login <-> work email <-> optional Slack ID) with an
in-process cache, self-service onboarding, and admin management.

- agent/dashboard/user_mappings.py: Store CRUD + login/email/slack-id
  indexes, sync cache readers for hot paths, async fallthrough, and a
  bulk_import that preserves existing richer records.
- Migrate all read sites (auth.py, agent_overrides.py, authorship.py,
  github_comments.py, webapp.py x2) off the dict.
- Unmapped Slack tags now run on the GitHub App installation token
  (use_installation_token_fallback) and get an ephemeral "link your
  GitHub account" prompt carrying the Slack id + email via a signed
  account-link token threaded through the OAuth state.
- OAuth callback completes a self-service (org-gated) mapping from that
  token, falling back to the verified GitHub email.
- Admin CRUD endpoints + one-time legacy import; dashboard UI section.
- Legacy dict retained only as the import payload (no longer read).

Tests: mapping store, account-link round-trip + completion, mapped vs
unmapped Slack flows; existing trust-gate tests updated to prime cache.

* Address review: cold-cache email resolution + stale alias de-indexing

- agent_overrides: add resolve_login_from_email_async that falls through to
  the Store on a cold cache; use it at the async repo-resolution call sites
  (Slack repo config, Linear comment, owner-metadata) so a mapped user still
  resolves to their GitHub login + dashboard default_repo on a fresh worker.
- user_mappings.upsert_mapping: de-index the existing login before re-indexing
  so a changed email/Slack id no longer leaves stale aliases resolving to the
  login in-process.
- Tests for both fixes; update Slack repo-config test to patch the async resolver.
2026-06-01 14:37:19 -07:00

50 lines
1.9 KiB
Python

"""Tests for the Slack→GitHub account-link OAuth threading."""
from __future__ import annotations
import pytest
from agent.dashboard import oauth
@pytest.fixture(autouse=True)
def _jwt_secret(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DASHBOARD_JWT_SECRET", "test-secret")
def test_account_link_round_trip() -> None:
token = oauth.issue_account_link(slack_user_id="U123", work_email="dev@x.com")
payload = oauth.decode_account_link(token)
assert payload is not None
assert payload["slack_user_id"] == "U123"
assert payload["work_email"] == "dev@x.com"
assert payload["kind"] == "account_link"
def test_decode_account_link_rejects_garbage() -> None:
assert oauth.decode_account_link("") is None
assert oauth.decode_account_link("not-a-jwt") is None
def test_decode_account_link_rejects_wrong_kind() -> None:
# A session token is a valid JWT but not an account-link token.
session = oauth.issue_session(login="x", email="x@x.com", avatar_url=None)
assert oauth.decode_account_link(session) is None
def test_build_account_link_url(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DASHBOARD_API_BASE_URL", "https://api.example.com/")
url = oauth.build_account_link_url(slack_user_id="U1", work_email="d@x.com")
assert url is not None
assert url.startswith("https://api.example.com/dashboard/api/auth/login?link=")
# The embedded token must decode back to the same identity.
token = url.split("link=", 1)[1]
from urllib.parse import unquote
payload = oauth.decode_account_link(unquote(token))
assert payload["slack_user_id"] == "U1"
def test_build_account_link_url_none_without_base(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("DASHBOARD_API_BASE_URL", raising=False)
assert oauth.build_account_link_url(slack_user_id="U1", work_email="d@x.com") is None