Files
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

149 lines
6.2 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest';
import { getQzoneProfile, setQzoneProfile } from '@snowluma/protocol/web/qzone';
import { RequestUtil, getBknFromCookie } from '@snowluma/protocol/web/request-util';
const cookies = { p_skey: 'PSK', skey: 'SK', uin: 'o10000' };
describe('qzone / getQzoneProfile', () => {
afterEach(() => vi.restoreAllMocks());
it('reads the selected space with the current account credentials and returns only its name and description', async () => {
const request = vi
.spyOn(RequestUtil, 'HttpGetText')
.mockResolvedValue(
'_Callback({"code":0,"data":{"spacename":"我的空间","desc":"生活 & 照片","nickname":"QQ昵称","signature":"private"}});',
);
await expect(getQzoneProfile(cookies, '10000', '20002')).resolves.toEqual({
name: '我的空间',
description: '生活 & 照片',
});
const [url, method, body, headers] = request.mock.calls[0]!;
const parsed = new URL(url);
expect(parsed.origin + parsed.pathname).toBe(
'https://h5.qzone.qq.com/proxy/domain/base.qzone.qq.com/cgi-bin/user/cgi_userinfo_get_all',
);
expect(Object.fromEntries(parsed.searchParams)).toMatchObject({
uin: '20002',
vuin: '10000',
fupdate: '1',
g_tk: getBknFromCookie(cookies),
});
expect(method).toBe('GET');
expect(body).toBe('');
expect(headers).toMatchObject({ Cookie: expect.stringContaining('p_skey=PSK') });
});
it('defaults to the current space and preserves empty fields', async () => {
const request = vi
.spyOn(RequestUtil, 'HttpGetText')
.mockResolvedValue('{"code":0,"data":{"spacename":"","desc":""}}');
await expect(getQzoneProfile(cookies, '10000')).resolves.toEqual({
name: '',
description: '',
});
expect(new URL(request.mock.calls[0]![0]).searchParams.get('uin')).toBe('10000');
});
it.each([
{},
{ code: 0 },
{ code: 0, data: { spacename: 'Name' } },
{ code: 0, data: { spacename: 42, desc: '' } },
])('rejects an incomplete profile instead of substituting empty values (%j)', async (data) => {
vi.spyOn(RequestUtil, 'HttpGetText').mockResolvedValue(JSON.stringify(data));
await expect(getQzoneProfile(cookies, '10000')).rejects.toThrow(/qzone profile/);
});
it('propagates login, access and transport failures', async () => {
vi.spyOn(RequestUtil, 'HttpGetText')
.mockResolvedValueOnce('{"code":-3000,"message":"need login"}')
.mockResolvedValueOnce('{"code":0,"subcode":-4001,"message":"denied"}')
.mockRejectedValueOnce(new Error('network down'));
await expect(getQzoneProfile(cookies, '10000')).rejects.toThrow('code=-3000');
await expect(getQzoneProfile(cookies, '10000')).rejects.toThrow('subcode=-4001');
await expect(getQzoneProfile(cookies, '10000')).rejects.toThrow('network down');
});
});
describe('qzone / setQzoneProfile', () => {
afterEach(() => vi.restoreAllMocks());
it.each([
{ profile: { name: '新名称' }, fields: { spacename: '新名称' }, bitmap: '2048' },
{ profile: { description: '' }, fields: { desc: '' }, bitmap: '4096' },
{
profile: { name: '名称 & +', description: '第一行\r\n第二行' },
fields: { spacename: '名称 & +', desc: '第一行\n第二行' },
bitmap: '6144',
},
])(
'updates only the supplied fields with bitmap $bitmap',
async ({ profile, fields, bitmap }) => {
const request = vi
.spyOn(RequestUtil, 'HttpGetText')
.mockResolvedValue('frameElement.callback({"code":0});');
await expect(setQzoneProfile(cookies, '10000', profile)).resolves.toBeUndefined();
expect(request).toHaveBeenCalledTimes(1);
const [url, method, body, headers] = request.mock.calls[0]!;
const parsed = new URL(url);
expect(parsed.origin + parsed.pathname).toBe(
'https://h5.qzone.qq.com/proxy/domain/w.qzone.qq.com/cgi-bin/user/cgi_apply_updateuserinfo_new',
);
expect(parsed.searchParams.get('g_tk')).toBe(getBknFromCookie(cookies));
expect(method).toBe('POST');
expect(Object.fromEntries(new URLSearchParams(body))).toEqual({
uin: '10000',
pageindex: '3',
fupdate: '1',
mb: bitmap,
...fields,
});
expect(headers).toMatchObject({
Cookie: expect.stringContaining('p_skey=PSK'),
'Content-Type': 'application/x-www-form-urlencoded',
});
},
);
it.each([-3001, -3002, -3000])(
'reports upstream rejection %s without retrying',
async (code) => {
const request = vi
.spyOn(RequestUtil, 'HttpGetText')
.mockResolvedValue(JSON.stringify({ code, message: 'verification required' }));
await expect(setQzoneProfile(cookies, '10000', { name: 'Name' })).rejects.toThrow(
`code=${code}`,
);
expect(request).toHaveBeenCalledTimes(1);
},
);
it('rejects a missing success code, non-zero subcode, HTML and network failures', async () => {
vi.spyOn(RequestUtil, 'HttpGetText')
.mockResolvedValueOnce('{}')
.mockResolvedValueOnce('{"code":0,"subcode":-1,"message":"denied"}')
.mockResolvedValueOnce('<html>login</html>')
.mockRejectedValueOnce(new Error('network down'));
await expect(setQzoneProfile(cookies, '10000', { name: 'Name' })).rejects.toThrow(
/qzone profile update/,
);
await expect(setQzoneProfile(cookies, '10000', { name: 'Name' })).rejects.toThrow(
'subcode=-1',
);
await expect(setQzoneProfile(cookies, '10000', { name: 'Name' })).rejects.toThrow(
'invalid response from qzone api',
);
await expect(setQzoneProfile(cookies, '10000', { name: 'Name' })).rejects.toThrow(
'network down',
);
});
});