feat: 完成Tab④账号管理
- 新增 accounts 服务层,封装账号 CRUD、目录创建、端口分配与登录检测 - 接入 PySide6 账号管理 Tab:表格、账号弹窗、启动登录、后台检测登录 - 密码仅本地保存并在 UI 打码,表格不展示明文;不自动登录或填密码 - 增加顶部 Tab 栏防误点样式,扩大点击区域并高亮当前 Tab - 补充账号服务和 GUI 单元测试,并同步架构、API、路由、任务和进度文档
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import accounts
|
||||
from app import config as account_config
|
||||
|
||||
|
||||
class AccountsTests(TempDirMixin, unittest.TestCase):
|
||||
def make_config(self, temp_dir):
|
||||
return {
|
||||
"chrome_path": "chrome.exe",
|
||||
"user_data_root": os.path.join(temp_dir, "chrome_user_data_dir"),
|
||||
"db_path": os.path.join(temp_dir, "cmshopee.db"),
|
||||
"debug_port_range": [9222, 9224],
|
||||
}
|
||||
|
||||
def test_create_account_stores_record_and_creates_user_data_dir(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
account = accounts.create_account(
|
||||
"主店",
|
||||
"alias",
|
||||
"https://seller.shopee.tw/portal/",
|
||||
9222,
|
||||
password="secret",
|
||||
note="备注",
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
self.assertEqual("主店", account.account_name)
|
||||
self.assertEqual("alias", account.alias)
|
||||
self.assertEqual("seller.shopee.tw", account.region_host)
|
||||
self.assertEqual("alias_cdb6fdbe", account.slug)
|
||||
self.assertTrue(os.path.isdir(account.user_data_dir))
|
||||
self.assertEqual("******", accounts.mask_password(account.password))
|
||||
self.assertEqual([account], accounts.list_accounts(config=cfg))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_update_account_can_change_alias_and_rebuild_directory(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
||||
|
||||
updated = accounts.update_account(
|
||||
"alias",
|
||||
"新主店",
|
||||
"new alias",
|
||||
"seller.shopee.tw",
|
||||
9333,
|
||||
password="new-secret",
|
||||
note="新备注",
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
self.assertEqual("new alias", updated.alias)
|
||||
self.assertEqual(account_config.make_slug("new alias"), updated.slug)
|
||||
self.assertEqual(9333, updated.debug_port)
|
||||
self.assertTrue(os.path.isdir(updated.user_data_dir))
|
||||
with self.assertRaises(accounts.AccountError):
|
||||
accounts.get_account("alias", config=cfg)
|
||||
self.assertEqual("new-secret", accounts.get_account("new alias", config=cfg).password)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_next_debug_port_skips_existing_accounts(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("店1", "a1", debug_port=9222, config=cfg)
|
||||
accounts.create_account("店2", "a2", debug_port=9223, config=cfg)
|
||||
|
||||
self.assertEqual(9224, accounts.next_debug_port(config=cfg))
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_launch_for_login_only_launches_chrome(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
account = accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
||||
process = object()
|
||||
|
||||
with mock.patch("app.accounts.chrome.launch_chrome", return_value=process) as launch:
|
||||
result = accounts.launch_for_login("alias", config=cfg)
|
||||
|
||||
self.assertIs(process, result)
|
||||
launch.assert_called_once_with(account, config=cfg)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_detect_login_updates_last_login_at_when_logged_in(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
||||
|
||||
with mock.patch(
|
||||
"app.accounts.editor.login_status",
|
||||
return_value={"logged_in": True, "reason": None},
|
||||
) as login_status:
|
||||
status = accounts.detect_login("alias", timeout=1, config=cfg)
|
||||
|
||||
self.assertTrue(status["logged_in"])
|
||||
login_status.assert_called_once()
|
||||
self.assertIsNotNone(accounts.get_account("alias", config=cfg).last_login_at)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_duplicate_alias_raises_clear_error(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("店1", "alias", debug_port=9222, config=cfg)
|
||||
|
||||
with self.assertRaises(accounts.AccountError):
|
||||
accounts.create_account("店2", "alias", debug_port=9223, config=cfg)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_duplicate_debug_port_raises_clear_error(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("店1", "a1", debug_port=9222, config=cfg)
|
||||
accounts.create_account("店2", "a2", debug_port=9223, config=cfg)
|
||||
|
||||
with self.assertRaises(accounts.AccountError):
|
||||
accounts.create_account("店3", "a3", debug_port=9222, config=cfg)
|
||||
with self.assertRaises(accounts.AccountError):
|
||||
accounts.update_account(
|
||||
"a2",
|
||||
"店2",
|
||||
"a2",
|
||||
"seller.shopee.tw",
|
||||
9222,
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+103
-17
@@ -5,40 +5,126 @@ import sys
|
||||
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
|
||||
from _helpers import TempDirMixin
|
||||
|
||||
from app import gui
|
||||
from app import accounts
|
||||
|
||||
if gui.QT_IMPORT_ERROR is not None:
|
||||
raise unittest.SkipTest("PySide6 未安装")
|
||||
|
||||
from PySide6.QtWidgets import QApplication
|
||||
from PySide6.QtWidgets import QApplication, QLineEdit
|
||||
|
||||
from app.gui import MainWindow, TAB_TITLES
|
||||
from app.gui import AccountDialog, AccountsTab, MainWindow, TAB_STYLE, TAB_TITLES
|
||||
|
||||
|
||||
class GuiTests(unittest.TestCase):
|
||||
class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
cls.app = QApplication.instance() or QApplication([])
|
||||
|
||||
def test_main_window_has_five_tabs_in_workflow_order(self):
|
||||
window = MainWindow()
|
||||
self.addCleanup(window.close)
|
||||
def make_config(self, temp_dir):
|
||||
return {
|
||||
"chrome_path": "chrome.exe",
|
||||
"user_data_root": os.path.join(temp_dir, "chrome_user_data_dir"),
|
||||
"db_path": os.path.join(temp_dir, "cmshopee.db"),
|
||||
"debug_port_range": [9222, 9260],
|
||||
}
|
||||
|
||||
self.assertEqual(5, window.tabs.count())
|
||||
self.assertEqual(
|
||||
TAB_TITLES,
|
||||
[window.tabs.tabText(index) for index in range(window.tabs.count())],
|
||||
)
|
||||
self.assertEqual("就绪", window.statusBar().currentMessage())
|
||||
def test_main_window_has_five_tabs_in_workflow_order(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
window = MainWindow(config=self.make_config(temp_dir))
|
||||
self.addCleanup(window.close)
|
||||
|
||||
self.assertEqual(5, window.tabs.count())
|
||||
self.assertEqual(
|
||||
TAB_TITLES,
|
||||
[window.tabs.tabText(index) for index in range(window.tabs.count())],
|
||||
)
|
||||
self.assertEqual("就绪", window.statusBar().currentMessage())
|
||||
self.assertEqual(TAB_STYLE, window.tabs.styleSheet())
|
||||
self.assertIn("min-width: 128px", window.tabs.styleSheet())
|
||||
self.assertIn("padding: 8px 18px", window.tabs.styleSheet())
|
||||
self.assertIn("margin-right: 8px", window.tabs.styleSheet())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_tab_switch_updates_status_bar(self):
|
||||
window = MainWindow()
|
||||
self.addCleanup(window.close)
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
window = MainWindow(config=self.make_config(temp_dir))
|
||||
self.addCleanup(window.close)
|
||||
|
||||
window.tabs.setCurrentIndex(2)
|
||||
window.tabs.setCurrentIndex(2)
|
||||
|
||||
self.assertEqual("③ 更新shopee", window.tabs.tabText(window.tabs.currentIndex()))
|
||||
self.assertEqual("当前:③ 更新shopee", window.statusBar().currentMessage())
|
||||
self.assertEqual("③ 更新shopee", window.tabs.tabText(window.tabs.currentIndex()))
|
||||
self.assertEqual("当前:③ 更新shopee", window.statusBar().currentMessage())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_accounts_tab_lists_accounts_without_showing_password(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account(
|
||||
"主店",
|
||||
"alias",
|
||||
"seller.shopee.tw",
|
||||
9222,
|
||||
password="secret",
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
window = MainWindow(config=cfg)
|
||||
self.addCleanup(window.close)
|
||||
tab = window.tabs.widget(TAB_TITLES.index("④ 账号管理"))
|
||||
|
||||
self.assertIsInstance(tab, AccountsTab)
|
||||
self.assertEqual(1, tab.table.rowCount())
|
||||
self.assertEqual("主店", tab.table.item(0, 0).text())
|
||||
self.assertEqual("alias", tab.table.item(0, 1).text())
|
||||
self.assertEqual("未知", tab.table.item(0, 4).text())
|
||||
visible_values = [
|
||||
tab.table.item(0, column).text()
|
||||
for column in range(tab.table.columnCount())
|
||||
if tab.table.item(0, column) is not None
|
||||
]
|
||||
self.assertNotIn("secret", visible_values)
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_account_dialog_masks_password_field(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
account = accounts.create_account(
|
||||
"主店",
|
||||
"alias",
|
||||
debug_port=9222,
|
||||
password="secret",
|
||||
config=cfg,
|
||||
)
|
||||
|
||||
dialog = AccountDialog(account=account, config=cfg)
|
||||
self.addCleanup(dialog.close)
|
||||
|
||||
self.assertEqual(QLineEdit.Password, dialog.password_edit.echoMode())
|
||||
self.assertEqual("secret", dialog.password_edit.text())
|
||||
self.assertIn(account.slug, dialog.user_data_dir_edit.text())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_accounts_tab_updates_login_status_cell(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
accounts.create_account("主店", "alias", debug_port=9222, config=cfg)
|
||||
tab = AccountsTab(config=cfg)
|
||||
self.addCleanup(tab.close)
|
||||
|
||||
tab._on_login_check_finished(
|
||||
{"alias": "alias", "status": {"logged_in": True, "reason": None}}
|
||||
)
|
||||
|
||||
self.assertEqual("已登录", tab.table.item(0, 4).text())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user