NapCatQQ source snapshot
This commit is contained in:
commit
f954252a34
1111 files changed
+139867
No files matched your search
@@ -0,0 +1,302 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import {
|
||||
buildQzoneBanBody,
|
||||
buildQzoneProfileBody,
|
||||
buildQzoneRightBody,
|
||||
normalizeQzoneRight,
|
||||
parseQzoneProfile,
|
||||
parseQzoneWebResponse,
|
||||
} from '@/napcat-core/data/qzone-web';
|
||||
|
||||
describe('Qzone management response parsing', () => {
|
||||
test.each([
|
||||
'{"code":0,"subcode":0,"data":{"field":"raw"}}',
|
||||
'_Callback({"code":0,"subcode":0,"data":{"field":"raw"}});',
|
||||
'callback({"code":0,"subcode":0,"data":{"field":"raw"}})',
|
||||
'_preloadCallback({"code":0,"subcode":0,"data":{"field":"raw"}});',
|
||||
'back({"code":0,"subcode":0,"data":{"field":"raw"}});',
|
||||
'frameElement.callback({"code":0,"subcode":0,"data":{"field":"raw"}});',
|
||||
'<script>frameElement.callback({"code":0,"subcode":0,"data":{"field":"raw"}});</script>',
|
||||
])('parses only an inert known response envelope (%s)', (text) => {
|
||||
expect(parseQzoneWebResponse(text, true)).toEqual({ code: 0, subcode: 0, data: { field: 'raw' } });
|
||||
});
|
||||
|
||||
test('repairs observed name string fragments without treating a name alone as success', () => {
|
||||
expect(parseQzoneWebResponse('frameElement.callback({"code":0,"subcode":0,"data":{name:"Ack"}});'))
|
||||
.toEqual({ code: 0, subcode: 0, data: { name: 'Ack' } });
|
||||
expect(() => parseQzoneWebResponse('frameElement.callback({name:"Ack"});')).toThrow('成功状态');
|
||||
expect(parseQzoneWebResponse('callback({"code":0,"data":{name:"Other"}});'))
|
||||
.toEqual({ code: 0, data: { name: 'Other' } });
|
||||
expect(parseQzoneWebResponse('frameElement.callback({name:"Ack","code":0,"subcode":0});'))
|
||||
.toEqual({ name: 'Ack', code: 0, subcode: 0 });
|
||||
expect(parseQzoneWebResponse('frameElement.callback({"code":0,"data":{name:"Ack","extra":true}});'))
|
||||
.toEqual({ code: 0, data: { name: 'Ack', extra: true } });
|
||||
expect(parseQzoneWebResponse('frameElement.callback({name:"Other","code":0});'))
|
||||
.toEqual({ name: 'Other', code: 0 });
|
||||
});
|
||||
|
||||
test.each([
|
||||
'{}',
|
||||
'null',
|
||||
'[]',
|
||||
'{"code":"0"}',
|
||||
'{"code":0,"subcode":null}',
|
||||
'{"code":-1,"subcode":0}',
|
||||
'{"code":0,"subcode":-1}',
|
||||
'{"code":0,"retcode":-1}',
|
||||
'{"code":-1,"code":0}',
|
||||
'{"code":-1,"\\u0063ode":0}',
|
||||
'unknownCallback({"code":0});',
|
||||
'alert("prefix");callback({"code":0});',
|
||||
'callback({"code":0});alert("suffix");',
|
||||
'<html><script>callback({"code":0});</script></html>',
|
||||
'<html>login</html>',
|
||||
'<script>{"code":0}</script>',
|
||||
'<script>alert("prefix");callback({"code":0});</script>',
|
||||
])('rejects ambiguous, failed or executable wrappers (%s)', (text) => {
|
||||
expect(() => parseQzoneWebResponse(text)).toThrow();
|
||||
});
|
||||
|
||||
test('requires explicit code=0 for profile routes and never exposes private response text', () => {
|
||||
expect(parseQzoneWebResponse('{"subcode":0}')).toEqual({ subcode: 0 });
|
||||
expect(() => parseQzoneWebResponse('{"subcode":0}', true)).toThrow('成功状态');
|
||||
let error: unknown;
|
||||
try {
|
||||
parseQzoneWebResponse('{"code":-3000,"message":"p_skey=private-secret"}');
|
||||
} catch (value) {
|
||||
error = value;
|
||||
}
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect((error as Error).message).toBe('QQ空间接口拒绝请求 (code=-3000)');
|
||||
expect((error as Error).message).not.toContain('private-secret');
|
||||
});
|
||||
|
||||
test('preserves exact profile text and does not invent missing fields', () => {
|
||||
expect(parseQzoneProfile({ code: 0, data: { spacename: '', desc: ' 第一行\n第二行 ', signature: 'not-returned' } }))
|
||||
.toEqual({ name: '', description: ' 第一行\n第二行 ' });
|
||||
for (const response of [{}, { code: 0 }, { code: 0, data: {} }, { code: 0, data: { spacename: 1, desc: '' } }]) {
|
||||
expect(() => parseQzoneProfile(response)).toThrow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Qzone profile and blacklist form bodies', () => {
|
||||
test.each([
|
||||
{ profile: { name: '名称 & +' }, fields: { spacename: '名称 & +' }, bitmap: '2048' },
|
||||
{ profile: { description: '' }, fields: { desc: '' }, bitmap: '4096' },
|
||||
{ profile: { name: '', description: '一\r\n二\r' }, fields: { spacename: '', desc: '一\n二' }, bitmap: '6144' },
|
||||
])('only submits supplied profile fields (%j)', ({ profile, fields, bitmap }) => {
|
||||
const body = new URLSearchParams(buildQzoneProfileBody('10001', profile));
|
||||
expect(Object.fromEntries(body)).toEqual({ uin: '10001', pageindex: '3', fupdate: '1', mb: bitmap, ...fields });
|
||||
expect(body.has('signature')).toBe(false);
|
||||
});
|
||||
|
||||
test('rejects invalid profile input and preserves omitted fields', () => {
|
||||
expect(() => buildQzoneProfileBody('10001', {})).toThrow();
|
||||
expect(() => buildQzoneProfileBody('10001', { name: undefined })).toThrow();
|
||||
expect(() => buildQzoneProfileBody('10001', { name: 'x'.repeat(8193) })).toThrow();
|
||||
expect(() => buildQzoneProfileBody('10001', { name: 42 } as never)).toThrow();
|
||||
expect(() => buildQzoneProfileBody('10001', { description: null } as never)).toThrow();
|
||||
expect(() => buildQzoneProfileBody('invalid', { name: 'space' })).toThrow();
|
||||
expect(() => buildQzoneProfileBody('10001', { signature: 'not-a-supported-field' } as never)).toThrow();
|
||||
expect(new URLSearchParams(buildQzoneProfileBody('10001', { name: 'space' })).has('desc')).toBe(false);
|
||||
});
|
||||
|
||||
test('builds ban and unban independently with an exact decimal target', () => {
|
||||
for (const enable of [true, false]) {
|
||||
expect(Object.fromEntries(new URLSearchParams(buildQzoneBanBody('10001', '9007199254740993', enable)))).toEqual({
|
||||
uin: '10001', act_uin: '9007199254740993', action: enable ? '1' : '2', fupdate: '1', qzreferrer: 'https://user.qzone.qq.com/10001/main',
|
||||
});
|
||||
}
|
||||
expect(() => buildQzoneBanBody('10001', 'bad', true)).toThrow();
|
||||
expect(() => buildQzoneBanBody('10001', '10002', 'false' as never)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
function detail (overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return { code: 0, subcode: 0, tid: 'T1', uin: '10001', content: ' original\ntext ', ...overrides };
|
||||
}
|
||||
|
||||
function observedEmptyForwardFields (): Record<string, unknown> {
|
||||
return { rt_tid: '', rt_uin: 0, rt_con: { conlist: null, content: '' }, rt_source: 0 };
|
||||
}
|
||||
|
||||
describe('Qzone permission update preflight', () => {
|
||||
test('validates rights and all target IDs without lossy conversion or silent omission', () => {
|
||||
expect(normalizeQzoneRight(16, ['10002', '10002', '9007199254740993']))
|
||||
.toEqual({ right: 16, targetUins: ['10002', '9007199254740993'] });
|
||||
for (const right of [0, 2, 3, 5, 256, NaN, Infinity]) expect(() => normalizeQzoneRight(right)).toThrow();
|
||||
expect(() => normalizeQzoneRight(16)).toThrow('target_uins');
|
||||
expect(() => normalizeQzoneRight(128, [])).toThrow('target_uins');
|
||||
expect(() => normalizeQzoneRight(128, ['10002', 'bad'])).toThrow();
|
||||
expect(() => normalizeQzoneRight(1, [10002] as never)).toThrow();
|
||||
});
|
||||
|
||||
test('rebuilds pure text with exact content, ownership, defaults and selective visibility', () => {
|
||||
const body = new URLSearchParams(buildQzoneRightBody(detail(), '10001', 'T1', 128, ['10002', '10002']));
|
||||
expect(Object.fromEntries(body)).toEqual({
|
||||
syn_tweet_verson: '1',
|
||||
tid: 'T1',
|
||||
paramstr: '1',
|
||||
pic_template: '',
|
||||
richtype: '',
|
||||
richval: '',
|
||||
special_url: '',
|
||||
subrichtype: '',
|
||||
pic_bo: '',
|
||||
con: ' original\ntext ',
|
||||
feedversion: '1',
|
||||
ver: '1',
|
||||
ugc_right: '128',
|
||||
to_sign: '0',
|
||||
ugcright_id: 'T1',
|
||||
hostuin: '10001',
|
||||
code_version: '1',
|
||||
format: 'fs',
|
||||
qzreferrer: 'https://user.qzone.qq.com/10001',
|
||||
allow_uins: '10002',
|
||||
});
|
||||
expect(new URLSearchParams(buildQzoneRightBody(detail(), '10001', 'T1', 64, ['10002'])).has('allow_uins')).toBe(false);
|
||||
expect(new URLSearchParams(buildQzoneRightBody(detail({ richtype: '', subrichtype: '' }), '10001', 'T1', 4)).get('richtype')).toBe('');
|
||||
});
|
||||
|
||||
test('accepts the observed plain-text forward placeholder and retains subtype 2', () => {
|
||||
const value = detail({ ...observedEmptyForwardFields(), t1_subtype: 2 });
|
||||
const body = new URLSearchParams(buildQzoneRightBody(value, '10001', 'T1', 4));
|
||||
expect(Object.fromEntries(body)).toMatchObject({ con: ' original\ntext ', richtype: '', richval: '', subrichtype: '2' });
|
||||
expect(value['rt_con']).toEqual({ conlist: null, content: '' });
|
||||
});
|
||||
|
||||
test('accepts the same observed placeholder on complete image posts without dropping image data', () => {
|
||||
const value = detail({
|
||||
...observedEmptyForwardFields(),
|
||||
t1_subtype: 1,
|
||||
pic: [{ pic_id: ',SYNTHETIC-ALBUM,SYNTHETIC-LOCATION', pictype: 3, height: 96, width: 128, smallurl: 'https://photo.example/a?bo=metadata' }],
|
||||
});
|
||||
const body = new URLSearchParams(buildQzoneRightBody(value, '10001', 'T1', 64));
|
||||
expect(Object.fromEntries(body)).toMatchObject({
|
||||
con: ' original\ntext ',
|
||||
richtype: '1',
|
||||
subrichtype: '1',
|
||||
richval: ',SYNTHETIC-ALBUM,SYNTHETIC-LOCATION,SYNTHETIC-LOCATION,3,96,128,,0,0',
|
||||
pic_bo: 'metadata\tmetadata',
|
||||
});
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ rt_con: { conlist: null, content: ' ' } },
|
||||
{ rt_con: { conlist: null, content: 'forwarded text' } },
|
||||
{ rt_con: { conlist: [], content: '' } },
|
||||
{ rt_con: { conlist: [{}], content: '' } },
|
||||
{ rt_con: { content: '' } },
|
||||
{ rt_con: { conlist: null } },
|
||||
{ rt_con: { conlist: null, content: '', unknown: 0 } },
|
||||
{ rt_con: [{ conlist: null, content: '' }] },
|
||||
{ rt_con: 'nonempty' },
|
||||
{ rt_tid: 'forwarded' },
|
||||
{ rt_uin: 10002 },
|
||||
{ rt_source: 1 },
|
||||
{ rt_tid: undefined },
|
||||
{ rt_uin: '0' },
|
||||
{ rt_source: undefined },
|
||||
{ video: { vid: 'video-id' } },
|
||||
{ music: { url: 'https://music.example/song' } },
|
||||
{ forward: { tid: 'forwarded' } },
|
||||
{ origin: { tid: 'original' } },
|
||||
])('does not generalize the observed empty forward object to other content (%j)', (overrides) => {
|
||||
const value = detail({ ...observedEmptyForwardFields(), t1_subtype: 1, ...overrides });
|
||||
expect(() => buildQzoneRightBody(value, '10001', 'T1', 4)).toThrow('媒体或转发内容');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ pic: [{ pic_id: ',ALBUM,LOCATION', pictype: 3, height: 96, width: 128 }] },
|
||||
{ richtype: 1 },
|
||||
{ richtype: 2 },
|
||||
{ richtype: '' },
|
||||
{ subrichtype: 2 },
|
||||
{ t1_subtype: 9 },
|
||||
{ rt_con: undefined },
|
||||
])('restricts subtype 2 to the observed ordinary text layout (%j)', (overrides) => {
|
||||
const value = detail({ ...observedEmptyForwardFields(), t1_subtype: 2, ...overrides });
|
||||
expect(() => buildQzoneRightBody(value, '10001', 'T1', 4)).toThrow();
|
||||
});
|
||||
|
||||
test('preserves all conlist segments including whitespace instead of using a truncated summary', () => {
|
||||
const value = detail({ content: 'summary', conlist: [{ con: ' \nfirst' }, { con: ' ' }, { con: 'last\n ' }] });
|
||||
expect(new URLSearchParams(buildQzoneRightBody(value, '10001', 'T1', 4)).get('con')).toBe(' \nfirst last\n ');
|
||||
expect(new URLSearchParams(buildQzoneRightBody(detail({ content: '' }), '10001', 'T1', 4)).get('con')).toBe('');
|
||||
expect(() => buildQzoneRightBody(detail({ conlist: [{ con: 'kept' }, {}] }), '10001', 'T1', 4)).toThrow('分段不完整');
|
||||
expect(() => buildQzoneRightBody(detail({ content: undefined }), '10001', 'T1', 4)).toThrow('完整正文');
|
||||
});
|
||||
|
||||
test('preserves image order, complete richvals, bo alignment and version metadata', () => {
|
||||
const value = detail({
|
||||
conlist: [{ con: 'image text' }],
|
||||
richtype: 1,
|
||||
t1_subtype: 1,
|
||||
pic_template: 'template',
|
||||
feedversion: 3,
|
||||
ver: '2',
|
||||
code_version: 5,
|
||||
to_sign: 1,
|
||||
ugcright_id: 'right-key',
|
||||
pic: [
|
||||
{ pic_id: 'X,ALBUM1,LLOC1,extra', pictype: 22, height: 800, width: 600, url1: 'https://photo.example/a?bo=abc%2Cdef' },
|
||||
{ pic_id: 'X,ALBUM2,LLOC2,extra', type: '23', b_height: '400', b_width: '300', smallurl: 'https://photo.example/b?bo=second' },
|
||||
],
|
||||
});
|
||||
const body = new URLSearchParams(buildQzoneRightBody(value, '10001', 'T1', 64));
|
||||
expect(body.get('richval')).toBe(',ALBUM1,LLOC1,LLOC1,22,800,600,,0,0\t,ALBUM2,LLOC2,LLOC2,23,400,300,,0,0');
|
||||
expect(body.get('pic_bo')).toBe('abc,def,second\tabc,def,second');
|
||||
expect(Object.fromEntries(body)).toMatchObject({ con: 'image text', pic_template: 'template', feedversion: '3', ver: '2', code_version: '5', to_sign: '1', ugcright_id: 'right-key' });
|
||||
expect(value['ugc_right']).toBeUndefined();
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ pic: [{ pic_id: 'X,A,L', height: 100, width: 100 }, {}], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'broken' }], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'X, ,L', height: 100, width: 100 }], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'X,A, ', height: 100, width: 100 }], richtype: 1 },
|
||||
{ pic: [], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'X,A,L' }], richtype: 0 },
|
||||
{ pic: [{ pic_id: 'X,A,L', height: 'unknown', width: 100 }], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'X,A,L', height: null, b_height: 100, width: 100 }], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'X,A,L', height: 100, width: 100, url1: 'https://photo.example/a?bo=one' }, { pic_id: 'X,A,L2', height: 100, width: 100 }], richtype: 1 },
|
||||
{ pic: [{ pic_id: 'X,A,L', height: 100, width: 100, url1: 'https://photo.example/a?bo=%GG' }], richtype: 1 },
|
||||
{ richval: 'unknown-media' },
|
||||
{ richtype: 2 },
|
||||
{ t1_subtype: 9 },
|
||||
{ video: { vid: 'video-id' } },
|
||||
{ music: { url: 'https://music.example/song' } },
|
||||
{ rt_tid: 'forwarded-tid' },
|
||||
{ special_url: 'https://example.com/shared' },
|
||||
])('rejects incomplete or unsupported media instead of partially resubmitting it (%j)', (overrides) => {
|
||||
expect(() => buildQzoneRightBody(detail(overrides), '10001', 'T1', 64)).toThrow();
|
||||
});
|
||||
|
||||
test('requires every existing image to retain positive known dimensions', () => {
|
||||
for (const picture of [
|
||||
{ pic_id: 'X,A,L', width: 100 },
|
||||
{ pic_id: 'X,A,L', height: 100 },
|
||||
{ pic_id: 'X,A,L', height: 0, width: 100 },
|
||||
{ pic_id: 'X,A,L', height: 100, width: '0' },
|
||||
]) {
|
||||
expect(() => buildQzoneRightBody(detail({ richtype: 1, pic: [picture] }), '10001', 'T1', 64)).toThrow();
|
||||
}
|
||||
expect(() => buildQzoneRightBody(detail({
|
||||
richtype: 1,
|
||||
pic: [
|
||||
{ pic_id: 'X,A,L1', height: 100, width: 100 },
|
||||
{ pic_id: 'X,A,L2', width: 100 },
|
||||
],
|
||||
}), '10001', 'T1', 64)).toThrow('尺寸不完整');
|
||||
});
|
||||
|
||||
test('requires the exact requested tid and current-account owner before returning a body', () => {
|
||||
expect(() => buildQzoneRightBody(detail({ tid: 'other-tid' }), '10001', 'T1', 64)).toThrow('不匹配');
|
||||
expect(() => buildQzoneRightBody(detail({ uin: '10002' }), '10001', 'T1', 64)).toThrow('自己的说说');
|
||||
expect(() => buildQzoneRightBody(detail({ uin: undefined }), '10001', 'T1', 64)).toThrow('自己的说说');
|
||||
expect(() => buildQzoneRightBody(detail({ code: -1, subcode: 0 }), '10001', 'T1', 64)).toThrow();
|
||||
expect(new URLSearchParams(buildQzoneRightBody(detail({ uin: 10001 }), '10001', 'T1', 4)).get('hostuin')).toBe('10001');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user