Release v1.0.56: Credential & Browser Profile Manager
All checks were successful
技能自动化发布 / release (push) Successful in 7s

This commit is contained in:
2026-05-05 18:51:00 +08:00
parent 6b6d2969c3
commit 91057fa3b7
29 changed files with 4582 additions and 900 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# account-manager tests

49
tests/conftest.py Normal file
View File

@@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
"""
Test fixtures for account-manager tests.
All tests use an isolated temporary database under a fake JIANGCHANG_DATA_ROOT
so they never touch real user data.
"""
import os
import sys
import tempfile
import shutil
# Ensure scripts/ is on the path
_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)
import pytest
@pytest.fixture(scope="session")
def fake_env():
"""Set up a fake JIANGCHANG_DATA_ROOT / JIANGCHANG_USER_ID for the session."""
tmp = tempfile.mkdtemp(prefix="account_manager_test_")
data_root = os.path.join(tmp, "claw-data")
os.makedirs(data_root)
fake_uid = "99999"
os.environ["JIANGCHANG_DATA_ROOT"] = data_root
os.environ["JIANGCHANG_USER_ID"] = fake_uid
yield {"data_root": data_root, "user_id": fake_uid, "tmp_dir": tmp}
@pytest.fixture(scope="function")
def clean_db(fake_env):
"""Each test gets a fresh in-memory DB (init_db uses the env vars set by fake_env)."""
# Import after env vars are set
from db.connection import get_conn, init_db
from util.constants import SKILL_SLUG
from util.logging_config import setup_skill_logging
setup_skill_logging(SKILL_SLUG, "openclaw.skill.account_manager_test")
init_db()
conn = get_conn()
conn.execute("DELETE FROM account_leases")
conn.execute("DELETE FROM credentials")
conn.execute("DELETE FROM accounts")
conn.commit()
yield conn
conn.close()

21
tests/run_tests.py Normal file
View File

@@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
"""
Test runner for account-manager.
Run:
pytest tests/ -v
python tests/run_tests.py -v
"""
import os
import sys
_script_dir = os.path.join(os.path.dirname(__file__), "..", "scripts")
if _script_dir not in sys.path:
sys.path.insert(0, _script_dir)
if __name__ == "__main__":
import pytest
tests_dir = os.path.dirname(os.path.abspath(__file__))
args = [tests_dir, "-v"] + sys.argv[1:]
sys.exit(pytest.main(args))

View File

