423 lines
15 KiB
Python
423 lines
15 KiB
Python
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from yuxi.channels.adapters.twitch.setup import (
|
||
|
|
SetupWizardState,
|
||
|
|
SetupWizardStep,
|
||
|
|
TwitchSetupWizard,
|
||
|
|
create_setup_wizard_steps,
|
||
|
|
setup_wizard_to_config,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
class TestSetupWizardStep:
|
||
|
|
def test_default_values(self):
|
||
|
|
step = SetupWizardStep(step_id="test")
|
||
|
|
assert step.step_id == "test"
|
||
|
|
assert step.required is True
|
||
|
|
assert step.completed is False
|
||
|
|
assert step.config_key == ""
|
||
|
|
assert step.config_value is None
|
||
|
|
|
||
|
|
def test_custom_values(self):
|
||
|
|
step = SetupWizardStep(
|
||
|
|
step_id="username",
|
||
|
|
title="Bot Username",
|
||
|
|
description="Enter username",
|
||
|
|
required=True,
|
||
|
|
completed=True,
|
||
|
|
config_key="bot_username",
|
||
|
|
config_value="mybot",
|
||
|
|
)
|
||
|
|
assert step.step_id == "username"
|
||
|
|
assert step.title == "Bot Username"
|
||
|
|
assert step.completed is True
|
||
|
|
assert step.config_value == "mybot"
|
||
|
|
|
||
|
|
|
||
|
|
class TestSetupWizardState:
|
||
|
|
def test_empty_state(self):
|
||
|
|
state = SetupWizardState()
|
||
|
|
assert state.steps == []
|
||
|
|
assert state.current_step == 0
|
||
|
|
assert state.current is None
|
||
|
|
assert state.is_complete is True
|
||
|
|
|
||
|
|
def test_with_steps_not_complete(self):
|
||
|
|
step1 = SetupWizardStep(step_id="s1", required=True)
|
||
|
|
step2 = SetupWizardStep(step_id="s2", required=True)
|
||
|
|
state = SetupWizardState(steps=[step1, step2])
|
||
|
|
assert state.is_complete is False
|
||
|
|
assert state.current == step1
|
||
|
|
|
||
|
|
def test_advance_marks_completed(self):
|
||
|
|
step1 = SetupWizardStep(step_id="s1", required=True)
|
||
|
|
step2 = SetupWizardStep(step_id="s2", required=True)
|
||
|
|
state = SetupWizardState(steps=[step1, step2])
|
||
|
|
state.advance()
|
||
|
|
assert step1.completed is True
|
||
|
|
assert state.current_step == 1
|
||
|
|
assert state.current == step2
|
||
|
|
|
||
|
|
def test_state_to_config(self):
|
||
|
|
step = SetupWizardStep(step_id="s1", config_key="bot_username", config_value="mybot")
|
||
|
|
state = SetupWizardState(steps=[step])
|
||
|
|
config = state.to_config()
|
||
|
|
assert config["bot_username"] == "mybot"
|
||
|
|
|
||
|
|
def test_skips_step_with_none_value(self):
|
||
|
|
step = SetupWizardStep(step_id="s1", config_key="key", config_value=None)
|
||
|
|
state = SetupWizardState(steps=[step])
|
||
|
|
config = state.to_config()
|
||
|
|
assert "key" not in config
|
||
|
|
|
||
|
|
def test_optional_step_not_required_for_complete(self):
|
||
|
|
step1 = SetupWizardStep(step_id="s1", required=True, completed=True)
|
||
|
|
step2 = SetupWizardStep(step_id="s2", required=False)
|
||
|
|
state = SetupWizardState(steps=[step1, step2])
|
||
|
|
assert state.is_complete is True
|
||
|
|
|
||
|
|
def test_advance_beyond_last_step(self):
|
||
|
|
step = SetupWizardStep(step_id="s1", required=True)
|
||
|
|
state = SetupWizardState(steps=[step])
|
||
|
|
state.advance()
|
||
|
|
state.advance()
|
||
|
|
assert state.current_step == 2
|
||
|
|
assert state.current is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestCreateSetupWizardSteps:
|
||
|
|
def test_returns_steps_list(self):
|
||
|
|
steps = create_setup_wizard_steps()
|
||
|
|
assert len(steps) > 0
|
||
|
|
|
||
|
|
def test_contains_required_steps(self):
|
||
|
|
steps = create_setup_wizard_steps()
|
||
|
|
step_ids = [s.step_id for s in steps]
|
||
|
|
assert "username" in step_ids
|
||
|
|
assert "token" in step_ids
|
||
|
|
assert "client_id" in step_ids
|
||
|
|
assert "channels" in step_ids
|
||
|
|
assert "group_access" in step_ids
|
||
|
|
|
||
|
|
def test_required_steps_are_required(self):
|
||
|
|
steps = create_setup_wizard_steps()
|
||
|
|
for step in steps:
|
||
|
|
if step.step_id in ("username", "token", "client_id", "channels", "group_access"):
|
||
|
|
assert step.required is True
|
||
|
|
|
||
|
|
def test_optional_steps_not_required(self):
|
||
|
|
steps = create_setup_wizard_steps()
|
||
|
|
for step in steps:
|
||
|
|
if step.step_id in ("client_secret", "refresh_token", "dm_policy", "allowed_roles", "silent", "probe_timeout"):
|
||
|
|
assert step.required is False
|
||
|
|
|
||
|
|
def test_completed_reuses_existing_config(self):
|
||
|
|
steps = create_setup_wizard_steps({"bot_username": "existing_bot", "client_id": "existing_cid"})
|
||
|
|
username_step = next(s for s in steps if s.step_id == "username")
|
||
|
|
assert username_step.completed is True
|
||
|
|
client_id_step = next(s for s in steps if s.step_id == "client_id")
|
||
|
|
assert client_id_step.completed is True
|
||
|
|
token_step = next(s for s in steps if s.step_id == "token")
|
||
|
|
assert token_step.completed is False
|
||
|
|
|
||
|
|
|
||
|
|
class TestSetupWizardToConfig:
|
||
|
|
def test_converts_state_to_dict(self):
|
||
|
|
step = SetupWizardStep(step_id="s1", config_key="bot_username", config_value="testbot")
|
||
|
|
state = SetupWizardState(steps=[step])
|
||
|
|
result = setup_wizard_to_config(state)
|
||
|
|
assert result == {"bot_username": "testbot"}
|
||
|
|
|
||
|
|
|
||
|
|
class TestTwitchSetupWizard:
|
||
|
|
def test_initial_state(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
assert wizard.config == {}
|
||
|
|
|
||
|
|
def test_initial_with_existing_config(self):
|
||
|
|
wizard = TwitchSetupWizard({"bot_username": "existing_bot"})
|
||
|
|
assert wizard.config["bot_username"] == "existing_bot"
|
||
|
|
|
||
|
|
def test_resolve_account_id(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.resolve_account_id("my_account")
|
||
|
|
assert result == "my_account"
|
||
|
|
|
||
|
|
def test_resolve_account_id_default(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.resolve_account_id()
|
||
|
|
assert result == "default"
|
||
|
|
|
||
|
|
def test_prompt_username_valid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_username("TestBot")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["bot_username"] == "testbot"
|
||
|
|
|
||
|
|
def test_prompt_username_empty(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_username("")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_username_whitespace(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_username(" ")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_token_with_value(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_token("mytoken")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["access_token"].startswith("oauth:")
|
||
|
|
|
||
|
|
def test_prompt_token_with_oauth_prefix(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_token("oauth:existing")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["access_token"] == "oauth:existing"
|
||
|
|
|
||
|
|
def test_prompt_token_from_env(self, monkeypatch):
|
||
|
|
import os
|
||
|
|
|
||
|
|
monkeypatch.setitem(os.environ, "TWITCH_ACCESS_TOKEN", "env_token")
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_token("")
|
||
|
|
assert result is True
|
||
|
|
assert "env_token" in wizard.config["access_token"]
|
||
|
|
|
||
|
|
def test_prompt_token_no_value_or_env(self, monkeypatch):
|
||
|
|
monkeypatch.delenv("TWITCH_ACCESS_TOKEN", raising=False)
|
||
|
|
monkeypatch.delenv("OPENCLAW_TWITCH_ACCESS_TOKEN", raising=False)
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_token("")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_client_id_valid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_client_id("my_client_id")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["client_id"] == "my_client_id"
|
||
|
|
|
||
|
|
def test_prompt_client_id_empty(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_client_id("")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_client_secret_with_value(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_client_secret("my_secret")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["client_secret"] == "my_secret"
|
||
|
|
|
||
|
|
def test_prompt_client_secret_empty(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_client_secret("")
|
||
|
|
assert result is True
|
||
|
|
|
||
|
|
def test_prompt_channels_string(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_channels("ch1, ch2, CH3")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["channels"] == ["ch1", "ch2", "ch3"]
|
||
|
|
|
||
|
|
def test_prompt_channels_list(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_channels(["ch1", "CH2"])
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["channels"] == ["ch1", "CH2"]
|
||
|
|
|
||
|
|
def test_prompt_channels_empty(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_channels("")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_refresh_token_with_value(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_refresh_token("refresh123")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["refresh_token"] == "refresh123"
|
||
|
|
|
||
|
|
def test_prompt_refresh_token_empty(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_refresh_token("")
|
||
|
|
assert result is True
|
||
|
|
|
||
|
|
def test_prompt_dm_policy_open(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_dm_policy("open")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["dm_policy"] == "open"
|
||
|
|
|
||
|
|
def test_prompt_dm_policy_pairing(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_dm_policy("pairing")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["dm_policy"] == "pairing"
|
||
|
|
|
||
|
|
def test_prompt_dm_policy_invalid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_dm_policy("block")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_group_access_valid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
for policy in ("open", "allowlist", "disabled", "mention_only"):
|
||
|
|
result = wizard.prompt_group_access(policy)
|
||
|
|
assert result is True
|
||
|
|
|
||
|
|
def test_prompt_group_access_invalid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_group_access("invalid")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_allowed_roles_string(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_allowed_roles("moderator, vip, subscriber")
|
||
|
|
assert result is True
|
||
|
|
assert "moderator" in wizard.config["allowedRoles"]
|
||
|
|
assert "vip" in wizard.config["allowedRoles"]
|
||
|
|
assert "subscriber" in wizard.config["allowedRoles"]
|
||
|
|
|
||
|
|
def test_prompt_allowed_roles_list(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_allowed_roles(["moderator", "vip"])
|
||
|
|
assert result is True
|
||
|
|
assert "moderator" in wizard.config["allowedRoles"]
|
||
|
|
|
||
|
|
def test_prompt_allowed_roles_invalid_defaults_to_all(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_allowed_roles("admin, super")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["allowedRoles"] == ["all"]
|
||
|
|
|
||
|
|
def test_prompt_silent_yes(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
for val in ("yes", "true", "1", "y", "on"):
|
||
|
|
result = wizard.prompt_silent(val)
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["silent"] is True
|
||
|
|
|
||
|
|
def test_prompt_silent_no(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_silent("no")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["silent"] is False
|
||
|
|
|
||
|
|
def test_prompt_probe_timeout_valid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_probe_timeout(5000)
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["probe_timeout_ms"] == 5000
|
||
|
|
|
||
|
|
def test_prompt_probe_timeout_string_valid(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_probe_timeout("5000")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["probe_timeout_ms"] == 5000
|
||
|
|
|
||
|
|
def test_prompt_probe_timeout_empty(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_probe_timeout("")
|
||
|
|
assert result is True
|
||
|
|
assert wizard.config["probe_timeout_ms"] == 10000
|
||
|
|
|
||
|
|
def test_prompt_probe_timeout_too_small(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_probe_timeout(500)
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_prompt_probe_timeout_invalid_string(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.prompt_probe_timeout("not_a_number")
|
||
|
|
assert result is False
|
||
|
|
|
||
|
|
def test_disable(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.disable()
|
||
|
|
assert result == {"enabled": False}
|
||
|
|
|
||
|
|
def test_finalize_complete(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard.prompt_username("testbot")
|
||
|
|
wizard.prompt_token("mytoken")
|
||
|
|
wizard.prompt_client_id("cid123")
|
||
|
|
wizard.prompt_channels("test_channel")
|
||
|
|
wizard.prompt_group_access("open")
|
||
|
|
result = wizard.finalize()
|
||
|
|
assert result["enabled"] is True
|
||
|
|
assert result["config"]["bot_username"] == "testbot"
|
||
|
|
assert result["validation"]["valid"] is True
|
||
|
|
|
||
|
|
def test_finalize_incomplete(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
result = wizard.finalize()
|
||
|
|
assert result["enabled"] is True
|
||
|
|
assert result["validation"]["valid"] is False
|
||
|
|
|
||
|
|
def test_finalize_disabled(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard.disable()
|
||
|
|
result = wizard.finalize()
|
||
|
|
assert result == {"enabled": False}
|
||
|
|
|
||
|
|
def test_finalize_and_write_no_path(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard.prompt_username("testbot")
|
||
|
|
wizard.prompt_token("token")
|
||
|
|
wizard.prompt_client_id("cid")
|
||
|
|
wizard.prompt_channels("ch")
|
||
|
|
wizard.prompt_group_access("open")
|
||
|
|
result = wizard.finalize_and_write()
|
||
|
|
assert result["storage"]["mode"] == "memory"
|
||
|
|
assert result["storage"]["success"] is True
|
||
|
|
|
||
|
|
def test_finalize_and_write_with_path(self, tmp_path):
|
||
|
|
import json
|
||
|
|
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard.prompt_username("testbot")
|
||
|
|
wizard.prompt_token("token")
|
||
|
|
wizard.prompt_client_id("cid")
|
||
|
|
wizard.prompt_channels("ch")
|
||
|
|
wizard.prompt_group_access("open")
|
||
|
|
result = wizard.finalize_and_write(str(tmp_path))
|
||
|
|
assert result["storage"]["success"] is True
|
||
|
|
config_path = tmp_path / "twitch_config.json"
|
||
|
|
assert config_path.exists()
|
||
|
|
saved = json.loads(config_path.read_text(encoding="utf-8"))
|
||
|
|
assert saved["bot_username"] == "testbot"
|
||
|
|
|
||
|
|
def test_state_property(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
assert isinstance(wizard.state, SetupWizardState)
|
||
|
|
|
||
|
|
def test_config_property(self):
|
||
|
|
wizard = TwitchSetupWizard({"bot_username": "mybot"})
|
||
|
|
assert wizard.config["bot_username"] == "mybot"
|
||
|
|
|
||
|
|
def test_validate_no_client_secret_warning(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard.prompt_username("bot")
|
||
|
|
wizard.prompt_token("tok")
|
||
|
|
wizard.prompt_client_id("cid")
|
||
|
|
wizard.prompt_channels("ch")
|
||
|
|
wizard.prompt_group_access("open")
|
||
|
|
result = wizard.finalize()
|
||
|
|
assert any("EventSub" in w for w in result["validation"]["warnings"])
|
||
|
|
|
||
|
|
def test_validate_invalid_group_policy_error(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard._config["group_policy"] = "invalid"
|
||
|
|
result = wizard.finalize()
|
||
|
|
assert result["validation"]["valid"] is False
|
||
|
|
|
||
|
|
def test_validate_unknown_dm_policy_warns(self):
|
||
|
|
wizard = TwitchSetupWizard()
|
||
|
|
wizard.prompt_username("bot")
|
||
|
|
wizard.prompt_token("tok")
|
||
|
|
wizard.prompt_client_id("cid")
|
||
|
|
wizard.prompt_channels("ch")
|
||
|
|
wizard.prompt_group_access("open")
|
||
|
|
wizard._config["dm_policy"] = "unknown_policy"
|
||
|
|
result = wizard.finalize()
|
||
|
|
assert any("dm_policy" in w for w in result["validation"]["warnings"])
|