Files
TrueGrowth/vendor/social-auto-upload/tests/test_xiaohongshu_uploader.py
Jammy 52636c91ae
Some checks failed
CI / main (push) Has been cancelled
CI / release-e2e (push) Has been cancelled
Track bundled vendor runtime sources
2026-07-07 10:05:50 +08:00

285 lines
9.1 KiB
Python

import asyncio
import os
import tempfile
import unittest
from pathlib import Path
from unittest.mock import AsyncMock, patch
import uploader.xiaohongshu_uploader.main as xhs_main
class FakeLocator:
def __init__(self, name, count=0, src=None, children=None):
self.name = name
self._count = count
self._src = src
self._children = children or {}
@property
def first(self):
return self
def locator(self, selector):
return self._children.get(selector, FakeLocator(selector))
def get_by_text(self, text, exact=False):
return self._children.get(f"text:{text}", FakeLocator(text))
def filter(self, **kwargs):
return self
def nth(self, index):
return self
async def count(self):
return self._count
async def wait_for(self, **kwargs):
return None
async def get_attribute(self, name):
if name == "src":
return self._src
return None
async def fill(self, value):
return None
async def click(self):
return None
class RecordingKeyboard:
def __init__(self):
self.actions = []
async def press(self, key):
self.actions.append(("press", key))
async def type(self, text, delay=None):
self.actions.append(("type", text, delay))
class RecordingLocator(FakeLocator):
def __init__(self, name):
super().__init__(name, count=1)
self.actions = []
async def fill(self, value):
self.actions.append(("fill", value))
async def click(self):
self.actions.append(("click",))
async def wait_for(self, **kwargs):
self.actions.append(("wait_for", kwargs))
class RecordingPage:
def __init__(self):
self.keyboard = RecordingKeyboard()
self.locators = {
'input[placeholder*="填写标题"]': RecordingLocator("title"),
'p[data-placeholder*="输入正文描述"]': RecordingLocator("desc"),
'#creator-editor-topic-container': RecordingLocator("topic-container"),
'#creator-editor-topic-container .item': RecordingLocator("topic-item"),
}
def locator(self, selector):
return self.locators[selector]
class XiaohongshuUploaderTests(unittest.TestCase):
def test_creator_urls_keep_xiaohongshu_domain_by_default(self):
with patch.dict(os.environ, {"SAU_XHS_CREATOR_BASE_URL": ""}):
self.assertEqual(
xhs_main._build_xhs_creator_url("/login"),
"https://creator.xiaohongshu.com/login",
)
def test_creator_urls_use_configured_rednote_domain(self):
with patch.dict(
os.environ,
{"SAU_XHS_CREATOR_BASE_URL": "https://creator.rednote.com/"},
):
self.assertEqual(
xhs_main._build_xhs_creator_url("/login"),
"https://creator.rednote.com/login",
)
self.assertEqual(
xhs_main._build_xhs_creator_url(
"/publish/publish?from=homepage&target=video"
),
"https://creator.rednote.com/publish/publish?from=homepage&target=video",
)
def test_find_xhs_qrcode_locator_prefers_scan_sibling_inside_login_box(self):
qrcode_locator = FakeLocator("qrcode", count=1, src="data:image/png;base64,abc")
scan_text_locator = FakeLocator(
"scan-text",
count=1,
children={
"xpath=..//following-sibling::div//img": qrcode_locator,
},
)
login_box_locator = FakeLocator(
"login-box",
count=1,
children={
"div:has-text('扫一扫')": scan_text_locator,
"text:APP扫一扫登录": scan_text_locator,
},
)
page = FakeLocator(
"page",
children={
"div[class*='login-box']": login_box_locator,
".login-box-container": login_box_locator,
},
)
locator = asyncio.run(xhs_main._find_xhs_qrcode_locator(page))
self.assertIs(locator, qrcode_locator)
def test_setup_returns_detail_when_cookie_invalid_without_handle(self):
with patch("uploader.xiaohongshu_uploader.main.os.path.exists", return_value=False):
result = asyncio.run(
xhs_main.xiaohongshu_setup(
"missing.json",
handle=False,
return_detail=True,
)
)
self.assertFalse(result["success"])
self.assertEqual(result["status"], "cookie_invalid")
def test_setup_uses_login_flow_when_handle_is_true(self):
login_result = {
"success": True,
"status": "success",
"message": "ok",
"account_file": "account.json",
"qrcode": {"image_path": "qrcode.png"},
"current_url": "https://creator.xiaohongshu.com/",
}
with patch("uploader.xiaohongshu_uploader.main.os.path.exists", return_value=False):
with patch(
"uploader.xiaohongshu_uploader.main.xiaohongshu_cookie_gen",
new=AsyncMock(return_value=login_result),
) as mock_login:
result = asyncio.run(
xhs_main.xiaohongshu_setup(
"account.json",
handle=True,
return_detail=True,
)
)
self.assertTrue(result["success"])
mock_login.assert_awaited_once()
def test_video_validate_upload_args_normalizes_video_and_thumbnail(self):
with tempfile.TemporaryDirectory() as tmp_dir:
video_path = Path(tmp_dir) / "demo.mp4"
thumbnail_path = Path(tmp_dir) / "demo.png"
cookie_path = Path(tmp_dir) / "account.json"
video_path.write_bytes(b"video")
thumbnail_path.write_bytes(b"image")
cookie_path.write_text("{}")
app = xhs_main.XiaoHongShuVideo(
title="demo",
file_path=str(video_path),
tags=["xhs"],
publish_date=0,
account_file=str(cookie_path),
thumbnail_path=str(thumbnail_path),
)
with patch(
"uploader.xiaohongshu_uploader.main.cookie_auth",
new=AsyncMock(return_value=True),
):
asyncio.run(app.validate_upload_args())
self.assertTrue(app.file_path.endswith("demo.mp4"))
self.assertTrue(app.thumbnail_path.endswith("demo.png"))
def test_note_uploader_exists_and_validates_required_fields(self):
note_cls = getattr(xhs_main, "XiaoHongShuNote")
app = note_cls(
image_paths=[],
note="",
tags=[],
publish_date=0,
account_file="account.json",
)
with patch.object(app, "validate_base_args", new=AsyncMock(return_value=None)):
with self.assertRaises(ValueError):
asyncio.run(app.validate_upload_args())
def test_video_fill_meta_uses_desc_then_first_tag(self):
app = xhs_main.XiaoHongShuVideo(
title="标题内容",
file_path="demo.mp4",
tags=["话题1"],
publish_date=0,
account_file="account.json",
desc="描述内容",
)
page = RecordingPage()
asyncio.run(app.fill_meta(page))
self.assertEqual(
page.locators['input[placeholder*="填写标题"]'].actions,
[("fill", "标题内容")],
)
self.assertEqual(
page.locators['p[data-placeholder*="输入正文描述"]'].actions,
[("click",)],
)
self.assertIn(("type", "描述内容", None), page.keyboard.actions)
self.assertIn(("type", "#话题1", 30), page.keyboard.actions)
self.assertEqual(
page.locators['#creator-editor-topic-container .item'].actions,
[("wait_for", {"state": "visible", "timeout": 2000}), ("click",)],
)
def test_video_fill_meta_can_fill_first_tag_without_desc(self):
app = xhs_main.XiaoHongShuVideo(
title="标题内容",
file_path="demo.mp4",
tags=["话题1"],
publish_date=0,
account_file="account.json",
)
page = RecordingPage()
asyncio.run(app.fill_meta(page))
self.assertEqual(
page.locators['p[data-placeholder*="输入正文描述"]'].actions,
[("click",)],
)
self.assertNotIn(("type", "", None), page.keyboard.actions)
self.assertIn(("type", "#话题1", 30), page.keyboard.actions)
def test_note_title_defaults_do_not_override_explicit_title(self):
app = xhs_main.XiaoHongShuNote(
image_paths=["a.png"],
note="正文",
tags=[],
publish_date=0,
account_file="account.json",
title="显式标题",
desc="图文正文",
)
self.assertEqual(app.title, "显式标题")
self.assertEqual(app.desc, "图文正文")
if __name__ == "__main__":
unittest.main()