test(cli): add stage C CLI tests
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
511
tests/test_cli_stage_c.py
Normal file
511
tests/test_cli_stage_c.py
Normal file
@@ -0,0 +1,511 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Stage C: CLI — add-web v2, local_encrypted add-secret, pick-web fields, get-credential."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
|
||||
import pytest
|
||||
|
||||
_scripts = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "scripts")
|
||||
if _scripts not in sys.path:
|
||||
sys.path.insert(0, _scripts)
|
||||
|
||||
MAIN_PY = os.path.join(_scripts, "main.py")
|
||||
|
||||
from util.constants import SKILL_SLUG # noqa: E402
|
||||
|
||||
|
||||
def _cli_env(roots: dict, **extra: str) -> dict:
|
||||
e = {**os.environ, **extra}
|
||||
e["JIANGCHANG_DATA_ROOT"] = roots["data_root"]
|
||||
e["JIANGCHANG_USER_ID"] = roots["user_id"]
|
||||
e["CLAW_DATA_ROOT"] = roots["data_root"]
|
||||
e["CLAW_USER_ID"] = roots["user_id"]
|
||||
return e
|
||||
|
||||
|
||||
def _db_path(roots: dict) -> str:
|
||||
return os.path.join(roots["data_root"], roots["user_id"], SKILL_SLUG, "account-manager.db")
|
||||
|
||||
|
||||
@dataclass
|
||||
class _ProcOut:
|
||||
returncode: int
|
||||
stdout: str
|
||||
stderr: str
|
||||
|
||||
|
||||
def _run_cli(env: dict, args: list[str], stdin: str | None = None) -> _ProcOut:
|
||||
inp = stdin.encode("utf-8") if stdin is not None else None
|
||||
r = subprocess.run(
|
||||
[sys.executable, MAIN_PY, *args],
|
||||
input=inp,
|
||||
capture_output=True,
|
||||
env=env,
|
||||
cwd=os.path.dirname(_scripts),
|
||||
)
|
||||
stdout_txt = r.stdout.decode("utf-8") if r.stdout else ""
|
||||
stderr_txt = r.stderr.decode("utf-8", errors="replace") if r.stderr else ""
|
||||
return _ProcOut(returncode=r.returncode, stdout=stdout_txt, stderr=stderr_txt)
|
||||
|
||||
|
||||
def _parse_ok_data(line: str) -> dict:
|
||||
j = json.loads(line.strip().splitlines()[0])
|
||||
assert j.get("success") is True
|
||||
return j["data"]
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def iso_cli_env():
|
||||
"""Function-scoped data root for subprocess CLI tests."""
|
||||
tmp = tempfile.mkdtemp(prefix="cli_stage_c_")
|
||||
data_root = os.path.join(tmp, "claw-data")
|
||||
os.makedirs(data_root)
|
||||
uid = "77701"
|
||||
yield {"data_root": data_root, "user_id": uid, "tmp_dir": tmp}
|
||||
shutil.rmtree(tmp, ignore_errors=True)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def with_master_key(monkeypatch):
|
||||
from cryptography.fernet import Fernet
|
||||
|
||||
key = Fernet.generate_key().decode("ascii")
|
||||
monkeypatch.setenv("ACCOUNT_MANAGER_MASTER_KEY", key)
|
||||
yield key
|
||||
|
||||
|
||||
class TestAddWebV2:
|
||||
def test_add_web_explicit_auth_strategy(self, iso_cli_env):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-web",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--label",
|
||||
"demo",
|
||||
"--auth-strategy",
|
||||
"qr_code_manual",
|
||||
"--session-persistent",
|
||||
"1",
|
||||
],
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
data = _parse_ok_data(proc.stdout)
|
||||
assert data["auth_strategy"] == "qr_code_manual"
|
||||
assert data["session_persistent"] == 1
|
||||
conn = sqlite3.connect(_db_path(iso_cli_env))
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT auth_strategy, session_persistent FROM accounts WHERE id = ?",
|
||||
(data["id"],),
|
||||
).fetchone()
|
||||
assert row == ("qr_code_manual", 1)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_add_web_platform_default_maersk(self, iso_cli_env):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "demo2"],
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
data = _parse_ok_data(proc.stdout)
|
||||
assert data["auth_strategy"] == "qr_code_manual"
|
||||
|
||||
def test_add_web_bad_auth_strategy(self, iso_cli_env):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-web",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--label",
|
||||
"x",
|
||||
"--auth-strategy",
|
||||
"foo",
|
||||
],
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
j = json.loads(proc.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_AUTH_STRATEGY_NOT_SUPPORTED"
|
||||
conn = sqlite3.connect(_db_path(iso_cli_env))
|
||||
try:
|
||||
n = conn.execute("SELECT COUNT(*) FROM accounts").fetchone()[0]
|
||||
assert n == 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_add_web_device_fingerprint_wrong_strategy(self, iso_cli_env):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-web",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--label",
|
||||
"x",
|
||||
"--auth-strategy",
|
||||
"password_auto",
|
||||
"--device-fingerprint",
|
||||
"fp1",
|
||||
],
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
j = json.loads(proc.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_DEVICE_FINGERPRINT_NOT_APPLICABLE"
|
||||
|
||||
|
||||
class TestAddSecretLocalEncrypted:
|
||||
def test_add_secret_local_encrypted_success(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--label",
|
||||
"pw1",
|
||||
],
|
||||
stdin="my-pw\n",
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
data = _parse_ok_data(proc.stdout)
|
||||
assert data["secret_storage"] == "local_encrypted"
|
||||
conn = sqlite3.connect(_db_path(iso_cli_env))
|
||||
try:
|
||||
row = conn.execute(
|
||||
"SELECT secret_storage, secret_mask, length(secret_ciphertext) FROM credentials WHERE id = ?",
|
||||
(data["credential_id"],),
|
||||
).fetchone()
|
||||
assert row is not None
|
||||
assert row[0] == "local_encrypted"
|
||||
assert row[1] # mask non-empty
|
||||
assert row[2] and row[2] > 0
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_add_secret_local_encrypted_requires_stdin(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--label",
|
||||
"x",
|
||||
],
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
j = json.loads(proc.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_LOCAL_ENCRYPTED_REQUIRES_STDIN"
|
||||
|
||||
def test_add_secret_local_encrypted_type_not_allowed(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"browser_profile",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
],
|
||||
stdin="x\n",
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
j = json.loads(proc.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_LOCAL_ENCRYPTED_TYPE_NOT_ALLOWED"
|
||||
|
||||
def test_add_secret_local_encrypted_master_missing(self, iso_cli_env, monkeypatch):
|
||||
monkeypatch.delenv("ACCOUNT_MANAGER_MASTER_KEY", raising=False)
|
||||
env = _cli_env(iso_cli_env)
|
||||
env.pop("ACCOUNT_MANAGER_MASTER_KEY", None)
|
||||
proc = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
],
|
||||
stdin="pw\n",
|
||||
)
|
||||
assert proc.returncode == 0
|
||||
j = json.loads(proc.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_MASTER_KEY_MISSING"
|
||||
|
||||
|
||||
class TestPickWebReturns:
|
||||
def test_pick_web_has_v2_fields(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
aw = _run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "pw"],
|
||||
)
|
||||
assert aw.returncode == 0
|
||||
aid = _parse_ok_data(aw.stdout)["id"]
|
||||
sec = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--account-id",
|
||||
str(aid),
|
||||
],
|
||||
stdin="secret-line\n",
|
||||
)
|
||||
assert sec.returncode == 0
|
||||
|
||||
pick = _run_cli(env, ["account", "pick-web", "--platform", "maersk"])
|
||||
assert pick.returncode == 0
|
||||
payload = json.loads(pick.stdout.strip().splitlines()[0])
|
||||
assert "auth_strategy" in payload
|
||||
assert "session_persistent" in payload
|
||||
assert "device_fingerprint" in payload
|
||||
|
||||
def test_pick_web_legacy_keys_present(self, iso_cli_env):
|
||||
env = _cli_env(iso_cli_env)
|
||||
aw = _run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "legacy"],
|
||||
)
|
||||
assert aw.returncode == 0
|
||||
pick = _run_cli(env, ["account", "pick-web", "--platform", "maersk"])
|
||||
assert pick.returncode == 0
|
||||
payload = json.loads(pick.stdout.strip().splitlines()[0])
|
||||
for k in ("id", "platform_key", "login_id", "profile_dir", "url"):
|
||||
assert k in payload
|
||||
|
||||
|
||||
class TestGetCredential:
|
||||
def test_get_credential_no_reveal_no_plaintext(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
aw = _run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "g1"],
|
||||
)
|
||||
aid = _parse_ok_data(aw.stdout)["id"]
|
||||
sec = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--account-id",
|
||||
str(aid),
|
||||
],
|
||||
stdin="hidden-pw\n",
|
||||
)
|
||||
assert sec.returncode == 0
|
||||
|
||||
g = _run_cli(env, ["account", "get-credential", str(aid)])
|
||||
assert g.returncode == 0
|
||||
j = json.loads(g.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is True
|
||||
assert "plaintext" not in j
|
||||
|
||||
def test_get_credential_reveal_missing_lease_token(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
aw = _run_cli(env, ["account", "add-web", "--platform", "maersk", "--label", "g2"])
|
||||
aid = _parse_ok_data(aw.stdout)["id"]
|
||||
sec = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--account-id",
|
||||
str(aid),
|
||||
],
|
||||
stdin="p\n",
|
||||
)
|
||||
assert sec.returncode == 0
|
||||
|
||||
g = _run_cli(env, ["account", "get-credential", str(aid), "--reveal"])
|
||||
j = json.loads(g.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_LEASE_INVALID"
|
||||
|
||||
def test_get_credential_wrong_lease_account(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
a1 = _parse_ok_data(
|
||||
_run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "acc1"],
|
||||
).stdout
|
||||
)["id"]
|
||||
sec1 = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--account-id",
|
||||
str(a1),
|
||||
],
|
||||
stdin="s1\n",
|
||||
)
|
||||
assert sec1.returncode == 0
|
||||
|
||||
lease_out = _run_cli(
|
||||
env,
|
||||
["account", "pick-web", "--platform", "maersk", "--lease", "--holder", "t"],
|
||||
)
|
||||
tok = json.loads(lease_out.stdout.strip().splitlines()[0])["lease_token"]
|
||||
|
||||
a2 = _parse_ok_data(
|
||||
_run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "acc2"],
|
||||
).stdout
|
||||
)["id"]
|
||||
sec2 = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--account-id",
|
||||
str(a2),
|
||||
],
|
||||
stdin="s2\n",
|
||||
)
|
||||
assert sec2.returncode == 0
|
||||
|
||||
bad = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"get-credential",
|
||||
str(a2),
|
||||
"--reveal",
|
||||
"--lease-token",
|
||||
tok,
|
||||
],
|
||||
)
|
||||
j = json.loads(bad.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is False
|
||||
assert j["error"]["code"] == "ERR_LEASE_INVALID"
|
||||
|
||||
def test_get_credential_reveal_success(self, iso_cli_env, with_master_key):
|
||||
env = _cli_env(iso_cli_env)
|
||||
a1 = _parse_ok_data(
|
||||
_run_cli(
|
||||
env,
|
||||
["account", "add-web", "--platform", "maersk", "--label", "okc"],
|
||||
).stdout
|
||||
)["id"]
|
||||
secret_text = "correct-horse-battery"
|
||||
sec = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"add-secret",
|
||||
"--platform",
|
||||
"maersk",
|
||||
"--credential-type",
|
||||
"password",
|
||||
"--secret-storage",
|
||||
"local_encrypted",
|
||||
"--secret-stdin",
|
||||
"--account-id",
|
||||
str(a1),
|
||||
],
|
||||
stdin=secret_text + "\n",
|
||||
)
|
||||
assert sec.returncode == 0
|
||||
|
||||
lease_out = _run_cli(
|
||||
env,
|
||||
["account", "pick-web", "--platform", "maersk", "--lease", "--holder", "h"],
|
||||
)
|
||||
tok = json.loads(lease_out.stdout.strip().splitlines()[0])["lease_token"]
|
||||
|
||||
g = _run_cli(
|
||||
env,
|
||||
[
|
||||
"account",
|
||||
"get-credential",
|
||||
str(a1),
|
||||
"--reveal",
|
||||
"--lease-token",
|
||||
tok,
|
||||
],
|
||||
)
|
||||
j = json.loads(g.stdout.strip().splitlines()[0])
|
||||
assert j["success"] is True
|
||||
assert j["plaintext"] == secret_text
|
||||
Reference in New Issue
Block a user