NapCatQQ source snapshot
This commit is contained in:
commit
f954252a34
1111 files changed
+139867
No files matched your search
@@ -0,0 +1,160 @@
|
||||
// StreamBasic must be evaluated before OneBotAction because of their circular import.
|
||||
import '../napcat-onebot/action/stream/StreamBasic';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { TypeCompiler } from '@sinclair/typebox/compiler';
|
||||
import { GetQzoneCommentList } from '@/napcat-onebot/action/extends/qzone/GetQzoneCommentList';
|
||||
import { GetQzoneCommentReplies } from '@/napcat-onebot/action/extends/qzone/GetQzoneCommentReplies';
|
||||
import { QzoneFeedReferenceExample } from '@/napcat-onebot/action/extends/qzone/common';
|
||||
import { QzoneCommentPageExample, QzoneReplyPageExample } from '@/napcat-onebot/action/extends/qzone/common-comment';
|
||||
|
||||
function createCore () {
|
||||
return {
|
||||
apis: {
|
||||
QzoneApi: {
|
||||
getComments: vi.fn().mockResolvedValue(QzoneCommentPageExample),
|
||||
getCommentReplies: vi.fn().mockResolvedValue(QzoneReplyPageExample),
|
||||
},
|
||||
},
|
||||
context: { logger: { logError: vi.fn() } },
|
||||
};
|
||||
}
|
||||
|
||||
describe('QQ空间评论读取 actions', () => {
|
||||
it('passes the complete reference and optional opaque cursor to comment reads', async () => {
|
||||
const core = createCore();
|
||||
const action = new GetQzoneCommentList(undefined as never, core as never);
|
||||
const reference = {
|
||||
...QzoneFeedReferenceExample,
|
||||
union_id: { ...QzoneFeedReferenceExample.union_id, unknown_field: { large_id: '9007199254740993' } },
|
||||
cell_feed_transfer: { opaque: [0, false, 'keep'] },
|
||||
};
|
||||
const expected = structuredClone(reference);
|
||||
|
||||
const first = await action.handle({ feed_ref: reference }, 'test', {} as never);
|
||||
await action.handle({ feed_ref: reference, cursor: 'next-comment-cursor' }, 'test', {} as never);
|
||||
|
||||
expect(core.apis.QzoneApi.getComments.mock.calls).toEqual([[expected, undefined], [expected, 'next-comment-cursor']]);
|
||||
expect(first.data).toBe(QzoneCommentPageExample);
|
||||
expect(reference).toEqual(expected);
|
||||
});
|
||||
|
||||
it('retains raw parent identity types, UID-only authors and every unknown field through handle parsing', async () => {
|
||||
const core = createCore();
|
||||
const action = new GetQzoneCommentReplies(undefined as never, core as never);
|
||||
const parent = {
|
||||
id: 0,
|
||||
user: { uid: 'u_commenter', uin: '', nick: '', portrait: { source: 'opaque-portrait' }, unknown_user: 'keep' },
|
||||
content: [{ type: 0, content: '原评论', unknown_segment: true }],
|
||||
replys: [{ id: 'reply-1', content: [{ type: 0, content: '内联回复' }] }],
|
||||
reply_attach_info: 'server-reply-pointer',
|
||||
like: { id: 'like-key', count: 0, status: 0 },
|
||||
unknown_field: { large_id: '9007199254740993' },
|
||||
};
|
||||
const expected = structuredClone(parent);
|
||||
|
||||
const result = await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: parent }, 'test', {} as never);
|
||||
await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: { id: '0' }, cursor: 'next-reply-cursor' }, 'test', {} as never);
|
||||
|
||||
expect(result.status).toBe('ok');
|
||||
expect(core.apis.QzoneApi.getCommentReplies.mock.calls).toEqual([
|
||||
[QzoneFeedReferenceExample, expected, undefined],
|
||||
[QzoneFeedReferenceExample, { id: '0' }, 'next-reply-cursor'],
|
||||
]);
|
||||
expect(typeof core.apis.QzoneApi.getCommentReplies.mock.calls[0]?.[1]?.id).toBe('number');
|
||||
expect(typeof core.apis.QzoneApi.getCommentReplies.mock.calls[1]?.[1]?.id).toBe('string');
|
||||
expect(parent).toEqual(expected);
|
||||
});
|
||||
|
||||
it('allows an omitted author or empty auxiliary user object when the parent ID is exact', async () => {
|
||||
const core = createCore();
|
||||
const action = new GetQzoneCommentReplies(undefined as never, core as never);
|
||||
for (const parent of [{ id: 'parent' }, { id: 'parent', user: {} }, { id: 0, user: { uin: '', uid: '' } }]) {
|
||||
const result = await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: parent }, 'test', {} as never);
|
||||
expect(result.status).toBe('ok');
|
||||
}
|
||||
expect(core.apis.QzoneApi.getCommentReplies).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('rejects malformed parent IDs before TypeBox can coerce them or invoke the core', async () => {
|
||||
const core = createCore();
|
||||
const action = new GetQzoneCommentReplies(undefined as never, core as never);
|
||||
for (const id of [undefined, null, false, true, '', ' ', -1, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1, {}, []]) {
|
||||
const result = await action.handle({ feed_ref: QzoneFeedReferenceExample, comment: { id } } as never, 'test', {} as never);
|
||||
expect(result.status).toBe('failed');
|
||||
}
|
||||
expect(core.apis.QzoneApi.getCommentReplies).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects malformed read payloads and unsupported page controls before invoking the core', async () => {
|
||||
const cases = [
|
||||
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, page_size: 100 }],
|
||||
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, cursor: false }],
|
||||
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, cursor: '' }],
|
||||
[GetQzoneCommentList, { feed_ref: QzoneFeedReferenceExample, cursor: 'x'.repeat(16385) }],
|
||||
[GetQzoneCommentList, { feed_ref: { union_id: {} } }],
|
||||
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: [] }],
|
||||
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent', user: [] } }],
|
||||
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent', user: null } }],
|
||||
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent', content: '中'.repeat(22 * 1024) } }],
|
||||
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent' }, page_size: 20 }],
|
||||
[GetQzoneCommentReplies, { feed_ref: QzoneFeedReferenceExample, comment: { id: 'parent' }, target_uin: '10002' }],
|
||||
] as const;
|
||||
for (const [Action, payload] of cases) {
|
||||
const core = createCore();
|
||||
const result = await new Action(undefined as never, core as never).handle(payload as never, 'test', {} as never);
|
||||
expect(result.status).toBe('failed');
|
||||
expect(core.apis.QzoneApi.getComments).not.toHaveBeenCalled();
|
||||
expect(core.apis.QzoneApi.getCommentReplies).not.toHaveBeenCalled();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns partial and unknown metadata exactly instead of claiming complete comment history', async () => {
|
||||
const core = createCore();
|
||||
const page = {
|
||||
...QzoneCommentPageExample,
|
||||
comment_num: null,
|
||||
comments: [{ ...QzoneCommentPageExample.comments[0]!, reply_num: null, has_more_replies: null, replies: [] }],
|
||||
has_more: true,
|
||||
next_cursor: 'next-page',
|
||||
raw: { unknown_server_field: true },
|
||||
};
|
||||
core.apis.QzoneApi.getComments.mockResolvedValueOnce(page);
|
||||
|
||||
const result = await new GetQzoneCommentList(undefined as never, core as never).handle(
|
||||
{ feed_ref: QzoneFeedReferenceExample }, 'test', {} as never
|
||||
);
|
||||
|
||||
expect(result.data).toBe(page);
|
||||
expect(page.comments[0]?.has_more_replies).toBeNull();
|
||||
expect(page.raw).toEqual({ unknown_server_field: true });
|
||||
});
|
||||
|
||||
it('surfaces cursor failures without retrying the read', async () => {
|
||||
const core = createCore();
|
||||
core.apis.QzoneApi.getComments.mockRejectedValueOnce(new Error('QQ空间评论游标无效或已过期'));
|
||||
|
||||
const result = await new GetQzoneCommentList(undefined as never, core as never).handle(
|
||||
{ feed_ref: QzoneFeedReferenceExample, cursor: 'expired-cursor' }, 'test', {} as never
|
||||
);
|
||||
|
||||
expect(result.status).toBe('failed');
|
||||
expect(result.message).toContain('游标无效或已过期');
|
||||
expect(core.apis.QzoneApi.getComments).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('declares complete API metadata with schema-valid detailed examples', () => {
|
||||
const core = createCore();
|
||||
const actions = [
|
||||
new GetQzoneCommentList(undefined as never, core as never),
|
||||
new GetQzoneCommentReplies(undefined as never, core as never),
|
||||
];
|
||||
expect(actions.map(action => action.actionName)).toEqual(['get_qzone_comment_list', 'get_qzone_comment_replies']);
|
||||
for (const action of actions) {
|
||||
expect(action.actionSummary).not.toBe('');
|
||||
expect(action.actionDescription).not.toBe('');
|
||||
expect(action.actionTags).toContain('扩展接口');
|
||||
expect(TypeCompiler.Compile(action.payloadSchema).Check(action.payloadExample)).toBe(true);
|
||||
expect(TypeCompiler.Compile(action.returnSchema).Check(action.returnExample)).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user