@@ -0,0 +1,297 @@
# -*- coding: utf-8 -*-
"""
Tests for account CRUD operations and pick-web filtering.
"""
import json
import os
import pytest
class TestAccountAddWeb:
"""account add-web creates records correctly without requiring phone numbers."""
def test_add_web_email_login_id(self, clean_db):
"""Email login_id should be accepted (no 11-digit phone restriction)."""
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform_input="maersk",
login_id="demo@maersk.com",
login_id_type="email",
label="Maersk sim test",
tenant_id="tenant-demo",
environment="simulator",
role="booking",
provider_code=None,
url=None,
extra_json_str=None,
profile_dir_override=None,
)
# Should succeed with JSON output
from db.accounts_repo import find_accounts
accs = find_accounts(platform_key="maersk")
assert len(accs) == 1
assert accs[0]["login_id"] == "demo@maersk.com"
assert accs[0]["login_id_type"] == "email"
assert accs[0]["environment"] == "simulator"
assert accs[0]["role"] == "booking"
assert accs[0]["tenant_id"] == "tenant-demo"
assert accs[0]["status"] == "active"
assert accs[0]["platform_key"] == "maersk"
def test_add_web_no_login_id(self, clean_db):
"""login_id can be None (e.g. for browser_profile-only accounts)."""
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform_input="deepseek",
login_id=None,
login_id_type="unknown",
label="DeepSeek prod",
tenant_id=None,
environment="production",
role="api",
provider_code=None,
url=None,
extra_json_str=None,
profile_dir_override=None,
)
from db.accounts_repo import find_accounts
accs = find_accounts(platform_key="deepseek")
assert len(accs) == 1
assert accs[0]["login_id"] == ""
def test_add_web_profile_dir_created(self, clean_db, fake_env):
"""profile_dir should be auto-created under fake_env data root."""
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform_input="maersk",
login_id="test@example.com",
login_id_type="email",
label="Profile dir test",
tenant_id="tenant-test",
environment="production",
role="booking",
provider_code=None,
url=None,
extra_json_str=None,
profile_dir_override=None,
)
from db.accounts_repo import find_accounts
accs = find_accounts(platform_key="maersk")
assert len(accs) == 1
profile_dir = accs[0]["profile_dir"]
assert profile_dir, "profile_dir should be non-empty"
assert os.path.isdir(profile_dir), f"profile_dir should be created: {profile_dir}"
# The path should be under the fake data root
assert profile_dir.startswith(fake_env["data_root"]), \
f"profile_dir should be under fake data root: {profile_dir}"
def test_add_web_extra_json(self, clean_db):
"""extra_json field is stored and retrieved correctly."""
from service.account_service import cmd_account_add_web
extra = {"api_version": "v1", "timeout": 30}
cmd_account_add_web(
platform_input="cma_cgm",
login_id="cma@test.com",
login_id_type="email",
label="CMA CGM test",
tenant_id="tenant-cma",
environment="production",
role="documentation",
provider_code=None,
url=None,
extra_json_str=json.dumps(extra),
profile_dir_override=None,
)
from db.accounts_repo import find_accounts
accs = find_accounts(platform_key="cma_cgm")
assert len(accs) == 1
raw_ex = accs[0].get("extra_json")
extra_parsed = raw_ex if isinstance(raw_ex, dict) else json.loads(raw_ex or "{}")
assert extra_parsed["api_version"] == "v1"
assert extra_parsed["timeout"] == 30
class TestAccountGetList:
"""account get/list operations."""
def test_get_account_found(self, clean_db):
"""account get returns JSON with correct account fields."""
from service.account_service import cmd_account_add_web, cmd_account_get
import io
import sys
# Add account first
cmd_account_add_web(
platform_input="evergreen",
login_id="ship@evergreen.com",
login_id_type="email",
label="Evergreen booking",
tenant_id="tenant-evg",
environment="production",
role="booking",
provider_code=None,
url=None,
extra_json_str=None,
profile_dir_override=None,
)
accs = find_accounts_()
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_get(accs[0]["id"])
finally:
sys.stdout = sys.__stdout__
output = captured.getvalue()
data = json.loads(output)
assert data["platform_key"] == "evergreen"
assert data["login_id"] == "ship@evergreen.com"
def test_get_account_not_found(self, clean_db):
"""account get for unknown id returns ERROR:ACCOUNT_NOT_FOUND."""
from service.account_service import cmd_account_get
import io
import sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_get(9999)
finally:
sys.stdout = sys.__stdout__
output = captured.getvalue()
assert "ERROR:ACCOUNT_NOT_FOUND" in output
def test_list_platform_filter(self, clean_db):
"""account list --platform filters by platform."""
from service.account_service import cmd_account_add_web, cmd_account_list
import io
import sys
cmd_account_add_web("maersk", "a@maersk.com", "email", "m", "t1", "prod", "booking", None, None, None, None)
cmd_account_add_web("maersk", "b@maersk.com", "email", "m2", "t1", "prod", "documentation", None, None, None, None)
cmd_account_add_web("cma_cgm", "c@cma.com", "email", "c", "t2", "prod", "booking", None, None, None, None)
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_list(platform_input="maersk")
finally:
sys.stdout = sys.__stdout__
output = captured.getvalue()
accs = json.loads(output)
assert len(accs) == 2
assert all(a["platform_key"] == "maersk" for a in accs)
def find_accounts_():
from db.accounts_repo import find_accounts
return find_accounts(limit=200)
class TestPickWebFiltering:
"""pick-web filtering by environment / role / tenant_id."""
def _add(self, platform, login, label, tenant, env, role):
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform, login, "email", label, tenant, env, role,
None, None, None, None,
)
def test_filter_environment(self, clean_db):
"""pick-web --environment simulator selects only simulator account."""
self._add("maersk", "sim@maersk.com", "S", "t1", "simulator", "booking")
self._add("maersk", "prod@maersk.com", "P", "t1", "production", "booking")
self._add("maersk", "prod2@maersk.com", "P2", "t1", "production", "documentation")
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web("maersk", environment="simulator", tenant_id=None, role=None, do_lease=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["login_id"] == "sim@maersk.com"
assert result["environment"] == "simulator"
def test_filter_role(self, clean_db):
"""pick-web --role booking selects only booking role."""
self._add("maersk", "a@maersk.com", "A", "t1", "simulator", "booking")
self._add("maersk", "b@maersk.com", "B", "t1", "simulator", "documentation")
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web("maersk", environment=None, tenant_id=None, role="documentation", do_lease=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["role"] == "documentation"
def test_filter_tenant(self, clean_db):
"""pick-web --tenant-id filters correctly."""
self._add("cma_cgm", "a@cma.com", "A", "tenant-a", "production", "booking")
self._add("cma_cgm", "b@cma.com", "B", "tenant-b", "production", "booking")
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web("cma_cgm", environment=None, tenant_id="tenant-b", role=None, do_lease=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["tenant_id"] == "tenant-b"
def test_no_accounts_returns_error(self, clean_db):
"""pick-web when no account matches returns ERROR:NO_ACCOUNT."""
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web("maersk", environment="nonexistent", tenant_id=None, role=None, do_lease=False)
finally:
sys.stdout = sys.__stdout__
output = captured.getvalue()
err = json.loads(output)
assert err["success"] is False
assert err["error"]["code"] == "ERROR:NO_ACCOUNT"
def test_platform_alias_chinese(self, clean_db):
"""pick-web resolves Chinese platform alias correctly."""
self._add("maersk", "test@test.com", "Test", "t", "production", "booking")
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web("马士基", environment=None, tenant_id=None, role=None, do_lease=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["platform_key"] == "maersk"

View File

@@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
"""
Browser launch helper checks (no real browser started).
"""
import pytest
class TestPlaywrightLaunchHelpers:
"""Persistent context launch parts must match RPA contract."""
def test_persistent_context_includes_maximized(self):
from util.playwright_stealth import persistent_context_launch_parts
args, _ignore = persistent_context_launch_parts()
assert "--start-maximized" in args
def test_open_child_runner_launch_keywords_documented(self):
"""launch_persistent_context kwargs contract (static read of source)."""
import os
path = os.path.join(
os.path.dirname(__file__),
"..",
"scripts",
"service",
"open_child_runner.py",
)
path = os.path.abspath(path)
with open(path, encoding="utf-8") as f:
src = f.read()
assert "launch_persistent_context" in src
assert "headless=False" in src
assert "no_viewport=True" in src
assert '"channel"' in src or "'channel'" in src
class TestResolveChromiumChannel:
"""resolve_chromium_channel returns a channel string when browsers exist."""
def test_returns_string_or_none(self):
from service.browser_service import resolve_chromium_channel
ch = resolve_chromium_channel()
assert ch is None or ch in ("chrome", "msedge")

340
tests/test_credentials.py Normal file
View File

@@ -0,0 +1,340 @@
# -*- coding: utf-8 -*-
"""
Tests for credential operations, secret storage, and reveal.
Note: windows_credential integration tests are skipped by default
(the real Windows Credential Manager requires admin and a real user session).
Secret store logic for windows_credential is tested via mocking.
"""
import json
import os
import pytest
class TestCredentialEnvStorage:
"""credential add-secret env / pick --reveal / SQLite no-secret verification."""
def test_add_secret_env_saves_ref_not_value(self, clean_db):
"""add-secret with storage=env stores only the env var name, not the secret."""
from service.account_service import cmd_account_add_secret
from db.connection import get_conn
# Set the env var so the command can read it if needed
os.environ["TEST_DEEPSEEK_KEY"] = "sk-test-1234567890abcdef"
try:
cmd_account_add_secret(
platform_input="deepseek",
credential_type="api_key",
label="DeepSeek test key",
secret_storage="env",
secret_stdin=False,
secret_ref="TEST_DEEPSEEK_KEY",
account_id=None,
environment="production",
role="api",
extra_json_str=None,
)
finally:
del os.environ["TEST_DEEPSEEK_KEY"]
conn = get_conn()
cur = conn.cursor()
cur.execute(
"SELECT secret_ref, secret_mask, secret_storage FROM credentials WHERE platform_key = ?",
("deepseek",),
)
row = cur.fetchone()
assert row is not None, "credential should be inserted"
secret_ref, secret_mask, secret_storage = row
assert secret_storage == "env"
assert secret_ref == "TEST_DEEPSEEK_KEY", "secret_ref should be the env var name"
# secret_mask should be a masked form of "env:TEST_DEEPSEEK_KEY", NOT the actual secret
assert "sk-test" not in secret_mask, "secret_mask must not contain the actual secret value"
assert "****" in secret_mask, "mask should contain ****"
conn.close()
def test_credential_pick_reveal_reads_environ(self, clean_db):
"""credential pick --reveal retrieves secret value from os.environ."""
from service.account_service import cmd_account_add_secret, cmd_credential_pick
import io, sys
os.environ["MY_SECRET_KEY"] = "sk-actual-secret-value-abcdefgh"
try:
cmd_account_add_secret(
platform_input="deepseek",
credential_type="api_key",
label="Reveal test",
secret_storage="env",
secret_stdin=False,
secret_ref="MY_SECRET_KEY",
account_id=None,
environment=None,
role=None,
extra_json_str=None,
)
# credential pick --reveal
captured = io.StringIO()
sys.stdout = captured
try:
cmd_credential_pick("deepseek", credential_type="api_key", reveal=True)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is True
assert result["secret"] == "sk-actual-secret-value-abcdefgh"
assert result["secret_mask"] != result["secret"], "mask should differ from secret"
finally:
del os.environ["MY_SECRET_KEY"]
def test_credential_pick_no_reveal_masks(self, clean_db):
"""credential pick (no --reveal) does NOT include the secret field."""
from service.account_service import cmd_account_add_secret, cmd_credential_pick
import io, sys
os.environ["MASK_TEST_KEY"] = "super-secret-value-xyz"
try:
cmd_account_add_secret(
platform_input="deepseek",
credential_type="api_key",
label="Mask test",
secret_storage="env",
secret_stdin=False,
secret_ref="MASK_TEST_KEY",
account_id=None,
environment=None,
role=None,
extra_json_str=None,
)
captured = io.StringIO()
sys.stdout = captured
try:
cmd_credential_pick("deepseek", credential_type="api_key", reveal=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is True
assert "secret" not in result, "no secret field when reveal=False"
assert result["secret_mask"] != "super-secret-value-xyz"
finally:
del os.environ["MASK_TEST_KEY"]
def test_env_missing_returns_error(self, clean_db):
"""credential pick --reveal when env var is missing returns SECRET_ENV_MISSING."""
from service.account_service import cmd_account_add_secret, cmd_credential_pick
import io, sys
# Add credential with a ref to a non-existent env var
cmd_account_add_secret(
platform_input="deepseek",
credential_type="api_key",
label="Missing env test",
secret_storage="env",
secret_stdin=False,
secret_ref="THIS_ENV_VAR_DOES_NOT_EXIST_AT_ALL",
account_id=None,
environment=None,
role=None,
extra_json_str=None,
)
captured = io.StringIO()
sys.stdout = captured
try:
cmd_credential_pick("deepseek", credential_type="api_key", reveal=True)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is False
assert result["error"]["code"] == "ERROR:SECRET_ENV_MISSING"
class TestCredentialNoneStorage:
"""credential add-secret storage=none (browser_profile / note)."""
def test_add_secret_none(self, clean_db):
"""storage=none works for browser_profile credential type."""
from service.account_service import cmd_account_add_secret
cmd_account_add_secret(
platform_input="maersk",
credential_type="browser_profile",
label="Maersk browser session",
secret_storage="none",
secret_stdin=False,
secret_ref=None,
account_id=None,
environment="simulator",
role="booking",
extra_json_str=None,
)
from db.connection import get_conn
conn = get_conn()
cur = conn.cursor()
cur.execute(
"SELECT secret_storage, secret_ref, secret_mask FROM credentials WHERE platform_key = ?",
("maersk",),
)
row = cur.fetchone()
assert row is not None
assert row[0] == "none"
assert row[1] is None or row[1] == ""
conn.close()
class TestCredentialWindowsCredentialMocked:
"""Mocked tests for windows_credential secret store logic."""
def test_mask_secret_never_returns_full_value(self):
"""mask_secret must never return the actual secret value."""
from service.secret_store import mask_secret
secrets = [
"sk-test-1234567890abcdefghijklmnop",
"short",
"abc",
"a" * 100,
"",
None,
]
for s in secrets:
if s is None or s == "":
masked = mask_secret(s)
assert masked == "****"
continue
result = mask_secret(s)
# The mask should never equal the original
assert result != s, f"mask_secret({s!r}) must not equal original"
# The mask should always contain "****"
assert "****" in result, f"mask should contain '****': {result}"
# For long secrets, original should not appear in mask
assert s not in result, f"original secret must not appear in mask: {s}"
def test_read_secret_env_success(self):
"""read_secret with storage=env returns the os.environ value."""
os.environ["TEST_ENV_FOR_READ"] = "env-secret-value-xyz"
try:
from service.secret_store import read_secret
val = read_secret("env", "TEST_ENV_FOR_READ")
assert val == "env-secret-value-xyz"
finally:
del os.environ["TEST_ENV_FOR_READ"]
def test_read_secret_env_missing_raises(self):
"""read_secret with storage=env raises ValueError when env var is missing."""
# Ensure the env var is not set
os.environ.pop("NONEXISTENT_VAR_FOR_TEST_123", None)
from service.secret_store import read_secret
with pytest.raises(ValueError) as exc_info:
read_secret("env", "NONEXISTENT_VAR_FOR_TEST_123")
assert "SECRET_ENV_MISSING" in str(exc_info.value)
def test_read_secret_none(self):
"""read_secret with storage=none returns empty string."""
from service.secret_store import read_secret
val = read_secret("none", "ignored-ref")
assert val == ""
def test_write_secret_none_is_noop(self):
"""write_secret with storage=none does not raise."""
from service.secret_store import write_secret
# Should not raise
write_secret("none", "ignored-ref", "any-secret-value")
def test_windows_credential_write_fails_on_non_win32(self, monkeypatch):
"""On non-Windows, windows_credential write raises OSError."""
import sys
monkeypatch.setattr(sys, "platform", "linux")
from service.secret_store import write_secret
with pytest.raises(OSError) as exc_info:
write_secret("windows_credential", "some-target", "some-secret")
assert "WINDOWS_CREDENTIAL_UNAVAILABLE" in str(exc_info.value) or \
"not available" in str(exc_info.value).lower()
class TestCredentialPickFiltering:
"""credential pick filtering by credential_type / environment."""
def test_pick_by_credential_type(self, clean_db):
from service.account_service import cmd_account_add_secret, cmd_credential_pick
import io, sys
os.environ["CREDS_TYPE_KEY_A"] = "val-a"
os.environ["CREDS_TYPE_KEY_B"] = "val-b"
try:
cmd_account_add_secret(
"deepseek", "api_key", "A", "env", False, "CREDS_TYPE_KEY_A",
None, None, None, None,
)
cmd_account_add_secret(
"deepseek", "model_param", "B", "env", False, "CREDS_TYPE_KEY_B",
None, None, None, None,
)
captured = io.StringIO()
sys.stdout = captured
try:
cmd_credential_pick("deepseek", credential_type="model_param", reveal=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is True
assert result["credential_type"] == "model_param"
finally:
del os.environ["CREDS_TYPE_KEY_A"]
del os.environ["CREDS_TYPE_KEY_B"]
def test_pick_no_credential_returns_error(self, clean_db):
"""credential pick when none matches returns CREDENTIAL_NOT_FOUND."""
from service.account_service import cmd_credential_pick
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_credential_pick("deepseek", credential_type="nonexistent_type", reveal=False)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is False
assert result["error"]["code"] == "ERROR:CREDENTIAL_NOT_FOUND"
class TestSQLiteNoPlaintextSecret:
"""Verify that SQLite never stores actual secret values."""
def test_credentials_table_never_has_plaintext(self, clean_db):
"""All credential rows should have secret_mask != original secret value."""
from service.account_service import cmd_account_add_secret
from db.connection import get_conn
test_secret = "SQLITE_NO_PLAINTEXT_TEST_SECRET_987654321"
os.environ["SQLITE_NO_PLAINTEXT_TEST"] = test_secret
try:
cmd_account_add_secret(
"deepseek", "api_key", "SQLite test", "env", False,
"SQLITE_NO_PLAINTEXT_TEST", None, None, None, None,
)
finally:
del os.environ["SQLITE_NO_PLAINTEXT_TEST"]
conn = get_conn()
cur = conn.cursor()
cur.execute("SELECT secret_ref, secret_mask FROM credentials")
rows = cur.fetchall()
conn.close()
for secret_ref, secret_mask in rows:
# secret_ref for env storage is the env var name, not the secret
if secret_ref == "SQLITE_NO_PLAINTEXT_TEST":
# If it matches our test env var name, check the mask
assert test_secret not in (secret_mask or ""), \
f"Plaintext secret must not appear in secret_mask or secret_ref columns. mask={secret_mask}"

242
tests/test_lease.py Normal file
View File

@@ -0,0 +1,242 @@
# -*- coding: utf-8 -*-
"""
Tests for lease acquire / conflict / release / cleanup.
"""
import json
import time
import pytest
class TestLeaseAcquire:
"""Lease acquisition on pick-web."""
def _add_account(self, platform, login, label, tenant, env, role):
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform, login, "email", label, tenant, env, role,
None, None, None, None,
)
def test_pick_web_lease_returns_token(self, clean_db):
"""pick-web --lease returns lease_token in result."""
self._add_account("maersk", "test@maersk.com", "Test", "t1", "production", "booking")
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web(
"maersk",
environment=None,
tenant_id=None,
role=None,
do_lease=True,
ttl_sec=900,
holder="test-holder",
purpose="rpa-run",
)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert "lease_token" in result
assert len(result["lease_token"]) == 32
assert "lease_expires_at" in result
assert result["lease_token"] not in ("", "none")
def test_active_lease_blocks_pick(self, clean_db):
"""When the only matching account is leased, pick-web finds no eligible row."""
from db.accounts_repo import acquire_lease, find_account_ids
self._add_account("cma_cgm", "test@cma.com", "CMA", "t1", "production", "booking")
ids = find_account_ids("cma_cgm")
assert len(ids) == 1
acc_id = ids[0]
acquire_lease(
account_id=acc_id,
lease_token="a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6",
holder="first-holder",
purpose="first-task",
ttl_sec=900,
)
assert find_account_ids("cma_cgm") == []
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web(
"cma_cgm",
environment=None,
tenant_id=None,
role=None,
do_lease=True,
ttl_sec=900,
holder="second-holder",
purpose="second-task",
)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is False
assert result["error"]["code"] == "ERROR:NO_ACCOUNT"
def test_release_then_pick(self, clean_db):
"""After release, the same account can be picked again."""
from db.accounts_repo import acquire_lease, release_lease, find_account_ids
self._add_account("evergreen", "ship@evergreen.com", "EVG", "t1", "production", "booking")
ids = find_account_ids("evergreen")
acc_id = ids[0]
token = "z9y8x7w6v5u4t3s2r1q0p9o8n7m6l5k4"
acquire_lease(acc_id, token, "holder", "purpose", 900)
ok = release_lease(token)
assert ok, "release should succeed"
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web(
"evergreen",
environment=None,
tenant_id=None,
role=None,
do_lease=True,
ttl_sec=900,
holder="new-holder",
purpose="new-task",
)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue().strip().splitlines()[-1])
assert result["id"] == acc_id
assert "lease_token" in result
class TestLeaseRelease:
"""lease release command."""
def test_release_unknown_token(self, clean_db):
"""release of non-existent token returns LEASE_NOT_FOUND."""
from service.account_service import cmd_lease_release
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_lease_release("nonexistent_token_123456789012")
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is False
assert result["error"]["code"] == "ERROR:LEASE_NOT_FOUND"
class TestLeaseCleanup:
"""lease cleanup / expired lease handling."""
def test_expired_lease_allows_pick(self, clean_db):
"""After cleanup marks expired, account can be picked again."""
from db.accounts_repo import acquire_lease, cleanup_expired_leases, find_account_ids
self._add_account_("cargo_wise", "cw@cargowise.com", "CW", "t1", "production", "booking")
ids = find_account_ids("cargo_wise")
acc_id = ids[0]
# Acquire lease with very short TTL (1 second)
acquire_lease(acc_id, "shortlived1234567890123456789012", "holder", "short", 1)
# Wait for it to expire
time.sleep(1.5)
count = cleanup_expired_leases()
assert count >= 1, "at least one lease should be cleaned"
# Now pick-web should succeed
from service.account_service import cmd_account_pick_web
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_account_pick_web(
"cargo_wise",
environment=None,
tenant_id=None,
role=None,
do_lease=True,
ttl_sec=900,
holder="after-cleanup",
purpose="fresh-task",
)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue().strip().splitlines()[-1])
assert result["id"] == acc_id
assert "lease_token" in result
def _add_account_(self, platform, login, label, tenant, env, role):
from service.account_service import cmd_account_add_web
cmd_account_add_web(
platform, login, "email", label, tenant, env, role,
None, None, None, None,
)
class TestLeaseList:
"""lease list command."""
def test_list_no_leases(self, clean_db):
"""lease list --active returns empty when no leases."""
from service.account_service import cmd_lease_list
import io, sys
captured = io.StringIO()
sys.stdout = captured
try:
cmd_lease_list(active_only=True)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is True
assert result["data"]["leases"] == []
def test_list_with_active_lease(self, clean_db):
"""lease list --active shows active lease after acquisition."""
from db.accounts_repo import acquire_lease, find_account_ids
from service.account_service import cmd_account_add_web, cmd_lease_list
import io, sys
cmd_account_add_web(
"sinotrans", "sinotest@sinotrans.com", "email", "Sino", "t1", "production", "booking",
None, None, None, None,
)
ids = find_account_ids("sinotrans")
acquire_lease(ids[0], "listtest1234567890123456789012345", "holder", "purpose", 900)
captured = io.StringIO()
sys.stdout = captured
try:
cmd_lease_list(active_only=True)
finally:
sys.stdout = sys.__stdout__
result = json.loads(captured.getvalue())
assert result["success"] is True
leases = result["data"]["leases"]
assert len(leases) == 1
assert leases[0]["account_id"] == ids[0]
assert leases[0]["status"] == "active"

83
tests/test_platforms.py Normal file
View File

@@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
"""
Platform resolution tests.
"""
import pytest
class TestPlatformResolution:
"""Platform key resolution: key / display_name / aliases -> canonical key."""
def test_maersk_key(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("maersk") == "maersk"
def test_maersk_alias_chinese(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("马士基") == "maersk"
def test_maersk_alias_upper(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("MAERSK") == "maersk"
def test_cma_cgm_key(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("cma_cgm") == "cma_cgm"
def test_cma_cgm_alias_chinese(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("达飞") == "cma_cgm"
def test_cma_cgm_alias_display(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("CMA CGM") == "cma_cgm"
def test_deepseek_key(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("deepseek") == "deepseek"
def test_deepseek_alias_capitalize(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("DeepSeek") == "deepseek"
def test_unknown_returns_none(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("not_a_platform_xyz") is None
def test_empty_returns_none(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("") is None
assert resolve_platform_key(None) is None
def test_list_platforms_all(self):
from util.platforms import list_platforms
all_plat = list_platforms()
assert any(p["platform_key"] == "maersk" for p in all_plat)
assert any(p["platform_key"] == "deepseek" for p in all_plat)
assert any(p["platform_key"] == "sohu" for p in all_plat)
def test_list_platforms_logistics_only(self):
from util.platforms import list_platforms
log_plat = list_platforms("logistics")
assert any(p["platform_key"] == "maersk" for p in log_plat)
assert any(p["platform_key"] == "cma_cgm" for p in log_plat)
assert not any(p["platform_key"] == "deepseek" for p in log_plat)
def test_list_platforms_llm_only(self):
from util.platforms import list_platforms
llm_plat = list_platforms("llm")
assert any(p["platform_key"] == "deepseek" for p in llm_plat)
assert any(p["platform_key"] == "doubao" for p in llm_plat)
assert not any(p["platform_key"] == "maersk" for p in llm_plat)
def test_evergreen_key(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("evergreen") == "evergreen"
def test_evergreen_alias_chinese(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("长荣") == "evergreen"
def test_cargo_wise_key(self):
from util.platforms import resolve_platform_key
assert resolve_platform_key("cargo_wise") == "cargo_wise"