import logging import unittest from unittest.mock import AsyncMock, patch from app.netease_qr_login import NeteaseQrLogin, _weapi_form class _Headers: def __init__(self, set_cookie=None): self.set_cookie = list(set_cookie or []) def get_all(self, name, default=None): if name.lower() == "set-cookie": return self.set_cookie return default or [] class _Response: def __init__(self, set_cookie=None): self.headers = _Headers(set_cookie) class NeteaseQrLoginTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.saved_music_u = "" self.save_callback = AsyncMock(side_effect=self._save) self.account_checker = AsyncMock(return_value={ "authenticated": True, "user_id": "10086", "nickname": "测试账号", "vip_type": 11, }) self.login = NeteaseQrLogin( api_base=lambda: "https://music.163.com", get_saved_music_u=lambda: self.saved_music_u, save_music_u=self.save_callback, account_checker=self.account_checker, logger=logging.getLogger("test-netease-qr"), ) async def _save(self, music_u, _account): self.saved_music_u = music_u async def _start(self): with patch( "app.netease_qr_login._request_weapi_json", return_value=({"code": 200, "unikey": "private-key"}, _Response()), ): return await self.login.start() def test_weapi_form_encrypts_payload(self): result = _weapi_form( {"type": 1, "csrf_token": ""}, secret_key="0123456789abcdef", ) self.assertEqual(set(result), {"params", "encSecKey"}) self.assertEqual(len(result["encSecKey"]), 256) self.assertNotIn("csrf_token", result["params"]) async def test_start_keeps_key_on_server(self): with patch( "app.netease_qr_login._request_weapi_json", return_value=({"code": 200, "unikey": "private-key"}, _Response()), ) as request_json: result = await self.login.start() self.assertEqual(result["state"], "awaiting_scan") self.assertTrue(result["has_qr_image"]) self.assertNotIn("key", result) self.assertNotIn("qr_url", result) self.assertFalse(result["credential_configured"]) self.assertTrue(request_json.call_args.args[0].endswith("/weapi/login/qrcode/unikey")) self.assertEqual(request_json.call_args.args[1], {"type": 1, "csrf_token": ""}) self.assertEqual( self.login._session["qr_url"], "http://music.163.com/login?codekey=private-key", ) async def test_poll_reports_scan_and_confirmation_states(self): await self._start() with patch( "app.netease_qr_login._request_weapi_json", side_effect=[ ({"code": 801, "message": "等待扫码"}, _Response()), ({"code": 802, "message": "待确认"}, _Response()), ], ) as request_json: waiting = await self.login.poll() confirming = await self.login.poll() self.assertEqual(waiting["state"], "awaiting_scan") self.assertEqual(confirming["state"], "awaiting_confirm") self.assertTrue( request_json.call_args_list[0].args[0].endswith( "/weapi/login/qrcode/client/login" ) ) self.assertEqual( request_json.call_args_list[0].args[1], {"key": "private-key", "type": 1, "csrf_token": ""}, ) self.save_callback.assert_not_awaited() async def test_success_validates_and_auto_saves_music_u(self): await self._start() response = _Response(["MUSIC_U=private-music-u; Path=/; HttpOnly; SameSite=None"]) with patch( "app.netease_qr_login._request_weapi_json", return_value=({"code": 803, "message": "授权登录成功"}, response), ): result = await self.login.poll() self.assertEqual(result["state"], "completed") self.assertTrue(result["credential_configured"]) self.assertEqual(result["account"]["nickname"], "测试账号") self.assertNotIn("music_u", result) self.account_checker.assert_awaited_once_with("https://music.163.com", "private-music-u") self.save_callback.assert_awaited_once() self.assertEqual(self.saved_music_u, "private-music-u") async def test_success_without_music_u_is_rejected(self): await self._start() with patch( "app.netease_qr_login._request_weapi_json", return_value=({"code": 803}, _Response()), ): result = await self.login.poll() self.assertEqual(result["state"], "failed") self.assertIn("缺少 MUSIC_U", result["message"]) self.save_callback.assert_not_awaited() if __name__ == "__main__": unittest.main()