Read the Portal's tool_access claim (JWT + /api/oauth/account) into NousToolAccessInfo and gate managed Tool Gateway access on it: tool_gateway_entitled (paid OR live pool) and per-category tool_gateway_entitled_for(). The pool funds web/image/tts/browser but not video, so per-backend availability, the charge picker (ensure_nous_portal_access coverage_category), and managed defaults all respect coverage. Setup: rebuild prompt_enable_tool_gateway as a per-tool checklist that renders whenever the pool is enabled, lists only pool-covered tools (video excluded for free-pool users), and is framed as the free tool pool for $0 subscribers rather than a paid subscription. get_gateway_eligible_tools now gates and filters off the entitlement snapshot.
526 lines
20 KiB
Python
526 lines
20 KiB
Python
"""Tests for Nous subscription feature detection."""
|
|
|
|
from hermes_cli.nous_account import NousPortalAccountInfo, NousToolAccessInfo
|
|
from hermes_cli import nous_subscription as ns
|
|
|
|
|
|
_POOL_COVERAGE = {
|
|
"firecrawl": True,
|
|
"fal": True,
|
|
"fal-video": False,
|
|
"openai-audio": True,
|
|
"browser-use": True,
|
|
"modal": True,
|
|
}
|
|
|
|
|
|
def _account(*, logged_in: bool, paid: bool | None = None) -> NousPortalAccountInfo:
|
|
return NousPortalAccountInfo(
|
|
logged_in=logged_in,
|
|
source="jwt" if logged_in else "none",
|
|
fresh=False,
|
|
paid_service_access=paid,
|
|
)
|
|
|
|
|
|
def _pool_account() -> NousPortalAccountInfo:
|
|
"""A $0 subscriber with a live free tool pool (no paid access)."""
|
|
return NousPortalAccountInfo(
|
|
logged_in=True,
|
|
source="jwt",
|
|
fresh=False,
|
|
paid_service_access=False,
|
|
tool_access=NousToolAccessInfo(enabled=True, coverage=_POOL_COVERAGE),
|
|
)
|
|
|
|
|
|
def test_get_nous_subscription_features_recognizes_direct_exa_backend(monkeypatch):
|
|
env = {"EXA_API_KEY": "exa-test"}
|
|
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=False)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "web")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
|
|
features = ns.get_nous_subscription_features({"web": {"backend": "exa"}})
|
|
|
|
assert features.web.available is True
|
|
assert features.web.active is True
|
|
assert features.web.managed_by_nous is False
|
|
assert features.web.direct_override is True
|
|
assert features.web.current_provider == "exa"
|
|
|
|
|
|
def test_get_nous_subscription_features_force_fresh_forwards_account_request(monkeypatch):
|
|
calls = []
|
|
|
|
def fake_account_info(*, force_fresh=False):
|
|
calls.append(force_fresh)
|
|
return _account(logged_in=True, paid=True)
|
|
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: "")
|
|
monkeypatch.setattr(ns, "get_nous_portal_account_info", fake_account_info)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: False)
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
monkeypatch.setattr(ns, "is_managed_tool_gateway_ready", lambda vendor: False)
|
|
|
|
features = ns.get_nous_subscription_features({}, force_fresh=True)
|
|
|
|
assert features.account_info is not None
|
|
assert features.account_info.paid_service_access is True
|
|
assert calls == [True]
|
|
|
|
|
|
def test_get_nous_subscription_features_prefers_managed_modal_in_auto_mode(monkeypatch):
|
|
monkeypatch.setattr("tools.tool_backend_helpers.managed_nous_tools_enabled", lambda: True)
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: "")
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "terminal")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: True)
|
|
monkeypatch.setattr(ns, "is_managed_tool_gateway_ready", lambda vendor: vendor == "modal")
|
|
|
|
features = ns.get_nous_subscription_features(
|
|
{"terminal": {"backend": "modal", "modal_mode": "auto"}}
|
|
)
|
|
|
|
assert features.modal.available is True
|
|
assert features.modal.active is True
|
|
assert features.modal.managed_by_nous is True
|
|
assert features.modal.direct_override is False
|
|
|
|
|
|
def test_get_nous_subscription_features_marks_browser_use_as_managed_when_gateway_ready(monkeypatch):
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: "")
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: True)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"is_managed_tool_gateway_ready",
|
|
lambda vendor: vendor == "browser-use",
|
|
)
|
|
|
|
features = ns.get_nous_subscription_features(
|
|
{"browser": {"cloud_provider": "browser-use"}}
|
|
)
|
|
|
|
assert features.browser.available is True
|
|
assert features.browser.active is True
|
|
assert features.browser.managed_by_nous is True
|
|
assert features.browser.direct_override is False
|
|
assert features.browser.current_provider == "Browser Use"
|
|
|
|
|
|
def test_get_nous_subscription_features_uses_direct_browserbase_when_no_managed_gateway(monkeypatch):
|
|
"""When direct Browserbase keys are set and no managed gateway is available,
|
|
the unconfigured fallback should pick Browserbase as a direct provider."""
|
|
env = {
|
|
"BROWSERBASE_API_KEY": "bb-key",
|
|
"BROWSERBASE_PROJECT_ID": "bb-project",
|
|
}
|
|
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: True)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"is_managed_tool_gateway_ready",
|
|
lambda vendor: False, # No managed gateway available
|
|
)
|
|
|
|
features = ns.get_nous_subscription_features({})
|
|
|
|
assert features.browser.available is True
|
|
assert features.browser.active is True
|
|
assert features.browser.managed_by_nous is False
|
|
assert features.browser.direct_override is True
|
|
assert features.browser.current_provider == "Browserbase"
|
|
|
|
|
|
def test_get_nous_subscription_features_prefers_camofox_over_managed_browser_use(monkeypatch):
|
|
env = {"CAMOFOX_URL": "http://localhost:9377"}
|
|
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"is_managed_tool_gateway_ready",
|
|
lambda vendor: vendor == "browser-use",
|
|
)
|
|
|
|
features = ns.get_nous_subscription_features(
|
|
{"browser": {"cloud_provider": "browser-use"}}
|
|
)
|
|
|
|
assert features.browser.available is True
|
|
assert features.browser.active is True
|
|
assert features.browser.managed_by_nous is False
|
|
assert features.browser.direct_override is True
|
|
assert features.browser.current_provider == "Camofox"
|
|
|
|
|
|
def test_get_nous_subscription_features_requires_agent_browser_for_browserbase(monkeypatch):
|
|
env = {
|
|
"BROWSERBASE_API_KEY": "bb-key",
|
|
"BROWSERBASE_PROJECT_ID": "bb-project",
|
|
}
|
|
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=False)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "browser")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
monkeypatch.setattr(ns, "is_managed_tool_gateway_ready", lambda vendor: False)
|
|
|
|
features = ns.get_nous_subscription_features(
|
|
{"browser": {"cloud_provider": "browserbase"}}
|
|
)
|
|
|
|
assert features.browser.available is False
|
|
assert features.browser.active is False
|
|
assert features.browser.managed_by_nous is False
|
|
assert features.browser.current_provider == "Browserbase"
|
|
|
|
|
|
def test_get_nous_subscription_features_does_not_treat_quoted_false_as_gateway_opt_in(monkeypatch):
|
|
env = {"EXA_API_KEY": "exa-test"}
|
|
|
|
monkeypatch.setattr(ns, "get_env_value", lambda name: env.get(name, ""))
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(ns, "_toolset_enabled", lambda config, key: key == "web")
|
|
monkeypatch.setattr(ns, "_has_agent_browser", lambda: False)
|
|
monkeypatch.setattr(ns, "resolve_openai_audio_api_key", lambda: "")
|
|
monkeypatch.setattr(ns, "has_direct_modal_credentials", lambda: False)
|
|
monkeypatch.setattr(ns, "is_managed_tool_gateway_ready", lambda vendor: vendor == "firecrawl")
|
|
|
|
features = ns.get_nous_subscription_features(
|
|
{"web": {"backend": "exa", "use_gateway": "false"}}
|
|
)
|
|
|
|
assert features.web.available is True
|
|
assert features.web.active is True
|
|
assert features.web.managed_by_nous is False
|
|
assert features.web.direct_override is True
|
|
assert features.web.current_provider == "exa"
|
|
|
|
|
|
def test_get_gateway_eligible_tools_ignores_quoted_false_opt_in(monkeypatch):
|
|
# Paid account: entitled to every category, including video.
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda **kw: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"_get_gateway_direct_credentials",
|
|
lambda: {"web": True, "image_gen": False, "video_gen": False, "tts": False, "browser": False},
|
|
)
|
|
|
|
unconfigured, has_direct, already_managed = ns.get_gateway_eligible_tools(
|
|
{
|
|
"model": {"provider": "nous"},
|
|
"web": {"use_gateway": "false"},
|
|
}
|
|
)
|
|
|
|
assert "web" in has_direct
|
|
assert "web" not in already_managed
|
|
assert set(unconfigured) == {"image_gen", "video_gen", "tts", "browser"}
|
|
|
|
|
|
def test_get_gateway_eligible_tools_pool_excludes_video(monkeypatch):
|
|
"""A free-tool-pool user is offered the covered tools but NOT video gen."""
|
|
monkeypatch.setattr(ns, "get_nous_portal_account_info", lambda **kw: _pool_account())
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"_get_gateway_direct_credentials",
|
|
lambda: {"web": False, "image_gen": False, "video_gen": False, "tts": False, "browser": False},
|
|
)
|
|
|
|
unconfigured, has_direct, already_managed = ns.get_gateway_eligible_tools(
|
|
{"model": {"provider": "nous"}}
|
|
)
|
|
|
|
assert set(unconfigured) == {"web", "image_gen", "tts", "browser"}
|
|
assert "video_gen" not in unconfigured
|
|
assert "video_gen" not in has_direct
|
|
assert "video_gen" not in already_managed
|
|
|
|
|
|
def test_get_gateway_eligible_tools_empty_when_not_entitled(monkeypatch):
|
|
"""A logged-in free user with no pool and no paid access gets nothing."""
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda **kw: _account(logged_in=True, paid=False)
|
|
)
|
|
|
|
unconfigured, has_direct, already_managed = ns.get_gateway_eligible_tools(
|
|
{"model": {"provider": "nous"}}
|
|
)
|
|
|
|
assert (unconfigured, has_direct, already_managed) == ([], [], [])
|
|
|
|
|
|
def _capture_checklist(monkeypatch, *, selected_idx):
|
|
"""Patch prompt_checklist to capture its args and return chosen indices."""
|
|
captured = {}
|
|
|
|
def _fake_checklist(title, items, pre_selected=None):
|
|
captured["title"] = title
|
|
captured["items"] = list(items)
|
|
captured["pre_selected"] = list(pre_selected or [])
|
|
return list(selected_idx)
|
|
|
|
import hermes_cli.setup as setup_mod
|
|
|
|
monkeypatch.setattr(setup_mod, "prompt_checklist", _fake_checklist, raising=False)
|
|
monkeypatch.setattr(
|
|
"hermes_cli.config.save_config", lambda cfg: None, raising=False
|
|
)
|
|
return captured
|
|
|
|
|
|
def test_prompt_enable_tool_gateway_pool_offers_covered_tools_only(monkeypatch):
|
|
"""Pool user's checklist lists web/image/tts/browser and never video."""
|
|
monkeypatch.setattr(ns, "get_nous_portal_account_info", lambda **kw: _pool_account())
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"_get_gateway_direct_credentials",
|
|
lambda: {"web": False, "image_gen": False, "video_gen": False, "tts": False, "browser": False},
|
|
)
|
|
captured = _capture_checklist(monkeypatch, selected_idx=[])
|
|
|
|
config = {"model": {"provider": "nous"}}
|
|
ns.prompt_enable_tool_gateway(config)
|
|
|
|
blob = " ".join(captured["items"]).lower()
|
|
assert "firecrawl" in blob # web offered
|
|
assert "video" not in blob # video NOT offered to a pool user
|
|
# Pool-aware framing, not "subscription".
|
|
assert "free" in captured["title"].lower() and "pool" in captured["title"].lower()
|
|
|
|
|
|
def test_prompt_enable_tool_gateway_writes_only_selected(monkeypatch):
|
|
"""Selecting a subset writes use_gateway only for those tools."""
|
|
monkeypatch.setattr(ns, "get_nous_portal_account_info", lambda **kw: _pool_account())
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"_get_gateway_direct_credentials",
|
|
lambda: {"web": False, "image_gen": False, "video_gen": False, "tts": False, "browser": False},
|
|
)
|
|
# Offered order is _ALL_GATEWAY_KEYS filtered to covered: web, image_gen, tts, browser.
|
|
# Select index 0 (web) and 1 (image_gen) only.
|
|
_capture_checklist(monkeypatch, selected_idx=[0, 1])
|
|
|
|
config = {"model": {"provider": "nous"}}
|
|
changed = ns.prompt_enable_tool_gateway(config)
|
|
|
|
assert changed == {"web", "image_gen"}
|
|
assert config["web"]["use_gateway"] is True
|
|
assert config["image_gen"]["use_gateway"] is True
|
|
assert "tts" not in config or config.get("tts", {}).get("use_gateway") is not True
|
|
assert "video_gen" not in config
|
|
|
|
|
|
def test_prompt_enable_tool_gateway_paid_user_offers_video(monkeypatch):
|
|
"""Paid users still get video gen in the offer (regression guard)."""
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda **kw: _account(logged_in=True, paid=True)
|
|
)
|
|
monkeypatch.setattr(
|
|
ns,
|
|
"_get_gateway_direct_credentials",
|
|
lambda: {"web": False, "image_gen": False, "video_gen": False, "tts": False, "browser": False},
|
|
)
|
|
captured = _capture_checklist(monkeypatch, selected_idx=[])
|
|
|
|
ns.prompt_enable_tool_gateway({"model": {"provider": "nous"}})
|
|
|
|
blob = " ".join(captured["items"]).lower()
|
|
assert "video" in blob
|
|
|
|
|
|
def test_apply_nous_managed_defaults_writes_video_gen_config(monkeypatch):
|
|
"""apply_nous_managed_defaults must write video_gen.provider and
|
|
video_gen.use_gateway when a Nous subscriber selects video_gen
|
|
without a direct FAL_KEY."""
|
|
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda **kw: True)
|
|
monkeypatch.delenv("FAL_KEY", raising=False)
|
|
monkeypatch.setattr(ns, "fal_key_is_configured", lambda: False)
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=True, paid=True),
|
|
)
|
|
|
|
config = {"model": {"provider": "nous"}}
|
|
changed = ns.apply_nous_managed_defaults(
|
|
config, enabled_toolsets=["video_gen"],
|
|
)
|
|
|
|
assert "video_gen" in changed
|
|
assert config["video_gen"]["provider"] == "fal"
|
|
assert config["video_gen"]["use_gateway"] is True
|
|
|
|
|
|
def test_apply_nous_managed_defaults_writes_image_gen_config(monkeypatch):
|
|
"""apply_nous_managed_defaults must write image_gen.use_gateway
|
|
when a Nous subscriber selects image_gen without a direct FAL_KEY."""
|
|
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda **kw: True)
|
|
monkeypatch.delenv("FAL_KEY", raising=False)
|
|
monkeypatch.setattr(ns, "fal_key_is_configured", lambda: False)
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=True, paid=True),
|
|
)
|
|
|
|
config = {"model": {"provider": "nous"}}
|
|
changed = ns.apply_nous_managed_defaults(
|
|
config, enabled_toolsets=["image_gen"],
|
|
)
|
|
|
|
assert "image_gen" in changed
|
|
assert config["image_gen"]["use_gateway"] is True
|
|
|
|
|
|
def test_apply_nous_managed_defaults_skips_fal_tools_when_key_present(monkeypatch):
|
|
"""When FAL_KEY is set, apply_nous_managed_defaults should not touch
|
|
image_gen or video_gen config — the user's direct key takes precedence."""
|
|
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda **kw: True)
|
|
monkeypatch.setenv("FAL_KEY", "fal-direct-key")
|
|
monkeypatch.setattr(ns, "fal_key_is_configured", lambda: True)
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=True, paid=True),
|
|
)
|
|
|
|
config = {"model": {"provider": "nous"}}
|
|
changed = ns.apply_nous_managed_defaults(
|
|
config, enabled_toolsets=["image_gen", "video_gen"],
|
|
)
|
|
|
|
assert "image_gen" not in changed
|
|
assert "video_gen" not in changed
|
|
assert "image_gen" not in config
|
|
assert "video_gen" not in config
|
|
|
|
|
|
def test_apply_nous_managed_defaults_preserves_existing_video_gen_section(monkeypatch):
|
|
"""When video_gen config already exists as a dict, the function should
|
|
update it in-place rather than replacing it."""
|
|
monkeypatch.setattr(ns, "managed_nous_tools_enabled", lambda **kw: True)
|
|
monkeypatch.delenv("FAL_KEY", raising=False)
|
|
monkeypatch.setattr(ns, "fal_key_is_configured", lambda: False)
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=True, paid=True),
|
|
)
|
|
|
|
config = {
|
|
"model": {"provider": "nous"},
|
|
"video_gen": {"model": "pixverse-v6"},
|
|
}
|
|
changed = ns.apply_nous_managed_defaults(
|
|
config, enabled_toolsets=["video_gen"],
|
|
)
|
|
|
|
assert "video_gen" in changed
|
|
assert config["video_gen"]["provider"] == "fal"
|
|
assert config["video_gen"]["use_gateway"] is True
|
|
# Pre-existing keys should be preserved
|
|
assert config["video_gen"]["model"] == "pixverse-v6"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# ensure_nous_portal_access — inline login gate for `hermes tools`
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_ensure_nous_portal_access_fast_path_when_already_paid(monkeypatch):
|
|
"""Already-entitled users return True without any login prompt."""
|
|
login_called = {"v": False}
|
|
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=True, paid=True),
|
|
)
|
|
|
|
def _login(**kw):
|
|
login_called["v"] = True
|
|
return True
|
|
|
|
monkeypatch.setattr(ns, "_run_nous_portal_login_only", _login)
|
|
|
|
assert ns.ensure_nous_portal_access() is True
|
|
assert login_called["v"] is False
|
|
|
|
|
|
def test_ensure_nous_portal_access_logs_in_then_grants(monkeypatch):
|
|
"""Logged-out user logs in, then entitlement re-check shows paid access."""
|
|
states = iter([
|
|
_account(logged_in=False, paid=None), # initial check
|
|
_account(logged_in=True, paid=True), # after login
|
|
])
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info", lambda **kw: next(states),
|
|
)
|
|
monkeypatch.setattr(ns, "_run_nous_portal_login_only", lambda **kw: True)
|
|
|
|
assert ns.ensure_nous_portal_access() is True
|
|
|
|
|
|
def test_ensure_nous_portal_access_returns_false_when_login_declined(monkeypatch):
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=False, paid=None),
|
|
)
|
|
monkeypatch.setattr(ns, "_run_nous_portal_login_only", lambda **kw: False)
|
|
|
|
assert ns.ensure_nous_portal_access() is False
|
|
|
|
|
|
def test_ensure_nous_portal_access_false_when_logged_in_but_unpaid(monkeypatch):
|
|
"""Logged in already but no paid access — no login attempt, returns False."""
|
|
login_called = {"v": False}
|
|
monkeypatch.setattr(
|
|
ns, "get_nous_portal_account_info",
|
|
lambda **kw: _account(logged_in=True, paid=False),
|
|
)
|
|
|
|
def _login(**kw):
|
|
login_called["v"] = True
|
|
return True
|
|
|
|
monkeypatch.setattr(ns, "_run_nous_portal_login_only", _login)
|
|
|
|
assert ns.ensure_nous_portal_access() is False
|
|
# Already logged in, so no device-code login should be attempted.
|
|
assert login_called["v"] is False
|