SnowLuma v2.4.2 source snapshot
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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,341 @@
import { afterEach, describe, expect, it, vi } from 'vitest';
import type { JsonObject } from '@snowluma/common/json';
import * as qzone from '@snowluma/protocol/web/qzone-public';
import { RequestUtil, getBknFromCookie } from '@snowluma/protocol/web/request-util';
const cookies = { p_skey: 'test-pskey', uin: 'o10001' };
const union: JsonObject = {
uf_key: 'feed-1',
ugc_id: '9007199254740999',
feed_id: 'feed-id',
ext_ids: [{ numeric: '9007199254740999' }],
};
const feed: JsonObject = {
cell_common: { union_id: union, feed_type: 1, domain_type: 3, cur_like_key: 'like-key' },
cell_feed_transfer: { opaque: 'transfer' },
cell_user_info: { user: { uid: 'author-uid', uin: '20002', nick: '作者' } },
cell_unknown: { keep: true },
};
const user = { uin: '10001', uid: 'self-uid', nick: '我' };
const comment = {
id: 'comment-1',
user: { uin: '20002', uid: 'comment-uid', nick: '' },
reply_attach_info: 'reply-first',
opaque: true,
};
const reply = { id: 'reply-1', user: { uin: '30003', uid: 'reply-uid', nick: '' } };
const baseParams = { union_id: union, cell_feed_transfer: feed.cell_feed_transfer };
function mockRequest(data: JsonObject = {}) {
return vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue({ retcode: 0, data });
}
function body(request: ReturnType<typeof mockRequest>, index = 0): JsonObject {
return request.mock.calls[index]![2] as JsonObject;
}
afterEach(() => vi.restoreAllMocks());
describe('public Qzone reader', () => {
it('uses the JSON gateway with the current account cookies and the method oidb headers', async () => {
const flags = { public_qzone_entrance_switch: true, can_upvote: true, unknown_flag: false };
const request = mockRequest({
results: [
{ uin: '20002', entrances: {} },
{ uin: '10001', entrances: flags },
],
});
await expect(qzone.getQzonePublicCapabilities(cookies, '10001')).resolves.toEqual(flags);
const [url, method, , headers] = request.mock.calls[0]!;
expect(url).toBe(
`https://h5.qzone.qq.com/http2rpc/gotrpc/v1/trpc.public_qzone.entrance_ctrl.EntranceCtrl/BatchEntranceWhitelist?g_tk=${getBknFromCookie(cookies)}`,
);
expect(method).toBe('POST');
expect(body(request)).toEqual({ uins: ['10001'], entrance_types: [1, 15] });
expect(headers).toEqual({
Cookie: 'p_skey=test-pskey; uin=o10001',
'Content-Type': 'application/json',
Referer: 'https://h5.qzone.qq.com/pc-qzone/index.html',
'x-web-qua': 'V1_HT5_QZ_9.9.32_001_IDC_B',
'x-oidb': JSON.stringify({ uint32_command: '0x9741', uint32_service_type: '0' }),
});
});
it('continues short and ad-only pages using the full last non-ad union and server has_more', async () => {
vi.spyOn(Date, 'now').mockReturnValue(12345);
const ad = { cell_common: { feed_type: 3, union_id: { uf_key: 'ad' } } };
const request = mockRequest()
.mockResolvedValueOnce({
retcode: 0,
data: { feeds: [{ feed }], has_more: true, attach_info: 'page-2', extra: 'keep' },
})
.mockResolvedValueOnce({
retcode: 0,
data: { feeds: [{ feed: ad }], has_more: true, attach_info: 'page-3' },
})
.mockResolvedValueOnce({ retcode: 0, data: { feeds: [], has_more: false } });
const first = await qzone.getQzonePublicFeeds(cookies, '10001');
expect(first).toMatchObject({ feeds: [feed], has_more: true, extra: 'keep' });
expect(body(request)).toEqual({ refresh_id: '10001_12345' });
const second = await qzone.getQzonePublicFeeds(cookies, '10001', first.next_cursor!);
expect(body(request, 1)).toEqual({
refresh_id: '10001_12345',
attach_info: 'page-2',
last_client_feed_union_id: union,
});
const last = await qzone.getQzonePublicFeeds(cookies, '10001', second.next_cursor!);
expect(body(request, 2)).toEqual({
refresh_id: '10001_12345',
attach_info: 'page-3',
last_client_feed_union_id: union,
});
expect(last).toEqual({ feeds: [], has_more: false, next_cursor: null });
expect(request).toHaveBeenCalledTimes(3);
});
it('preserves unknown feed cells without using them as pagination anchors', async () => {
const unknown = { cell_common: { feed_type: 4 }, cell_unknown: { keep: true } };
const request = mockRequest({
feeds: [{ feed }, { feed: unknown }],
has_more: true,
attach_info: 'next',
});
const first = await qzone.getQzonePublicFeeds(cookies, '10001');
expect(first.feeds).toEqual([feed, unknown]);
request.mockResolvedValueOnce({
retcode: 0,
data: { feeds: [{ feed: unknown }], has_more: true, attach_info: 'after-unknown' },
});
const next = await qzone.getQzonePublicFeeds(cookies, '10001', first.next_cursor!);
request.mockResolvedValueOnce({ retcode: 0, data: { feeds: [], has_more: false } });
await qzone.getQzonePublicFeeds(cookies, '10001', next.next_cursor!);
expect(body(request, 2).last_client_feed_union_id).toEqual(union);
});
it('rejects malformed or cross-account cursors before a network request', async () => {
const request = mockRequest({ feeds: [{ feed }], has_more: true, attach_info: 'next' });
const first = await qzone.getQzonePublicFeeds(cookies, '10001');
request.mockClear();
for (const cursor of [
'',
'!',
'a'.repeat(65537),
Buffer.from('{}').toString('base64url'),
first.next_cursor!,
]) {
await expect(qzone.getQzonePublicFeeds(cookies, '20002', cursor)).rejects.toThrow(
/cursor/,
);
}
expect(request).not.toHaveBeenCalled();
});
it.each([
{ feeds: [], has_more: true },
{ feeds: [{ feed: null }], has_more: false },
{ feeds: null, has_more: false },
{ feeds: [], has_more: 1 },
])('reports invalid upstream page data (%j)', async (data) => {
mockRequest(data);
await expect(qzone.getQzonePublicFeeds(cookies, '10001')).rejects.toThrow(
/GetPublicRecomFeeds/,
);
});
it('preserves detail/comment/reply result fields and forwards their separate pagination state', async () => {
const detail = {
feed: { feed: { ...feed, cell_comment: { comments: [comment] } } },
attach_info: 'comments-next',
has_more: true,
};
const request = mockRequest(detail);
await expect(qzone.getQzonePublicFeedDetail(cookies, feed)).resolves.toEqual(detail);
expect(body(request)).toEqual({ ...baseParams, feed_action_domain_type: 3, from_page: 1 });
request.mockResolvedValueOnce({
retcode: 0,
data: {
cell_comment: { comments: [comment] },
attach_info: 'comments-end',
has_more: false,
},
});
await qzone.getQzonePublicComments(cookies, feed, 'comments-next');
expect(body(request, 1)).toEqual({ ...baseParams, attach_info: 'comments-next' });
request.mockResolvedValue({
retcode: 0,
data: { replys: [reply], reply_attach_info: 'reply-next', has_more: true },
});
await qzone.getQzonePublicReplies(cookies, feed, comment, '', 20);
expect(body(request, 2)).toEqual({
...baseParams,
cur_comment: comment,
comment_attach_info: '',
reply_attach_info: 'reply-first',
page_size: 20,
});
await qzone.getQzonePublicReplies(cookies, feed, comment, 'reply-next', 5);
expect(body(request, 3).reply_attach_info).toBe('reply-next');
});
});
describe('public Qzone interactions', () => {
it.each([
[true, 'Upvote', '0x9d18', qzone.setQzonePublicUpvote, { upvote: { num: 1 } }],
[false, 'Downvote', '0x9d19', qzone.setQzonePublicUpvote, { upvote: { num: 0 } }],
[true, 'DoLike', '0x9699', qzone.setQzonePublicLike, { like: { num: 1 } }],
[false, 'UnLike', '0x969c', qzone.setQzonePublicLike, { like: { num: 0 } }],
] as const)(
'sends %s to %s with its own success field',
async (enable, method, command, invoke, result) => {
const request = mockRequest(result);
await expect(invoke(cookies, feed, enable)).resolves.toEqual(result);
expect(request.mock.calls[0]![0]).toContain(`/${method}?`);
expect(JSON.parse(request.mock.calls[0]![3]!['x-oidb']!)).toEqual({
uint32_command: command,
uint32_service_type: '1',
});
expect(body(request)).toEqual(
method.includes('Like')
? { ...baseParams, like_key: 'like-key', like_type: 1 }
: baseParams,
);
},
);
it('encodes text as plain RichItem and retains rich arrays and reply target identities', async () => {
const request = mockRequest({ comment });
await qzone.commentQzonePublicFeed(cookies, feed, user, '中文 & +\n第二行');
expect(body(request)).toEqual({
...baseParams,
comment: { user, content: [{ type: 0, content: '中文 & +\n第二行' }] },
});
const rich = [
{ type: 0, content: '文字' },
{ type: 2, unknown: { keep: true } },
];
request.mockResolvedValue({ retcode: 0, data: { reply } });
await qzone.replyQzonePublicFeed(cookies, feed, user, comment, rich);
expect(body(request, 1)).toEqual({
...baseParams,
comment,
reply: { user, target_user: comment.user, content: rich },
});
await qzone.replyQzonePublicFeed(cookies, feed, user, comment, '回复', reply);
expect((body(request, 2).reply as JsonObject).target_user).toEqual(reply.user);
});
it('likes comments and replies using their upstream like.id rather than comment IDs', async () => {
const request = mockRequest({ like: { count: 1 } });
await qzone.setQzonePublicCommentLike(
cookies,
feed,
{ ...comment, like: { id: 'comment-like-key' } },
true,
);
expect(body(request)).toEqual({
...baseParams,
like_key: 'comment-like-key',
like_type: 2,
});
await qzone.setQzonePublicCommentLike(cookies, feed, comment, false, {
...reply,
like: { id: 'reply-like-key' },
});
expect(body(request, 1)).toEqual({
...baseParams,
like_key: 'reply-like-key',
like_type: 3,
});
await expect(qzone.setQzonePublicCommentLike(cookies, feed, comment, true)).rejects.toThrow(
/like_key/,
);
expect(request).toHaveBeenCalledTimes(2);
});
it('passes complete comment/reply objects for deletion and forwards without a second comment', async () => {
const request = mockRequest({ unknown: 'keep' });
await qzone.deleteQzonePublicComment(cookies, feed, comment);
expect(body(request)).toEqual({ ...baseParams, comment });
await qzone.deleteQzonePublicReply(cookies, feed, comment, reply);
expect(body(request, 1)).toEqual({ ...baseParams, comment, reply });
await qzone.forwardQzonePublicFeed(cookies, feed, '');
expect(body(request, 2)).toEqual({ ...baseParams, reason: '' });
expect(request.mock.calls[2]![0]).toContain(
'/trpc.qzone.feeds_writer.FeedsWriter/ForwardFeed?',
);
expect(request).toHaveBeenCalledTimes(3);
});
it('follows public authors by uid and other authors by uin, exposing nested relation failures', async () => {
const request = mockRequest({ ret_code: '0' });
await qzone.setQzonePublicFollow(cookies, feed, true);
expect(body(request)).toEqual({
follow_from_type: 1,
target_uid: 'author-uid',
follow_feed_id: 'feed-1',
cell_feed_transfer: feed.cell_feed_transfer,
});
const other = {
...feed,
cell_common: { ...(feed.cell_common as JsonObject), domain_type: 1 },
};
await qzone.setQzonePublicFollow(cookies, other, false);
expect(body(request, 1).target_uin).toBe('20002');
expect(request.mock.calls[1]![0]).toContain('/Unfollow?');
request.mockResolvedValueOnce({ retcode: 0, data: { ret_code: 403, ret_msg: 'denied' } });
await expect(qzone.setQzonePublicFollow(cookies, feed, true)).rejects.toThrow(
'ret_code=403 denied',
);
});
it('rejects missing operation identities before writing', async () => {
const request = mockRequest();
expect(() => qzone.forwardQzonePublicFeed(cookies, {}, '')).toThrow(/union_id/);
await expect(
qzone.setQzonePublicLike(cookies, { cell_common: { union_id: union } }, true),
).rejects.toThrow(/like_key/);
await expect(
qzone.setQzonePublicFollow(cookies, { cell_common: { union_id: union } }, true),
).rejects.toThrow(/uid\/uin/);
await expect(qzone.replyQzonePublicFeed(cookies, feed, user, {}, 'text')).rejects.toThrow(
/user/,
);
expect(request).not.toHaveBeenCalled();
});
});
describe('public Qzone errors', () => {
it.each([
null,
{},
{ retcode: 0 },
{ retcode: 0, data: [] },
{ retcode: 4003, message: 'need login' },
])('does not report malformed or failed envelopes as success (%j)', async (response) => {
const request = vi.spyOn(RequestUtil, 'HttpGetJson').mockResolvedValue(response);
await expect(qzone.getQzonePublicFeedDetail(cookies, feed)).rejects.toThrow(
/qzone public GetFeedDetail failed/,
);
expect(request).toHaveBeenCalledTimes(1);
});
it('propagates transport errors and does not retry writes or accept missing success fields', async () => {
const request = mockRequest().mockRejectedValueOnce(new Error('connection closed'));
await expect(qzone.commentQzonePublicFeed(cookies, feed, user, 'text')).rejects.toThrow(
'connection closed',
);
await expect(qzone.setQzonePublicLike(cookies, feed, true)).rejects.toThrow(/缺少 like/);
await expect(qzone.setQzonePublicUpvote(cookies, feed, false)).rejects.toThrow(
/缺少 upvote/,
);
await expect(qzone.commentQzonePublicFeed(cookies, feed, user, 'text')).rejects.toThrow(
/缺少 comment/,
);
await expect(
qzone.replyQzonePublicFeed(cookies, feed, user, comment, 'text'),
).rejects.toThrow(/缺少 reply/);
expect(request).toHaveBeenCalledTimes(5);
});
});