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
149 lines
6.2 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|