mirror of
https://github.com/usestrix/strix.git
synced 2026-09-05 08:06:08 +00:00
* feat(cli): add strix login for managed platform sign-in (device flow) * feat(cli): add --scopes flag to strix login * docs: document strix login and managed billing in README, AGENTS, docs, and managed skill * fix(cli): handle malformed login responses and credential file failures * fix(cli): reject sign-in responses without an API token * feat(login): interactive workspace and scope selection with presets * fix(login): reject malformed API token values in sign-in responses * fix(login): skip the scope prompt when stdin is not a terminal * fix(login): tolerate malformed selection containers and remove unreadable credential files on logout * fix(login): treat overflowing timing values as invalid * fix(login): show the configured platform host in the sign-in banner * fix(login): bound device flow timing values and clean up unreplaced secret temp files * feat(cli): add the strix cloud command surface for the managed platform * feat(cli): manage workspaces and hosted onboarding links from strix cloud * fix(cli): report a leftover temporary secret file instead of hiding it * feat(cli): pass a Stripe payment method to the top-up wallet client * docs(cloud): recommend the Stripe agent wallet as the default payment path * fix(cloud): preserve API auth during MPP payment * fix(cloud): drop knowledge query and settings commands removed from the API * fix(cloud): align agent commands with API contracts * fix(cloud): send required PR review integration fields * fix(cloud): preserve scopes when switching workspaces * fix(cloud): make session command help non-destructive * feat(cloud): improve human navigation and output * feat(cli): add native shell completions * feat(cloud): tailor human list and detail views * feat(cloud): upload local source for managed scans * fix(cloud): infer scan type from local targets * Add agent-friendly managed cloud CLI * Harden cloud CLI type boundaries * Clarify cloud test user MFA options * Correct cloud vulnerability status guidance * Clarify chat file path handling * Allow signed storage upload URLs * Fix provider token request handling * Improve cloud CLI human list views * Make cloud CLI workflows actionable and safe * Make cloud workspace switching session-safe * Preserve CLI session metadata in JSON output * Remove preview protection bypass plumbing from cloud CLI
165 lines
5.4 KiB
Python
165 lines
5.4 KiB
Python
"""CLI-session lifecycle, scope, and workspace-race behavior."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import pytest
|
|
from rich.console import Console
|
|
|
|
from strix.interface import cloud, platform_cli, platform_identity
|
|
from strix.interface.cloud import http
|
|
from strix.interface.cloud import session as cloud_session
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
from pathlib import Path
|
|
|
|
|
|
class Response:
|
|
def __init__(self, payload: Any = None, status_code: int = 200) -> None:
|
|
self._payload = payload
|
|
self.status_code = status_code
|
|
self.ok = 200 <= status_code < 400
|
|
self.text = json.dumps(payload) if payload is not None else ""
|
|
self.headers = {"content-type": "application/json"}
|
|
|
|
def json(self) -> Any:
|
|
return self._payload
|
|
|
|
|
|
@pytest.fixture
|
|
def auth_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
|
|
path = tmp_path / "platform-auth.json"
|
|
monkeypatch.setattr(platform_cli, "AUTH_PATH", path)
|
|
monkeypatch.delenv("STRIX_API_TOKEN", raising=False)
|
|
monkeypatch.delenv("STRIX_WORKSPACE_ID", raising=False)
|
|
return path
|
|
|
|
|
|
def test_http_workspace_pin_is_captured_once(
|
|
auth_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
platform_cli.save_record(
|
|
{
|
|
"api_token": "secret",
|
|
"organization_id": "org_start",
|
|
"app_url": "https://app.example.test",
|
|
}
|
|
)
|
|
assert auth_path.exists()
|
|
sent: list[dict[str, str]] = []
|
|
|
|
def fake_request(*_args: Any, **kwargs: Any) -> Response:
|
|
sent.append(dict(kwargs["headers"]))
|
|
return Response({})
|
|
|
|
monkeypatch.setattr(http.requests, "request", fake_request)
|
|
http.configure()
|
|
platform_cli.save_record(
|
|
{
|
|
"api_token": "secret",
|
|
"organization_id": "org_changed_elsewhere",
|
|
"app_url": "https://app.example.test",
|
|
}
|
|
)
|
|
http.request("GET", "/scans")
|
|
assert sent[0]["X-Strix-Workspace"] == "org_start"
|
|
|
|
|
|
def test_session_scope_update_persists_only_for_stored_session(
|
|
auth_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: Any
|
|
) -> None:
|
|
platform_cli.save_record(
|
|
{
|
|
"api_token": "secret",
|
|
"organization_id": "org_1",
|
|
"app_url": "https://app.example.test",
|
|
"scopes": ["scans:read"],
|
|
}
|
|
)
|
|
monkeypatch.setattr(
|
|
http,
|
|
"request",
|
|
lambda *_args, **_kwargs: Response(
|
|
{
|
|
"scopes": ["scans:read", "scans:write", "billing:read"],
|
|
"requested_scopes": ["scans:read", "scans:write", "billing:read"],
|
|
"scope_ceiling": ["scans:read", "scans:write", "billing:read"],
|
|
"scope_profile": "minimal",
|
|
}
|
|
),
|
|
)
|
|
assert cloud.run_cloud(["session", "scopes", "set", "minimal", "--json"]) == 0
|
|
stored = platform_cli.read_record()
|
|
assert stored is not None
|
|
assert stored["scope_profile"] == "minimal"
|
|
assert json.loads(capsys.readouterr().out)["scope_profile"] == "minimal"
|
|
|
|
before = auth_path.read_text(encoding="utf-8")
|
|
assert (
|
|
cloud.run_cloud(["session", "scopes", "set", "minimal", "--token", "override", "--json"])
|
|
== 0
|
|
)
|
|
assert auth_path.read_text(encoding="utf-8") == before
|
|
|
|
|
|
def test_logout_keeps_local_token_when_remote_outcome_is_not_definitive(
|
|
auth_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: Any
|
|
) -> None:
|
|
platform_cli.save_record(
|
|
{
|
|
"api_token": "secret",
|
|
"organization_id": "org_1",
|
|
"app_url": "https://app.example.test",
|
|
}
|
|
)
|
|
monkeypatch.setattr(
|
|
platform_cli.requests,
|
|
"delete",
|
|
lambda *_args, **_kwargs: Response({"detail": "unavailable"}, 503),
|
|
)
|
|
assert cloud.run_cloud(["logout", "--json"]) == 1
|
|
assert auth_path.exists()
|
|
assert json.loads(capsys.readouterr().out)["removed"] is False
|
|
|
|
|
|
def test_local_only_logout_is_explicit_and_recoverable(auth_path: Path, capsys: Any) -> None:
|
|
platform_cli.save_record({"api_token": "secret"})
|
|
assert cloud.run_cloud(["logout", "--local-only", "--json"]) == 0
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload["local_only"] is True
|
|
assert payload["remotely_revoked"] is False
|
|
assert not auth_path.exists()
|
|
|
|
|
|
def test_session_json_errors_preserve_machine_readable_server_details(capsys: Any) -> None:
|
|
error = http.CloudError(
|
|
"workspace changed",
|
|
payload={
|
|
"detail": "workspace changed",
|
|
"code": "workspace_session_changed",
|
|
"current_organization_id": "org_current",
|
|
},
|
|
)
|
|
|
|
assert cloud_session._error(Console(), error, as_json=True) == http.EXIT_ERROR
|
|
payload = json.loads(capsys.readouterr().out)
|
|
assert payload == {
|
|
"code": "workspace_session_changed",
|
|
"current_organization_id": "org_current",
|
|
"error": "workspace changed",
|
|
}
|
|
|
|
|
|
def test_cli_device_identity_is_stable_and_privacy_safe(
|
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
) -> None:
|
|
path = tmp_path / "cli-identity.json"
|
|
monkeypatch.setattr(platform_identity, "IDENTITY_PATH", path)
|
|
first = platform_identity.read_or_create_identity()
|
|
second = platform_identity.read_or_create_identity(device_name=" Build laptop ")
|
|
assert second["client_instance_id"] == first["client_instance_id"]
|
|
assert second["device_name"] == "Build laptop"
|
|
assert path.stat().st_mode & 0o777 == 0o600
|