feat(onboarding): add first-run membership activation
This commit is contained in:
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user