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
145 lines
5.9 KiB
TypeScript
145 lines
5.9 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
import { getQzoneMsgDetail } from '@snowluma/protocol/web/qzone';
|
|
import { RequestUtil, getBknFromCookie } from '@snowluma/protocol/web/request-util';
|
|
|
|
const cookies = { p_skey: 'PSK', skey: 'SK', uin: 'o10000' };
|
|
// Synthetic data using Qzone-API's documented commentlist / list_3 wire fields.
|
|
const reply = { tid: 1, uin: 10000, name: 'Bot', create_time: 1700000002, content: 'Thanks!' };
|
|
const comment = {
|
|
tid: 0,
|
|
uin: '20002',
|
|
name: 'Reader',
|
|
create_time: '1700000001',
|
|
content: 'Hello\n[em]e100[/em]',
|
|
list_3: [reply],
|
|
};
|
|
const detail = { code: 0, tid: 'T1', content: 'Post', cmtnum: 12, commentlist: [comment] };
|
|
|
|
describe('qzone / getQzoneMsgDetail', () => {
|
|
afterEach(() => vi.restoreAllMocks());
|
|
|
|
it('reads a comment page through the authenticated detail CGI and maps replies', async () => {
|
|
const request = vi
|
|
.spyOn(RequestUtil, 'HttpGetText')
|
|
.mockResolvedValue(`_preloadCallback(${JSON.stringify(detail)});`);
|
|
const result = await getQzoneMsgDetail(cookies, '30003', 'T1', 5, 10);
|
|
expect(result).toEqual({
|
|
tid: 'T1',
|
|
content: 'Post',
|
|
comment_num: 12,
|
|
comments: [
|
|
{
|
|
comment_id: '0',
|
|
uin: 20002,
|
|
name: 'Reader',
|
|
time: 1700000001,
|
|
content: 'Hello\n[em]e100[/em]',
|
|
replies: [
|
|
{
|
|
comment_id: '1',
|
|
parent_comment_id: '0',
|
|
parent_comment_uin: 20002,
|
|
uin: 10000,
|
|
name: 'Bot',
|
|
time: 1700000002,
|
|
content: 'Thanks!',
|
|
},
|
|
],
|
|
},
|
|
],
|
|
});
|
|
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/taotao.qq.com/cgi-bin/emotion_cgi_msgdetail_v6',
|
|
);
|
|
expect(Object.fromEntries(parsed.searchParams)).toMatchObject({
|
|
uin: '30003',
|
|
tid: 'T1',
|
|
pos: '5',
|
|
num: '10',
|
|
not_trunc_con: '1',
|
|
g_tk: getBknFromCookie(cookies),
|
|
});
|
|
expect(method).toBe('GET');
|
|
expect(body).toBe('');
|
|
expect(headers).toMatchObject({ Cookie: expect.stringContaining('p_skey=PSK') });
|
|
});
|
|
|
|
it.each([[undefined], [null], [[]]])(
|
|
'accepts an empty comment page (%j)',
|
|
async (commentlist) => {
|
|
const request = vi
|
|
.spyOn(RequestUtil, 'HttpGetText')
|
|
.mockResolvedValue(
|
|
JSON.stringify({ ...detail, content: '', cmtnum: 0, commentlist }),
|
|
);
|
|
await expect(getQzoneMsgDetail(cookies, '10000', 'T1')).resolves.toEqual({
|
|
tid: 'T1',
|
|
content: '',
|
|
comment_num: 0,
|
|
comments: [],
|
|
});
|
|
const query = new URL(request.mock.calls[0]![0]).searchParams;
|
|
expect(query.get('pos')).toBe('0');
|
|
expect(query.get('num')).toBe('20');
|
|
},
|
|
);
|
|
|
|
it('preserves the total when a page is empty and normalizes absent replies', async () => {
|
|
vi.spyOn(RequestUtil, 'HttpGetText')
|
|
.mockResolvedValueOnce(JSON.stringify({ ...detail, commentlist: [] }))
|
|
.mockResolvedValueOnce(
|
|
JSON.stringify({ ...detail, commentlist: [{ ...comment, list_3: null }] }),
|
|
);
|
|
await expect(getQzoneMsgDetail(cookies, '10000', 'T1', 20)).resolves.toMatchObject({
|
|
comment_num: 12,
|
|
comments: [],
|
|
});
|
|
const result = await getQzoneMsgDetail(cookies, '10000', 'T1');
|
|
expect(result.comments[0]!.replies).toEqual([]);
|
|
});
|
|
|
|
it.each([{ code: -3000, subcode: 0 }, { code: 0, subcode: -4001 }, { subcode: -4001 }])(
|
|
'propagates remote failure %j',
|
|
async (codes) => {
|
|
vi.spyOn(RequestUtil, 'HttpGetText').mockResolvedValue(
|
|
JSON.stringify({ ...codes, message: 'denied' }),
|
|
);
|
|
await expect(getQzoneMsgDetail(cookies, '10000', 'T1')).rejects.toThrow(
|
|
/qzone msg detail failed: .*denied/,
|
|
);
|
|
},
|
|
);
|
|
|
|
it.each([
|
|
{},
|
|
{ ...detail, tid: undefined },
|
|
{ ...detail, tid: 'OTHER' },
|
|
{ ...detail, code: '0' },
|
|
{ ...detail, content: {} },
|
|
{ ...detail, cmtnum: -1 },
|
|
{ ...detail, commentlist: {} },
|
|
{ ...detail, commentlist: [null] },
|
|
{ ...detail, commentlist: [{ ...comment, uin: 'bad' }] },
|
|
{ ...detail, commentlist: [{ ...comment, tid: {} }] },
|
|
{ ...detail, commentlist: [{ ...comment, create_time: 'yesterday' }] },
|
|
{ ...detail, commentlist: [{ ...comment, list_3: {} }] },
|
|
{ ...detail, commentlist: [{ ...comment, list_3: [{ ...reply, content: {} }] }] },
|
|
])('rejects malformed data instead of returning an empty success (%#)', async (data) => {
|
|
vi.spyOn(RequestUtil, 'HttpGetText').mockResolvedValue(JSON.stringify(data));
|
|
await expect(getQzoneMsgDetail(cookies, '10000', 'T1')).rejects.toThrow(/qzone msg detail/);
|
|
});
|
|
|
|
it('propagates transport and JSONP parse errors', async () => {
|
|
vi.spyOn(RequestUtil, 'HttpGetText')
|
|
.mockRejectedValueOnce(new Error('network down'))
|
|
.mockResolvedValueOnce('<html>login</html>');
|
|
await expect(getQzoneMsgDetail(cookies, '10000', 'T1')).rejects.toThrow('network down');
|
|
await expect(getQzoneMsgDetail(cookies, '10000', 'T1')).rejects.toThrow(
|
|
'invalid response from qzone api',
|
|
);
|
|
});
|
|
});
|