mirror of
https://github.com/usestrix/strix.git
synced 2026-08-16 09:26:39 +02:00
165 lines
5.7 KiB
Python
165 lines
5.7 KiB
Python
"""Tests for the `strix auth` CLI: subcommand routing and provider naming."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
|
|
from strix.config import codex, opencode
|
|
from strix.interface import auth_cli
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _tmp_store(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(codex, "AUTH_PATH", tmp_path / "home" / ".strix" / "subscription-auth.json")
|
|
|
|
|
|
def test_login_provider_is_chatgpt() -> None:
|
|
assert auth_cli.LOGIN_PROVIDER == "chatgpt"
|
|
assert codex.PROVIDER in auth_cli._ACCEPTED_PROVIDERS
|
|
assert "chatgpt" in auth_cli._ACCEPTED_PROVIDERS
|
|
|
|
|
|
def test_unknown_subcommand_returns_usage_error() -> None:
|
|
assert auth_cli.run_auth(["bogus"]) == 2
|
|
|
|
|
|
def test_help_returns_zero() -> None:
|
|
assert auth_cli.run_auth(["--help"]) == 0
|
|
|
|
|
|
def test_status_not_signed_in() -> None:
|
|
assert auth_cli.run_auth(["status"]) == 1
|
|
|
|
|
|
def test_login_rejects_unsupported_provider(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
def _should_not_run(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
|
|
msg = "OAuth flow must not start for an unsupported provider"
|
|
raise AssertionError(msg)
|
|
|
|
monkeypatch.setattr(auth_cli, "_run_oauth_flow", _should_not_run)
|
|
assert auth_cli.run_auth(["login", "gemini"]) == 2
|
|
|
|
|
|
def test_finish_requires_state_on_loopback(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
|
|
|
|
# Loopback (require_state=True): missing or mismatched state is rejected.
|
|
with pytest.raises(codex.CodexAuthError) as missing:
|
|
auth_cli._finish("code", None, "verifier", "expected", require_state=True)
|
|
assert missing.value.code == "state_mismatch"
|
|
with pytest.raises(codex.CodexAuthError) as mismatch:
|
|
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=True)
|
|
assert mismatch.value.code == "state_mismatch"
|
|
|
|
# Matching state proceeds to the exchange.
|
|
assert auth_cli._finish("code", "expected", "verifier", "expected", require_state=True) == {
|
|
"ok": True
|
|
}
|
|
|
|
|
|
def test_finish_manual_paste_allows_absent_state(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr(codex, "exchange_code", lambda *_: {"ok": True})
|
|
# Manual paste (require_state=False): a bare code with no state is accepted,
|
|
# but a present-and-wrong state is still rejected.
|
|
assert auth_cli._finish("code", None, "verifier", "expected", require_state=False) == {
|
|
"ok": True
|
|
}
|
|
with pytest.raises(codex.CodexAuthError):
|
|
auth_cli._finish("code", "wrong", "verifier", "expected", require_state=False)
|
|
|
|
|
|
def test_finish_rejects_missing_code() -> None:
|
|
with pytest.raises(codex.CodexAuthError) as exc:
|
|
auth_cli._finish(None, "expected", "verifier", "expected", require_state=True)
|
|
assert exc.value.code == "no_code"
|
|
|
|
|
|
def test_model_subcommand_removed() -> None:
|
|
assert auth_cli.run_auth(["model", "gpt-5.5"]) == 2
|
|
|
|
|
|
@pytest.mark.parametrize("provider", ["chatgpt", "codex", "ChatGPT"])
|
|
def test_login_accepts_provider_aliases(provider: str, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
reached = {"flow": False}
|
|
|
|
def _fake_flow(*_args: Any, **_kwargs: Any) -> dict[str, Any]:
|
|
reached["flow"] = True
|
|
return {
|
|
"type": "oauth",
|
|
"provider": "codex",
|
|
"access": "a",
|
|
"refresh": "r",
|
|
"account_id": "acct",
|
|
"expires_at": 0,
|
|
}
|
|
|
|
monkeypatch.setattr(auth_cli, "_run_oauth_flow", _fake_flow)
|
|
monkeypatch.setattr(codex, "save_record", lambda _record: None)
|
|
|
|
assert auth_cli.run_auth(["login", provider]) == 0
|
|
assert reached["flow"] is True
|
|
|
|
|
|
@pytest.mark.parametrize("provider", ["opencode", "OpenCode", "opencode-go", "zen"])
|
|
def test_login_accepts_opencode_aliases(provider: str, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
reached = {"login": False}
|
|
|
|
def _fake_login(_console: Any) -> int:
|
|
reached["login"] = True
|
|
return 0
|
|
|
|
monkeypatch.setattr(auth_cli, "_login_opencode", _fake_login)
|
|
assert auth_cli.run_auth(["login", provider]) == 0
|
|
assert reached["login"] is True
|
|
|
|
|
|
def test_login_opencode_validates_and_saves(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
saved: dict[str, str] = {}
|
|
monkeypatch.setattr("rich.console.Console.input", lambda _self, *_a, **_k: " sk-oc-test ")
|
|
monkeypatch.setattr(opencode, "validate_api_key", lambda key: saved.setdefault("checked", key))
|
|
monkeypatch.setattr(opencode, "save_api_key", lambda key: saved.setdefault("key", key))
|
|
|
|
assert auth_cli.run_auth(["login", "opencode"]) == 0
|
|
assert saved == {"checked": "sk-oc-test", "key": "sk-oc-test"}
|
|
|
|
|
|
def test_login_opencode_rejects_bad_key(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
monkeypatch.setattr("rich.console.Console.input", lambda _self, *_a, **_k: "bad")
|
|
|
|
def _reject(_key: str) -> None:
|
|
raise opencode.OpencodeAuthError("invalid_key")
|
|
|
|
monkeypatch.setattr(opencode, "validate_api_key", _reject)
|
|
assert auth_cli.run_auth(["login", "opencode"]) == 1
|
|
assert opencode.is_authenticated() is False
|
|
|
|
|
|
def test_logout_provider_scoped() -> None:
|
|
codex.save_record(
|
|
{
|
|
"type": "oauth",
|
|
"provider": "codex",
|
|
"access": "a",
|
|
"refresh": "r",
|
|
"account_id": "acct",
|
|
"expires_at": time.time() + 3600,
|
|
}
|
|
)
|
|
opencode.save_api_key("sk-oc-test")
|
|
|
|
assert auth_cli.run_auth(["logout", "opencode"]) == 0
|
|
assert opencode.is_authenticated() is False
|
|
assert codex.is_authenticated() is True
|
|
|
|
assert auth_cli.run_auth(["logout"]) == 0
|
|
assert codex.is_authenticated() is False
|
|
|
|
assert auth_cli.run_auth(["logout", "bogus"]) == 2
|