feat(onboarding): add first-run membership activation

This commit is contained in:
chengma
2026-07-23 17:29:16 +08:00
parent a0862dacbd
commit 7dabeb9971
15 changed files with 855 additions and 36 deletions
+262
View File
@@ -64,6 +64,7 @@ from app.gui import (
ImageStudioPreviewDialog,
ImageStudioTab,
MainWindow,
MembershipActivationDialog,
ProductSuitePreviewDialog,
ProductSuiteTab,
SettingsTab,
@@ -344,6 +345,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
DestructiveRole = object()
RejectRole = object()
Warning = object()
Information = object()
def __init__(self, parent=None):
self.parent = parent
@@ -407,6 +409,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
DestructiveRole = object()
RejectRole = object()
Warning = object()
Information = object()
def __init__(self, parent=None):
self.parent = parent
@@ -11194,6 +11197,265 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_membership_activation_dialog_collects_only_key_and_validates_locally(self):
dialog = MembershipActivationDialog()
self.addCleanup(dialog.close)
submitted = []
dialog.validationRequested.connect(submitted.append)
dialog.validate_button.click()
self.assertEqual([], submitted)
self.assertIn("请输入会员 API Key", dialog.status_label.text())
self.assertIsNone(dialog.findChild(QLineEdit, "cmhubBaseUrlEdit"))
self.assertEqual(QLineEdit.Password, dialog.api_key_edit.echoMode())
dialog.api_key_edit.setText("temporary-test-key")
dialog.show_key_checkbox.setChecked(True)
dialog.validate_button.click()
self.assertEqual(["temporary-test-key"], submitted)
self.assertEqual(QLineEdit.Normal, dialog.api_key_edit.echoMode())
self.assertEqual("验证并开始使用", dialog.validate_button.text())
def test_missing_membership_key_opens_activation_instead_of_raw_settings(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
self.addCleanup(window.close)
status = subscription.SubscriptionStatus(
subscription.STATUS_NOT_CONFIGURED
)
with mock.patch.object(window, "_show_membership_activation") as activation:
window._apply_subscription_status(status)
activation.assert_called_once_with()
self.assertEqual("请先绑定会员账号", window.statusBar().currentMessage())
for index, title in enumerate(TAB_TITLES):
self.assertEqual(title == "设置", window.tabs.isTabEnabled(index))
def test_activation_validation_uses_in_memory_key_worker(self):
class _Worker:
def __init__(self):
self.finished = DummySignal()
self.cancelled = DummySignal()
self.cancel_requested = False
def cancel(self):
self.cancel_requested = True
with self.make_temp_dir() as temp_dir:
config_path = os.path.join(temp_dir, "config.json")
cfg = appconfig.load_config(config_path)
window = MainWindow(config=cfg, config_path=config_path)
self.addCleanup(window.close)
window._show_membership_activation()
worker = _Worker()
thread = FakeThread()
with mock.patch.object(
main_window,
"SubscriptionCheckWorker",
return_value=worker,
) as worker_factory, mock.patch.object(
main_window,
"run_worker",
return_value=thread,
):
window._activation_dialog.api_key_edit.setText("temporary-test-key")
window._activation_dialog.validate_button.click()
self.assertTrue(thread.started)
self.assertFalse(window._activation_dialog.validate_button.isEnabled())
kwargs = worker_factory.call_args.kwargs
self.assertEqual("temporary-test-key", kwargs["api_key"])
self.assertEqual(appconfig.DEFAULT_CMHUB_BASE_URL, kwargs["base_url"])
self.assertEqual(
"",
appconfig.get_cmhub_api_key(cfg["cmhub_config_path"]),
)
def test_activation_invalid_key_is_not_persisted(self):
with self.make_temp_dir() as temp_dir:
config_path = os.path.join(temp_dir, "config.json")
cfg = appconfig.load_config(config_path)
window = MainWindow(config=cfg, config_path=config_path)
self.addCleanup(window.close)
window._show_membership_activation()
window._subscription_request_token = 7
window._on_activation_checked(
7,
"invalid-test-key",
{
"subscription": subscription.SubscriptionStatus(
subscription.STATUS_KEY_INVALID
)
},
)
self.assertEqual(
"",
appconfig.get_cmhub_api_key(cfg["cmhub_config_path"]),
)
self.assertIn("会员 API Key 无效", window._activation_dialog.status_label.text())
self.assertFalse(
window.tabs.isTabEnabled(TAB_TITLES.index("① 导入采集"))
)
def test_activation_valid_key_without_plan_is_saved_but_stays_restricted(self):
with self.make_temp_dir() as temp_dir:
config_path = os.path.join(temp_dir, "config.json")
cfg = appconfig.load_config(config_path)
window = MainWindow(config=cfg, config_path=config_path)
self.addCleanup(window.close)
window._show_membership_activation()
window._subscription_request_token = 8
window._on_activation_checked(
8,
"valid-without-plan-key",
{
"subscription": subscription.SubscriptionStatus(
subscription.STATUS_REQUIRED,
manage_url="https://cm.833729.com",
)
},
)
self.assertEqual(
"valid-without-plan-key",
appconfig.get_cmhub_api_key(cfg["cmhub_config_path"]),
)
self.assertIsNotNone(window._activation_dialog)
self.assertIn(
"尚未开通",
window._activation_dialog.status_label.text(),
)
for index, title in enumerate(TAB_TITLES):
self.assertEqual(title == "设置", window.tabs.isTabEnabled(index))
def test_closing_activation_dialog_exits_restricted_app(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
self.addCleanup(window.close)
window._subscription_status = subscription.SubscriptionStatus(
subscription.STATUS_NOT_CONFIGURED
)
window._show_membership_activation()
with mock.patch.object(window, "close") as close:
window._activation_dialog.reject()
close.assert_called_once_with()
self.assertIsNone(window._activation_dialog)
def test_activation_success_saves_key_and_marks_guide_pending(self):
with self.make_temp_dir() as temp_dir:
config_path = os.path.join(temp_dir, "config.json")
cfg = appconfig.load_config(config_path)
window = MainWindow(config=cfg, config_path=config_path)
self.addCleanup(window.close)
window._show_membership_activation()
window._subscription_request_token = 9
active = subscription.SubscriptionStatus(
subscription.STATUS_ACTIVE,
account_name="新账号",
plan_name="测试套餐",
expires_at="2026-08-20T23:59:59+08:00",
)
with mock.patch.object(main_window.QTimer, "singleShot"):
window._on_activation_checked(
9,
"valid-test-key",
{"subscription": active},
)
self.assertIsNone(window._activation_dialog)
self.assertEqual(
"valid-test-key",
appconfig.get_cmhub_api_key(cfg["cmhub_config_path"]),
)
self.assertEqual(
"pending",
appconfig.first_use_guide_state(
appconfig.load_config(config_path)
),
)
self.assertEqual(
"valid-test-key",
window._settings_tab().cmhub_api_key_edit.text(),
)
self.assertTrue(
all(window.tabs.isTabEnabled(index) for index in range(window.tabs.count()))
)
def test_first_use_guide_can_start_account_setup_or_remind_later(self):
with self.make_temp_dir() as temp_dir:
config_path = os.path.join(temp_dir, "config.json")
cfg = appconfig.load_config(config_path)
appconfig.save_first_use_guide_state("pending", path=config_path)
cfg = appconfig.load_config(config_path)
window = MainWindow(config=cfg, config_path=config_path)
self.addCleanup(window.close)
window._subscription_status = subscription.SubscriptionStatus(
subscription.STATUS_ACTIVE,
account_name="新账号",
plan_name="测试套餐",
expires_at="2026-08-20T23:59:59+08:00",
)
fake_box, boxes = self.make_fake_message_box("开始配置店铺")
with mock.patch.object(main_window, "QMessageBox", fake_box):
window._show_first_use_guide_if_pending()
self.assertEqual(1, len(boxes))
self.assertIn("1. 添加蝦皮店铺账号", boxes[0].text)
self.assertEqual(TAB_TITLES.index("账号管理"), window.tabs.currentIndex())
self.assertEqual(
"completed",
appconfig.first_use_guide_state(appconfig.load_config(config_path)),
)
appconfig.save_first_use_guide_state("pending", path=config_path)
window.config.clear()
window.config.update(appconfig.load_config(config_path))
window._first_use_guide_shown = False
fake_box, boxes = self.make_fake_message_box("稍后提醒")
with mock.patch.object(main_window, "QMessageBox", fake_box):
window._show_first_use_guide_if_pending()
self.assertEqual(1, len(boxes))
self.assertEqual(
"pending",
appconfig.first_use_guide_state(appconfig.load_config(config_path)),
)
self.assertIn("稍后", window.statusBar().currentMessage())
def test_activation_member_center_uses_fixed_official_https_url(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))
self.addCleanup(window.close)
window._show_membership_activation()
with mock.patch.object(
main_window.QDesktopServices,
"openUrl",
return_value=False,
) as open_url:
window._open_activation_member_center()
open_url.assert_called_once()
self.assertEqual(
appconfig.DEFAULT_MEMBER_CENTER_URL,
open_url.call_args.args[0].toString(),
)
self.assertIn(
"无法打开会员中心",
window._activation_dialog.status_label.text(),
)
def test_main_window_displays_active_subscription_in_header(self):
with self.make_temp_dir() as temp_dir:
window = MainWindow(config=self.make_config(temp_dir))