Files
NapCatQQ/packages/napcat-test/qzoneManagementActions.test.ts
T
TanTan f954252a34
Build NapCat Artifacts / Type Check & Test (push) Waiting to run
Release Tag / validate-version (push) Waiting to run
NapCatQQ source snapshot
2026-11-13 09:01:47 +08:00

203 lines
9.8 KiB
TypeScript

// 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 { GetQzoneProfile } from '@/napcat-onebot/action/extends/qzone/GetQzoneProfile';
import { SetQzoneProfile } from '@/napcat-onebot/action/extends/qzone/SetQzoneProfile';
import { SetQzoneMsgRight } from '@/napcat-onebot/action/extends/qzone/SetQzoneMsgRight';
import { SetQzoneBan } from '@/napcat-onebot/action/extends/qzone/SetQzoneBan';
function createCore () {
const QzoneApi = {
getProfile: vi.fn().mockResolvedValue({ name: '', description: '' }),
setProfile: vi.fn().mockResolvedValue(undefined),
setMsgRight: vi.fn().mockImplementation(async (_tid: string, right: number) => ({ ugc_right: right })),
setBan: vi.fn().mockResolvedValue(undefined),
};
return { apis: { QzoneApi }, context: { logger: { logError: vi.fn() } } };
}
describe('QQ空间资料和权限 actions', () => {
it('reads the current profile by default and preserves large decimal target strings', async () => {
const core = createCore();
const action = new GetQzoneProfile(undefined as never, core as never);
const result = await action.handle({}, 'test', {} as never);
await action.handle({ target_uin: 10002 }, 'test', {} as never);
await action.handle({ target_uin: '9007199254740993' }, 'test', {} as never);
expect(result).toMatchObject({ status: 'ok', data: { name: '', description: '' } });
expect(core.apis.QzoneApi.getProfile.mock.calls).toEqual([
[undefined], ['10002'], ['9007199254740993'],
]);
});
it('distinguishes omitted profile fields from explicit empty strings through handle parsing', async () => {
const core = createCore();
const action = new SetQzoneProfile(undefined as never, core as never);
const result = await action.handle({ name: '' }, 'test', {} as never);
await action.handle({ description: '' }, 'test', {} as never);
await action.handle({ name: undefined, description: '说明' }, 'test', {} as never);
expect(JSON.parse(JSON.stringify(result))).toMatchObject({ status: 'ok', retcode: 0, data: null });
expect(core.apis.QzoneApi.setProfile.mock.calls).toEqual([
[{ name: '' }], [{ description: '' }], [{ description: '说明' }],
]);
expect(Object.keys(core.apis.QzoneApi.setProfile.mock.calls[0]?.[0] ?? {})).toEqual(['name']);
});
it('normalizes supported profile scalars to strings without trimming content', async () => {
const core = createCore();
const action = new SetQzoneProfile(undefined as never, core as never);
await action.handle({ name: 0, description: false }, 'test', {} as never);
await action.handle({ name: true, description: ' 保留空白\r\n ' }, 'test', {} as never);
expect(core.apis.QzoneApi.setProfile.mock.calls).toEqual([
[{ name: '0', description: 'false' }],
[{ name: 'true', description: ' 保留空白\r\n ' }],
]);
});
it('normalizes right codes and target IDs before a single own-post permission request', async () => {
const core = createCore();
const action = new SetQzoneMsgRight(undefined as never, core as never);
const result = await action.handle({
tid: 'existing-tid',
ugc_right: '16',
target_uins: [10002, '9007199254740993', '10002'],
}, 'test', {} as never);
expect(core.apis.QzoneApi.setMsgRight).toHaveBeenCalledExactlyOnceWith(
'existing-tid', 16, ['10002', '9007199254740993']
);
expect(result).toMatchObject({ status: 'ok', data: { ugc_right: 16 } });
});
it('keeps enable false and serializes successful ban writes with explicit null data', async () => {
const core = createCore();
const action = new SetQzoneBan(undefined as never, core as never);
const disabled = await action.handle({ user_id: '9007199254740993', enable: false }, 'test', {} as never);
const enabled = await action.handle({ user_id: 10002 }, 'test', {} as never);
expect(core.apis.QzoneApi.setBan.mock.calls).toEqual([
['9007199254740993', false], ['10002', true],
]);
expect(JSON.parse(JSON.stringify(disabled))).toMatchObject({ status: 'ok', data: null });
expect(JSON.parse(JSON.stringify(enabled))).toMatchObject({ status: 'ok', data: null });
});
it('normalizes safe numeric permission-update IDs without accepting lossy values', async () => {
const core = createCore();
const result = await new SetQzoneMsgRight(undefined as never, core as never).handle({ tid: 0, ugc_right: '4' }, 'test', {} as never);
expect(result).toMatchObject({ status: 'ok', data: { ugc_right: 4 } });
expect(core.apis.QzoneApi.setMsgRight).toHaveBeenCalledExactlyOnceWith('0', 4, []);
});
it('normalizes supported SnowLuma boolean spellings without treating arbitrary text as true', async () => {
const core = createCore();
const action = new SetQzoneBan(undefined as never, core as never);
for (const enable of ['false', '0', ' NO ', 'Off', 0]) {
const result = await action.handle({ user_id: '10002', enable }, 'test', {} as never);
expect(result.status).toBe('ok');
expect(core.apis.QzoneApi.setBan).toHaveBeenLastCalledWith('10002', false);
}
for (const enable of ['true', '1', ' YES ', 'On', 1]) {
const result = await action.handle({ user_id: '10002', enable }, 'test', {} as never);
expect(result.status).toBe('ok');
expect(core.apis.QzoneApi.setBan).toHaveBeenLastCalledWith('10002', true);
}
});
it('rejects malformed or lossy values before any core call', async () => {
const cases = [
[GetQzoneProfile, { target_uin: Number.MAX_SAFE_INTEGER + 1 }],
[GetQzoneProfile, { target_uin: false }],
[GetQzoneProfile, { target_uin: 'bad-uin' }],
[SetQzoneProfile, {}],
[SetQzoneProfile, { name: undefined }],
[SetQzoneProfile, { name: null }],
[SetQzoneProfile, { name: ['text'] }],
[SetQzoneProfile, { description: { content: 'text' } }],
[SetQzoneProfile, { name: Infinity }],
[SetQzoneProfile, { name: NaN }],
[SetQzoneProfile, { name: 'x'.repeat(8193) }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 2 }],
[SetQzoneMsgRight, { tid: '', ugc_right: 1 }],
[SetQzoneMsgRight, { tid: ' ', ugc_right: 1 }],
[SetQzoneMsgRight, { tid: true, ugc_right: 1 }],
[SetQzoneMsgRight, { tid: Number.MAX_SAFE_INTEGER + 1, ugc_right: 1 }],
[SetQzoneMsgRight, { tid: Infinity, ugc_right: 1 }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: true }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 16 }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 128, target_uins: [] }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 16, target_uins: [Number.MAX_SAFE_INTEGER + 1] }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 16, target_uins: ['10002', false] }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 16, target_uins: '10002' }],
[SetQzoneBan, { user_id: 0 }],
[SetQzoneBan, { user_id: Number.MAX_SAFE_INTEGER + 1 }],
[SetQzoneBan, { user_id: '10002', enable: 'unknown' }],
[SetQzoneBan, { user_id: '10002', enable: 2 }],
[SetQzoneBan, { user_id: '10002', enable: null }],
] 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');
for (const method of Object.values(core.apis.QzoneApi)) expect(method).not.toHaveBeenCalled();
}
});
it('rejects unknown parameters and current-account overrides before any core call', async () => {
const cases = [
[GetQzoneProfile, { target_uin: '10002', unknown: true }],
[SetQzoneProfile, { name: 'text', target_uin: '10002' }],
[SetQzoneMsgRight, { tid: 'tid', ugc_right: 1, target_uin: '10002' }],
[SetQzoneBan, { user_id: '10002', unknown: true }],
] 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');
for (const method of Object.values(core.apis.QzoneApi)) expect(method).not.toHaveBeenCalled();
}
});
it('surfaces a failed write without retrying or claiming null success', async () => {
const core = createCore();
core.apis.QzoneApi.setProfile.mockRejectedValue(new Error('QQ空间未明确确认资料修改成功'));
const result = await new SetQzoneProfile(undefined as never, core as never).handle({ name: 'text' }, 'test', {} as never);
expect(result.status).toBe('failed');
expect(result.message).toContain('未明确确认');
expect(core.apis.QzoneApi.setProfile).toHaveBeenCalledTimes(1);
});
it('has complete API metadata and schema-valid examples for all four actions', () => {
const core = createCore();
const actions = [
new GetQzoneProfile(undefined as never, core as never),
new SetQzoneProfile(undefined as never, core as never),
new SetQzoneMsgRight(undefined as never, core as never),
new SetQzoneBan(undefined as never, core as never),
];
expect(actions.map(action => action.actionName)).toEqual([
'get_qzone_profile', 'set_qzone_profile', 'set_qzone_msg_right', 'set_qzone_ban',
]);
for (const action of actions) {
expect(action.actionSummary.length).toBeGreaterThan(0);
expect(action.actionDescription.length).toBeGreaterThan(0);
expect(action.actionTags).toContain('扩展接口');
expect(TypeCompiler.Compile(action.payloadSchema).Check(action.payloadExample)).toBe(true);
expect(TypeCompiler.Compile(action.returnSchema).Check(action.returnExample)).toBe(true);
}
});
});