Files
SnowLuma/packages/onebot/tests/actions-group-admin.test.ts
TanTan fa9c73363d
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
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

1587 lines
52 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import type { ApiActionContext } from '../src/api-handler';
import { actions } from '../src/actions/group-admin';
function requireAction(name: string) {
const action = actions.find((entry) => entry.names[0] === name);
if (!action) {
throw new Error(`${name} action missing`);
}
return action;
}
function adminCtx(groupAdmin: Record<string, ReturnType<typeof vi.fn>>): {
ctx: ApiActionContext;
groupAdmin: typeof groupAdmin;
} {
return {
groupAdmin,
ctx: { bridge: { apis: { groupAdmin } } } as unknown as ApiActionContext,
};
}
function portraitCtx(setGroupAvatar = vi.fn(async () => {})): {
ctx: ApiActionContext;
setGroupAvatar: ReturnType<typeof vi.fn>;
} {
return {
setGroupAvatar,
ctx: {
bridge: { apis: { profile: { setGroupAvatar } } },
} as unknown as ApiActionContext,
};
}
const setGroupKick = requireAction('set_group_kick');
const setGroupKickMembers = requireAction('set_group_kick_members');
const setGroupBan = requireAction('set_group_ban');
const setGroupWholeBan = requireAction('set_group_whole_ban');
const setGroupAddOption = requireAction('set_group_add_option');
const setGroupSearch = requireAction('set_group_search');
const setGroupMemberInvitePolicy = requireAction('set_group_member_invite_policy');
const setGroupNewMemberHistoryVisibility = requireAction('set_group_new_member_history_visibility');
const setGroupMemberPermissions = requireAction('set_group_member_permissions');
const getGroupAdminSettings = requireAction('get_group_admin_settings');
const getGroupTags = requireAction('get_group_tags');
const addGroupTag = requireAction('add_group_tag');
const deleteGroupTag = requireAction('delete_group_tag');
const setGroupAdmin = requireAction('set_group_admin');
const getAdminPermissions = requireAction('get_group_admin_permissions');
const setAdminPermissions = requireAction('set_group_admin_permissions');
const setAdminReceiveVerify = requireAction('set_group_admin_receive_verify');
const setGroupCard = requireAction('set_group_card');
const setGroupName = requireAction('set_group_name');
const setGroupLeave = requireAction('set_group_leave');
const setGroupSpecialTitle = requireAction('set_group_special_title');
const setGroupAnonymous = requireAction('set_group_anonymous');
const setGroupAnonymousBan = requireAction('set_group_anonymous_ban');
const setGroupPortrait = requireAction('set_group_portrait');
describe('group admin actions catalog', () => {
it('exports the public group-admin actions in source order', () => {
expect(actions.map((action) => [...action.names])).toEqual([
['set_group_kick'],
['set_group_kick_members'],
['set_group_ban'],
['set_group_whole_ban'],
['set_group_add_option'],
['set_group_search'],
['set_group_member_invite_policy'],
['set_group_new_member_history_visibility'],
['set_group_member_permissions'],
['get_group_admin_settings'],
['get_group_tags'],
['add_group_tag'],
['delete_group_tag'],
['set_group_admin'],
['get_group_admin_permissions'],
['set_group_admin_permissions'],
['set_group_admin_receive_verify'],
['set_group_card'],
['set_group_name'],
['set_group_leave'],
['set_group_special_title'],
['set_group_anonymous'],
['set_group_anonymous_ban'],
['set_group_portrait'],
]);
expect(actions.map((action) => action.kind)).toEqual([
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
'normal',
]);
});
});
describe('set_group_kick parse', () => {
it('requires group_id and user_id and defaults reject_add_request to false', () => {
expect(setGroupKick.parse({ group_id: 314159, user_id: 271828 })).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, reject_add_request: false },
});
});
it('coerces numeric-string ids and an explicit reject flag', () => {
expect(
setGroupKick.parse({
group_id: '80001',
user_id: '24680',
reject_add_request: 'yes',
}),
).toEqual({
ok: true,
value: { group_id: 80001, user_id: 24680, reject_add_request: true },
});
});
it('rejects a missing group_id', () => {
expect(setGroupKick.parse({ user_id: 271828 })).toEqual({
ok: false,
field: 'group_id',
reason: 'is required',
});
});
it('rejects group_id 0 and user_id 0', () => {
expect(setGroupKick.parse({ group_id: 0, user_id: 271828 })).toEqual({
ok: false,
field: 'group_id',
reason: 'must be >= 1',
});
expect(setGroupKick.parse({ group_id: 314159, user_id: 0 })).toEqual({
ok: false,
field: 'user_id',
reason: 'must be >= 1',
});
});
it('rejects an unrecognised reject_add_request synonym', () => {
expect(
setGroupKick.parse({
group_id: 314159,
user_id: 271828,
reject_add_request: 'maybe',
}),
).toEqual({
ok: false,
field: 'reject_add_request',
reason: 'expected a boolean',
});
});
});
describe('set_group_kick toHandler', () => {
it('forwards the default reject flag and returns an empty ok payload', async () => {
const kickMember = vi.fn(async () => {});
const { ctx } = adminCtx({ kickMember });
await expect(
setGroupKick.toHandler(ctx)({ group_id: 314159, user_id: 271828 }),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(kickMember.mock.calls).toEqual([[314159, 271828, false]]);
});
it('forwards an explicit reject_add_request', async () => {
const kickMember = vi.fn(async () => {});
const { ctx } = adminCtx({ kickMember });
await expect(
setGroupKick.toHandler(ctx)({
group_id: 80002,
user_id: 30003,
reject_add_request: 1,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(kickMember.mock.calls).toEqual([[80002, 30003, true]]);
});
it('returns BAD_REQUEST for a missing user_id without kicking', async () => {
const kickMember = vi.fn(async () => {});
const { ctx } = adminCtx({ kickMember });
await expect(setGroupKick.toHandler(ctx)({ group_id: 314159 })).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'user_id: is required',
});
expect(kickMember).not.toHaveBeenCalled();
});
});
describe('set_group_kick_members parse', () => {
it('requires a non-empty member-id array and defaults reject_add_request', () => {
expect(
setGroupKickMembers.parse({
group_id: 888001,
user_id: [271828, '13579'],
}),
).toEqual({
ok: true,
value: { group_id: 888001, user_id: [271828, 13579], reject_add_request: false },
});
});
it('keeps an explicit reject_add_request of false', () => {
expect(
setGroupKickMembers.parse({
group_id: 888001,
user_id: [24680],
reject_add_request: false,
}),
).toEqual({
ok: true,
value: { group_id: 888001, user_id: [24680], reject_add_request: false },
});
});
it('rejects a missing or non-array user_id', () => {
expect(setGroupKickMembers.parse({ group_id: 888001 })).toEqual({
ok: false,
field: 'user_id',
reason: 'is required',
});
expect(setGroupKickMembers.parse({ group_id: 888001, user_id: 271828 })).toEqual({
ok: false,
field: 'user_id',
reason: 'expected an array',
});
});
it('rejects an empty member list and names a bad element index', () => {
expect(setGroupKickMembers.parse({ group_id: 888001, user_id: [] })).toEqual({
ok: false,
field: 'user_id',
reason: 'must not be empty',
});
expect(setGroupKickMembers.parse({ group_id: 888001, user_id: [271828, 0] })).toEqual({
ok: false,
field: 'user_id[1]',
reason: 'must be >= 1',
});
});
});
describe('set_group_kick_members toHandler', () => {
it('forwards the member list and default reject flag', async () => {
const kickMembers = vi.fn(async () => {});
const { ctx } = adminCtx({ kickMembers });
await expect(
setGroupKickMembers.toHandler(ctx)({
group_id: 888001,
user_id: [271828, 13579],
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(kickMembers.mock.calls).toEqual([[888001, [271828, 13579], false]]);
});
it('forwards reject_add_request when kicking several members', async () => {
const kickMembers = vi.fn(async () => {});
const { ctx } = adminCtx({ kickMembers });
await expect(
setGroupKickMembers.toHandler(ctx)({
group_id: '70001',
user_id: ['24681'],
reject_add_request: 'on',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(kickMembers.mock.calls).toEqual([[70001, [24681], true]]);
});
it('returns BAD_REQUEST for an empty member list without kicking', async () => {
const kickMembers = vi.fn(async () => {});
const { ctx } = adminCtx({ kickMembers });
await expect(
setGroupKickMembers.toHandler(ctx)({
group_id: 888001,
user_id: [],
}),
).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'user_id: must not be empty',
});
expect(kickMembers).not.toHaveBeenCalled();
});
});
describe('set_group_ban parse', () => {
it('defaults duration to 1800 and keeps a present 0 as unmute', () => {
expect(setGroupBan.parse({ group_id: 314159, user_id: 271828 })).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, duration: 1800 },
});
expect(setGroupBan.parse({ group_id: 314159, user_id: 271828, duration: 0 })).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, duration: 0 },
});
});
it('truncates a numeric-string duration', () => {
expect(
setGroupBan.parse({
group_id: 314159,
user_id: 271828,
duration: '600.9',
}),
).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, duration: 600 },
});
});
it('rejects a negative duration', () => {
expect(
setGroupBan.parse({
group_id: 314159,
user_id: 271828,
duration: -1,
}),
).toEqual({
ok: false,
field: 'duration',
reason: 'must be >= 0',
});
});
});
describe('set_group_ban toHandler', () => {
it('mutes with the default duration', async () => {
const muteMember = vi.fn(async () => {});
const { ctx } = adminCtx({ muteMember });
await expect(
setGroupBan.toHandler(ctx)({ group_id: 314159, user_id: 271828 }),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(muteMember.mock.calls).toEqual([[314159, 271828, 1800]]);
});
it('forwards duration 0 as unmute', async () => {
const muteMember = vi.fn(async () => {});
const { ctx } = adminCtx({ muteMember });
await expect(
setGroupBan.toHandler(ctx)({
group_id: 80003,
user_id: 30004,
duration: 0,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(muteMember.mock.calls).toEqual([[80003, 30004, 0]]);
});
it('returns BAD_REQUEST for a negative duration without muting', async () => {
const muteMember = vi.fn(async () => {});
const { ctx } = adminCtx({ muteMember });
await expect(
setGroupBan.toHandler(ctx)({
group_id: 314159,
user_id: 271828,
duration: -3,
}),
).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'duration: must be >= 0',
});
expect(muteMember).not.toHaveBeenCalled();
});
});
describe('set_group_ban describe', () => {
it('documents the group-user preset and the unmute-capable duration', () => {
const doc = setGroupBan.describe();
expect(doc.name).toBe('set_group_ban');
expect(doc.aliases).toEqual([]);
expect(doc.summary).toBe('禁言群成员(duration=0 解除)');
expect(doc.readOnly).toBe(false);
expect(doc.returns).toBeUndefined();
expect(doc.returnsSchema).toMatchObject({ type: 'null' });
expect(doc.stream).toBeUndefined();
expect(doc.invariants).toEqual([]);
expect(doc.params).toEqual([
{
name: 'group_id',
type: 'uint',
required: true,
desc: '群号',
role: 'group_id',
schema: { type: 'integer', minimum: 1 },
},
{
name: 'user_id',
type: 'uint',
required: true,
desc: 'QQ 号',
role: 'member_id',
schema: { type: 'integer', minimum: 1 },
},
{
name: 'duration',
type: 'int',
required: false,
default: 1800,
role: 'duration',
schema: { type: 'integer', minimum: 0 },
},
]);
expect(doc.inputSchema).toEqual({
type: 'object',
additionalProperties: true,
required: ['group_id', 'user_id'],
properties: {
group_id: {
type: 'integer',
minimum: 1,
description: '群号',
'x-role': 'group_id',
},
user_id: {
type: 'integer',
minimum: 1,
description: 'QQ 号',
'x-role': 'member_id',
},
duration: {
type: 'integer',
minimum: 0,
default: 1800,
'x-role': 'duration',
},
},
});
});
});
describe('set_group_whole_ban parse', () => {
it('defaults enable to true', () => {
expect(setGroupWholeBan.parse({ group_id: 941657197 })).toEqual({
ok: true,
value: { group_id: 941657197, enable: true },
});
});
it('coerces enable false synonyms', () => {
expect(setGroupWholeBan.parse({ group_id: 941657197, enable: 0 })).toEqual({
ok: true,
value: { group_id: 941657197, enable: false },
});
expect(setGroupWholeBan.parse({ group_id: 941657197, enable: 'OFF' })).toEqual({
ok: true,
value: { group_id: 941657197, enable: false },
});
});
});
describe('set_group_whole_ban toHandler', () => {
it('enables whole-group mute by default', async () => {
const muteAll = vi.fn(async () => {});
const { ctx } = adminCtx({ muteAll });
await expect(setGroupWholeBan.toHandler(ctx)({ group_id: 941657197 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
expect(muteAll.mock.calls).toEqual([[941657197, true]]);
});
it('forwards an explicit disable', async () => {
const muteAll = vi.fn(async () => {});
const { ctx } = adminCtx({ muteAll });
await expect(
setGroupWholeBan.toHandler(ctx)({
group_id: 12345,
enable: false,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(muteAll.mock.calls).toEqual([[12345, false]]);
});
});
describe('set_group_add_option parse', () => {
it('defaults add_type to 0 and keeps a present 0', () => {
expect(setGroupAddOption.parse({ group_id: 12345 })).toEqual({
ok: true,
value: {
group_id: 12345,
add_type: 0,
group_question: undefined,
group_answer: undefined,
},
});
expect(setGroupAddOption.parse({ group_id: 12345, add_type: 0 })).toEqual({
ok: true,
value: {
group_id: 12345,
add_type: 0,
group_question: undefined,
group_answer: undefined,
},
});
});
it('accepts a positive add_type and rejects a negative one', () => {
expect(setGroupAddOption.parse({ group_id: 12345, add_type: '2' })).toEqual({
ok: true,
value: {
group_id: 12345,
add_type: 2,
group_question: undefined,
group_answer: undefined,
},
});
expect(setGroupAddOption.parse({ group_id: 12345, add_type: -1 })).toEqual({
ok: false,
field: 'add_type',
reason: 'must be >= 0',
});
});
it('keeps optional question and answer when present', () => {
expect(
setGroupAddOption.parse({
group_id: 12345,
add_type: 4,
group_question: 'q',
group_answer: 'a',
}),
).toEqual({
ok: true,
value: {
group_id: 12345,
add_type: 4,
group_question: 'q',
group_answer: 'a',
},
});
});
});
describe('set_group_add_option toHandler', () => {
it('forwards the default add_type of 0', async () => {
const setAddOption = vi.fn(async () => {});
const { ctx } = adminCtx({ setAddOption });
await expect(setGroupAddOption.toHandler(ctx)({ group_id: 12345 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
expect(setAddOption.mock.calls).toEqual([[12345, 0, undefined, undefined]]);
});
it('forwards an explicit add_type', async () => {
const setAddOption = vi.fn(async () => {});
const { ctx } = adminCtx({ setAddOption });
await expect(
setGroupAddOption.toHandler(ctx)({
group_id: 941657197,
add_type: 3,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setAddOption.mock.calls).toEqual([[941657197, 3, undefined, undefined]]);
});
it('forwards question and answer', async () => {
const setAddOption = vi.fn(async () => {});
const { ctx } = adminCtx({ setAddOption });
await expect(
setGroupAddOption.toHandler(ctx)({
group_id: 941657197,
add_type: 4,
group_question: 'q',
group_answer: 'a',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setAddOption.mock.calls).toEqual([[941657197, 4, 'q', 'a']]);
});
});
describe('set_group_search parse', () => {
it('leaves both search switches undefined when omitted', () => {
expect(setGroupSearch.parse({ group_id: 70001 })).toEqual({
ok: true,
value: {
group_id: 70001,
no_finger_open: undefined,
no_code_finger_open: undefined,
},
});
});
it('keeps a present 0 instead of treating it as missing', () => {
expect(
setGroupSearch.parse({
group_id: 70001,
no_finger_open: 0,
no_code_finger_open: '1',
}),
).toEqual({
ok: true,
value: {
group_id: 70001,
no_finger_open: 0,
no_code_finger_open: 1,
},
});
});
it('rejects a negative search switch', () => {
expect(setGroupSearch.parse({ group_id: 70001, no_finger_open: -1 })).toEqual({
ok: false,
field: 'no_finger_open',
reason: 'must be >= 0',
});
expect(setGroupSearch.parse({ group_id: 70001, no_code_finger_open: -2 })).toEqual({
ok: false,
field: 'no_code_finger_open',
reason: 'must be >= 0',
});
});
});
describe('set_group_search toHandler', () => {
it('forwards omitted switches as undefined', async () => {
const setSearch = vi.fn(async () => {});
const { ctx } = adminCtx({ setSearch });
await expect(setGroupSearch.toHandler(ctx)({ group_id: 70001 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
expect(setSearch.mock.calls).toEqual([[70001, undefined, undefined]]);
});
it('forwards both supplied switches', async () => {
const setSearch = vi.fn(async () => {});
const { ctx } = adminCtx({ setSearch });
await expect(
setGroupSearch.toHandler(ctx)({
group_id: 70002,
no_finger_open: 1,
no_code_finger_open: 0,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setSearch.mock.calls).toEqual([[70002, 1, 0]]);
});
});
describe('set_group_member_invite_policy parse', () => {
it.each(['disabled', 'require_approval', 'no_approval', 'no_approval_under_100'] as const)(
'accepts policy %s',
(policy) => {
expect(setGroupMemberInvitePolicy.parse({ group_id: 12345, policy })).toEqual({
ok: true,
value: { group_id: 12345, policy },
});
},
);
it('requires policy and rejects an unknown value', () => {
expect(setGroupMemberInvitePolicy.parse({ group_id: 12345 })).toEqual({
ok: false,
field: 'policy',
reason: 'is required',
});
expect(
setGroupMemberInvitePolicy.parse({
group_id: 12345,
policy: 'anything',
}),
).toEqual({
ok: false,
field: 'policy',
reason: 'expected one of: disabled, require_approval, no_approval, no_approval_under_100',
});
});
});
describe('set_group_member_invite_policy toHandler', () => {
it('forwards the validated policy', async () => {
const setMemberInvitePolicy = vi.fn(async () => {});
const { ctx } = adminCtx({ setMemberInvitePolicy });
await expect(
setGroupMemberInvitePolicy.toHandler(ctx)({
group_id: '12345',
policy: 'no_approval_under_100',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setMemberInvitePolicy.mock.calls).toEqual([[12345, 'no_approval_under_100']]);
});
it('returns BAD_REQUEST for an unknown policy without calling the API', async () => {
const setMemberInvitePolicy = vi.fn(async () => {});
const { ctx } = adminCtx({ setMemberInvitePolicy });
await expect(
setGroupMemberInvitePolicy.toHandler(ctx)({
group_id: 12345,
policy: 'open',
}),
).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording:
'policy: expected one of: disabled, require_approval, no_approval, no_approval_under_100',
});
expect(setMemberInvitePolicy).not.toHaveBeenCalled();
});
});
describe('set_group_new_member_history_visibility parse', () => {
it('requires visible and coerces boolean synonyms', () => {
expect(
setGroupNewMemberHistoryVisibility.parse({
group_id: 12345,
visible: 'false',
}),
).toEqual({
ok: true,
value: { group_id: 12345, visible: false },
});
expect(setGroupNewMemberHistoryVisibility.parse({ group_id: 12345 })).toEqual({
ok: false,
field: 'visible',
reason: 'is required',
});
});
});
describe('set_group_new_member_history_visibility toHandler', () => {
it('forwards the normalized group id and visibility', async () => {
const setNewMemberHistoryVisibility = vi.fn(async () => {});
const { ctx } = adminCtx({ setNewMemberHistoryVisibility });
await expect(
setGroupNewMemberHistoryVisibility.toHandler(ctx)({
group_id: '12345',
visible: false,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setNewMemberHistoryVisibility.mock.calls).toEqual([[12345, false]]);
});
it('returns BAD_REQUEST when visible is missing', async () => {
const setNewMemberHistoryVisibility = vi.fn(async () => {});
const { ctx } = adminCtx({ setNewMemberHistoryVisibility });
await expect(
setGroupNewMemberHistoryVisibility.toHandler(ctx)({ group_id: 12345 }),
).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'visible: is required',
});
expect(setNewMemberHistoryVisibility).not.toHaveBeenCalled();
});
});
describe('set_group_member_permissions parse', () => {
it('accepts a single capability switch and leaves the others undefined', () => {
expect(
setGroupMemberPermissions.parse({
group_id: 12345,
allow_member_upload_album: false,
}),
).toEqual({
ok: true,
value: {
group_id: 12345,
allow_member_upload_album: false,
allow_member_temporary_session: undefined,
allow_member_create_group: undefined,
},
});
});
it('coerces every supplied capability switch', () => {
expect(
setGroupMemberPermissions.parse({
group_id: '12345',
allow_member_upload_album: 'true',
allow_member_temporary_session: false,
allow_member_create_group: 1,
}),
).toEqual({
ok: true,
value: {
group_id: 12345,
allow_member_upload_album: true,
allow_member_temporary_session: false,
allow_member_create_group: true,
},
});
});
it('rejects an empty update after optional fields resolve to undefined', () => {
expect(setGroupMemberPermissions.parse({ group_id: 12345 })).toEqual({
ok: false,
field: '',
reason: 'at least one of: allow_member_upload_album, allow_member_temporary_session, allow_member_create_group',
});
});
});
describe('set_group_member_permissions toHandler', () => {
it('remaps all supplied switches into the group-admin payload', async () => {
const setMemberPermissions = vi.fn(async () => {});
const { ctx } = adminCtx({ setMemberPermissions });
await expect(
setGroupMemberPermissions.toHandler(ctx)({
group_id: '12345',
allow_member_upload_album: 'true',
allow_member_temporary_session: false,
allow_member_create_group: 1,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setMemberPermissions.mock.calls).toEqual([
[
12345,
{
allowMemberUploadAlbum: true,
allowMemberTemporarySession: false,
allowMemberCreateGroup: true,
},
],
]);
});
it('forwards a single supplied switch with the others undefined', async () => {
const setMemberPermissions = vi.fn(async () => {});
const { ctx } = adminCtx({ setMemberPermissions });
await expect(
setGroupMemberPermissions.toHandler(ctx)({
group_id: 12345,
allow_member_temporary_session: true,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setMemberPermissions.mock.calls).toEqual([
[
12345,
{
allowMemberUploadAlbum: undefined,
allowMemberTemporarySession: true,
allowMemberCreateGroup: undefined,
},
],
]);
});
it('returns BAD_REQUEST for an empty update without calling the API', async () => {
const setMemberPermissions = vi.fn(async () => {});
const { ctx } = adminCtx({ setMemberPermissions });
await expect(
setGroupMemberPermissions.toHandler(ctx)({ group_id: 12345 }),
).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording:
'at least one of: allow_member_upload_album, allow_member_temporary_session, allow_member_create_group',
});
expect(setMemberPermissions).not.toHaveBeenCalled();
});
});
describe('get_group_admin_settings parse', () => {
it('requires group_id and has no extra params', () => {
expect(getGroupAdminSettings.parse({})).toEqual({
ok: false,
field: 'group_id',
reason: 'is required',
});
expect(getGroupAdminSettings.parse({ group_id: 12345 })).toEqual({
ok: true,
value: { group_id: 12345 },
});
});
});
describe('get_group_admin_settings toHandler', () => {
it('forwards group_id and returns the current settings', async () => {
const settings = {
add_type: 2,
group_question: 'q',
group_answer: 'a',
robot_member_switch: 1,
robot_member_examine: 0,
member_invite_policy: 'require_approval',
allow_member_upload_album: true,
allow_member_temporary_session: false,
allow_member_create_group: true,
new_member_history_visible: true,
no_finger_open: 0,
no_code_finger_open: 1,
};
const getAdminSettings = vi.fn(async () => settings);
const { ctx } = adminCtx({ getAdminSettings });
await expect(getGroupAdminSettings.toHandler(ctx)({ group_id: 12345 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: settings,
});
expect(getAdminSettings.mock.calls).toEqual([[12345]]);
});
});
describe('set_group_member_permissions describe', () => {
it('documents the at-least-one capability invariant', () => {
const doc = setGroupMemberPermissions.describe();
expect(doc.name).toBe('set_group_member_permissions');
expect(doc.summary).toBe('设置群成员权限(仅群主可改)');
expect(doc.invariants).toEqual([
'at least one of: allow_member_upload_album, allow_member_temporary_session, allow_member_create_group',
]);
expect(doc.inputSchema).toEqual({
type: 'object',
additionalProperties: true,
required: ['group_id'],
properties: {
group_id: {
type: 'integer',
minimum: 1,
description: '群号',
'x-role': 'group_id',
},
allow_member_upload_album: { type: 'boolean' },
allow_member_temporary_session: { type: 'boolean' },
allow_member_create_group: { type: 'boolean' },
},
});
});
});
describe('per-administrator permissions', () => {
const target = { group_id: 12345, user_id: 67890 };
it.each([
{},
{ kick_member_per_day: 20 },
{ kick_member: true },
{ kick_member: false, kick_member_per_day: 20 },
{ kick_member: true, kick_member_per_day: 1 },
{ kick_member: true, kick_member_per_day: 201 },
])('rejects an ambiguous or invalid patch before effects: %j', async (patch) => {
const set = vi.fn();
const { ctx } = adminCtx({ setAdminPermissions: set });
expect(setAdminPermissions.parse({ ...target, ...patch }).ok).toBe(false);
await expect(
setAdminPermissions.toHandler(ctx)({ ...target, ...patch }),
).resolves.toMatchObject({ status: 'failed' });
expect(set).not.toHaveBeenCalled();
});
it.each([0, 5, 10, 20, 50, 100, 200])(
'accepts a supported explicit daily limit %s',
(limit) => {
expect(
setAdminPermissions.parse({
...target,
kick_member: 'yes',
kick_member_per_day: String(limit),
}),
).toMatchObject({
ok: true,
value: { ...target, kick_member: true, kick_member_per_day: limit },
});
},
);
it('forwards false and preserves omitted permissions without promoting the member', async () => {
const set = vi.fn(async () => {});
const promote = vi.fn();
const { ctx } = adminCtx({ setAdminPermissions: set, setAdmin: promote });
await expect(
setAdminPermissions.toHandler(ctx)({ ...target, recall_member_message: 'no' }),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(set).toHaveBeenCalledExactlyOnceWith(12345, 67890, {
recallMemberMessage: false,
kickMember: undefined,
kickMemberPerDay: undefined,
});
expect(promote).not.toHaveBeenCalled();
});
it('requires an explicit receive-verification switch and forwards false', async () => {
expect(setAdminReceiveVerify.parse(target)).toMatchObject({ ok: false, field: 'enable' });
const set = vi.fn(async () => {});
const { ctx } = adminCtx({ setAdminReceiveVerify: set });
await expect(
setAdminReceiveVerify.toHandler(ctx)({ ...target, enable: 'off' }),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(set).toHaveBeenCalledExactlyOnceWith(12345, 67890, false);
});
it.each([true, false, null])(
'returns receive-verification state %s and nullable permissions',
async (receiveVerify) => {
const snapshot = {
...target,
recall_member_message: null,
kick_member: true,
kick_member_per_day: 0,
receive_verify: receiveVerify,
};
const get = vi.fn(async () => snapshot);
const { ctx } = adminCtx({ getAdminPermissions: get });
await expect(getAdminPermissions.toHandler(ctx)(target)).resolves.toEqual({
status: 'ok',
retcode: 0,
data: snapshot,
});
expect(get).toHaveBeenCalledExactlyOnceWith(12345, 67890);
expect(getAdminPermissions.describe().readOnly).toBe(true);
},
);
it('propagates a rejected or unconfirmed update', async () => {
const set = vi.fn(async () => {
throw new Error('target not confirmed');
});
const { ctx } = adminCtx({ setAdminReceiveVerify: set });
await expect(
setAdminReceiveVerify.toHandler(ctx)({ ...target, enable: true }),
).rejects.toThrow('target not confirmed');
});
});
describe('set_group_admin parse', () => {
it('defaults enable to true', () => {
expect(setGroupAdmin.parse({ group_id: 314159, user_id: 271828 })).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, enable: true },
});
});
it('accepts an explicit disable', () => {
expect(
setGroupAdmin.parse({
group_id: 314159,
user_id: 271828,
enable: 'no',
}),
).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, enable: false },
});
});
});
describe('set_group_admin toHandler', () => {
it('promotes a member with the default enable flag', async () => {
const setAdmin = vi.fn(async () => {});
const { ctx } = adminCtx({ setAdmin });
await expect(
setGroupAdmin.toHandler(ctx)({ group_id: 314159, user_id: 271828 }),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setAdmin.mock.calls).toEqual([[314159, 271828, true]]);
});
it('forwards an explicit demotion', async () => {
const setAdmin = vi.fn(async () => {});
const { ctx } = adminCtx({ setAdmin });
await expect(
setGroupAdmin.toHandler(ctx)({
group_id: 80004,
user_id: 30005,
enable: false,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setAdmin.mock.calls).toEqual([[80004, 30005, false]]);
});
});
describe('set_group_card parse', () => {
it('defaults card to an empty string and keeps an explicit empty clear', () => {
expect(setGroupCard.parse({ group_id: 314159, user_id: 271828 })).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, card: '' },
});
expect(
setGroupCard.parse({
group_id: 314159,
user_id: 271828,
card: '',
}),
).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, card: '' },
});
});
it('stringifies a numeric card and rejects a non-stringifiable value', () => {
expect(
setGroupCard.parse({
group_id: 314159,
user_id: 271828,
card: 404,
}),
).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, card: '404' },
});
expect(
setGroupCard.parse({
group_id: 314159,
user_id: 271828,
card: { text: 'nick' },
}),
).toEqual({
ok: false,
field: 'card',
reason: 'expected a string',
});
});
});
describe('set_group_card toHandler', () => {
it('forwards the default empty card as a clear', async () => {
const setCard = vi.fn(async () => {});
const { ctx } = adminCtx({ setCard });
await expect(
setGroupCard.toHandler(ctx)({ group_id: 314159, user_id: 271828 }),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setCard.mock.calls).toEqual([[314159, 271828, '']]);
});
it('forwards an explicit card string', async () => {
const setCard = vi.fn(async () => {});
const { ctx } = adminCtx({ setCard });
await expect(
setGroupCard.toHandler(ctx)({
group_id: 80005,
user_id: 30006,
card: '值班号',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setCard.mock.calls).toEqual([[80005, 30006, '值班号']]);
});
});
describe('set_group_name parse', () => {
it('defaults group_name to an empty string', () => {
expect(setGroupName.parse({ group_id: 314159 })).toEqual({
ok: true,
value: { group_id: 314159, group_name: '' },
});
});
it('stringifies a boolean group_name', () => {
expect(setGroupName.parse({ group_id: 314159, group_name: true })).toEqual({
ok: true,
value: { group_id: 314159, group_name: 'true' },
});
});
});
describe('set_group_name toHandler', () => {
it('forwards the default empty name', async () => {
const setName = vi.fn(async () => {});
const { ctx } = adminCtx({ setName });
await expect(setGroupName.toHandler(ctx)({ group_id: 314159 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
expect(setName.mock.calls).toEqual([[314159, '']]);
});
it('forwards an explicit group name', async () => {
const setName = vi.fn(async () => {});
const { ctx } = adminCtx({ setName });
await expect(
setGroupName.toHandler(ctx)({
group_id: 941657197,
group_name: 'Muted Group',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setName.mock.calls).toEqual([[941657197, 'Muted Group']]);
});
});
describe('set_group_leave parse', () => {
it('requires only group_id and drops undeclared keys', () => {
expect(setGroupLeave.parse({ group_id: 314159, extra: true })).toEqual({
ok: true,
value: { group_id: 314159 },
});
});
it('rejects a missing group_id', () => {
expect(setGroupLeave.parse({})).toEqual({
ok: false,
field: 'group_id',
reason: 'is required',
});
});
});
describe('set_group_leave toHandler', () => {
it('forwards the group id to leave', async () => {
const leave = vi.fn(async () => {});
const { ctx } = adminCtx({ leave });
await expect(setGroupLeave.toHandler(ctx)({ group_id: 314159 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
expect(leave.mock.calls).toEqual([[314159]]);
});
it('returns BAD_REQUEST for group_id 0 without leaving', async () => {
const leave = vi.fn(async () => {});
const { ctx } = adminCtx({ leave });
await expect(setGroupLeave.toHandler(ctx)({ group_id: 0 })).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'group_id: must be >= 1',
});
expect(leave).not.toHaveBeenCalled();
});
});
describe('set_group_special_title parse', () => {
it('defaults special_title to an empty string', () => {
expect(setGroupSpecialTitle.parse({ group_id: 314159, user_id: 271828 })).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, special_title: '' },
});
});
it('keeps an explicit empty title', () => {
expect(
setGroupSpecialTitle.parse({
group_id: 314159,
user_id: 271828,
special_title: '',
}),
).toEqual({
ok: true,
value: { group_id: 314159, user_id: 271828, special_title: '' },
});
});
});
describe('set_group_special_title toHandler', () => {
it('forwards the default empty title', async () => {
const setSpecialTitle = vi.fn(async () => {});
const { ctx } = adminCtx({ setSpecialTitle });
await expect(
setGroupSpecialTitle.toHandler(ctx)({
group_id: 314159,
user_id: 271828,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setSpecialTitle.mock.calls).toEqual([[314159, 271828, '']]);
});
it('forwards an explicit title', async () => {
const setSpecialTitle = vi.fn(async () => {});
const { ctx } = adminCtx({ setSpecialTitle });
await expect(
setGroupSpecialTitle.toHandler(ctx)({
group_id: 80006,
user_id: 30007,
special_title: '龙王',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setSpecialTitle.mock.calls).toEqual([[80006, 30007, '龙王']]);
});
});
describe('set_group_anonymous parse', () => {
it('accepts any params and returns an empty object', () => {
expect(setGroupAnonymous.parse({})).toEqual({ ok: true, value: {} });
expect(setGroupAnonymous.parse({ group_id: 314159, enable: true })).toEqual({
ok: true,
value: {},
});
});
});
describe('set_group_anonymous toHandler', () => {
it('returns ok without touching the bridge', async () => {
await expect(
setGroupAnonymous.toHandler({} as ApiActionContext)({
group_id: 314159,
enable: false,
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
});
});
describe('set_group_anonymous describe', () => {
it('documents the unimplemented anonymous switch', () => {
const doc = setGroupAnonymous.describe();
expect(doc.name).toBe('set_group_anonymous');
expect(doc.summary).toBe('匿名开关(未实现,返回 ok)');
expect(doc.readOnly).toBe(false);
expect(doc.params).toEqual([]);
expect(doc.invariants).toEqual([]);
expect(doc.inputSchema).toEqual({
type: 'object',
properties: {},
additionalProperties: true,
});
});
});
describe('set_group_anonymous_ban parse', () => {
it('accepts any params and returns an empty object', () => {
expect(
setGroupAnonymousBan.parse({
group_id: 314159,
anonymous_flag: 'x',
duration: 60,
}),
).toEqual({ ok: true, value: {} });
});
});
describe('set_group_anonymous_ban toHandler', () => {
it('returns ok without touching the bridge', async () => {
await expect(
setGroupAnonymousBan.toHandler({} as ApiActionContext)({
flag: 'anon',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
});
});
describe('set_group_anonymous_ban describe', () => {
it('documents the unimplemented anonymous mute', () => {
expect(setGroupAnonymousBan.describe().summary).toBe('匿名禁言(未实现,返回 ok)');
});
});
describe('set_group_portrait parse', () => {
it('requires a non-empty image source and stringifies primitives', () => {
expect(
setGroupPortrait.parse({
group_id: 314159,
file: 'https://example.invalid/avatar.png',
}),
).toEqual({
ok: true,
value: { group_id: 314159, file: 'https://example.invalid/avatar.png' },
});
expect(setGroupPortrait.parse({ group_id: 314159, file: 10001 })).toEqual({
ok: true,
value: { group_id: 314159, file: '10001' },
});
});
it('rejects a missing or empty file', () => {
expect(setGroupPortrait.parse({ group_id: 314159 })).toEqual({
ok: false,
field: 'file',
reason: 'is required',
});
expect(setGroupPortrait.parse({ group_id: 314159, file: '' })).toEqual({
ok: false,
field: 'file',
reason: 'must not be empty',
});
});
it('rejects a non-stringifiable file', () => {
expect(
setGroupPortrait.parse({
group_id: 314159,
file: { url: 'https://example.invalid/avatar.png' },
}),
).toEqual({
ok: false,
field: 'file',
reason: 'expected a string',
});
});
});
describe('set_group_portrait toHandler', () => {
it('forwards the image source to profile.setGroupAvatar', async () => {
const { ctx, setGroupAvatar } = portraitCtx();
await expect(
setGroupPortrait.toHandler(ctx)({
group_id: 314159,
file: 'base64://abc',
}),
).resolves.toEqual({ status: 'ok', retcode: 0, data: null });
expect(setGroupAvatar.mock.calls).toEqual([[314159, 'base64://abc']]);
});
it('returns BAD_REQUEST for an empty file without setting the avatar', async () => {
const { ctx, setGroupAvatar } = portraitCtx();
await expect(
setGroupPortrait.toHandler(ctx)({
group_id: 314159,
file: '',
}),
).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'file: must not be empty',
});
expect(setGroupAvatar).not.toHaveBeenCalled();
});
});
describe('get_group_tags parse', () => {
it('accepts category 0 and a numeric string', () => {
expect(getGroupTags.parse({ group_class: 0 })).toEqual({
ok: true,
value: { group_class: 0 },
});
expect(getGroupTags.parse({ group_class: '12' })).toEqual({
ok: true,
value: { group_class: 12 },
});
});
it('rejects a missing, negative, or oversized category', () => {
expect(getGroupTags.parse({})).toEqual({
ok: false,
field: 'group_class',
reason: 'is required',
});
expect(getGroupTags.parse({ group_class: -1 })).toEqual({
ok: false,
field: 'group_class',
reason: 'must be >= 0',
});
expect(getGroupTags.parse({ group_class: 0x1_0000_0000 })).toEqual({
ok: false,
field: 'group_class',
reason: 'must be <= 4294967295',
});
});
});
describe('get_group_tags toHandler', () => {
it('returns the tag list for the category', async () => {
const listCategoryTags = vi.fn(async () => ({
tags: [{ id: 7, name: '学习', type: 1 }],
}));
const { ctx } = adminCtx({ listCategoryTags });
await expect(getGroupTags.toHandler(ctx)({ group_class: 4 })).resolves.toEqual({
status: 'ok',
retcode: 0,
data: { tags: [{ id: 7, name: '学习', type: 1 }] },
});
expect(listCategoryTags.mock.calls).toEqual([[4]]);
});
});
describe('add_group_tag and delete_group_tag', () => {
it('rejects an empty name and does not call the bridge', async () => {
const addCategoryTag = vi.fn(async () => {});
const removeCategoryTag = vi.fn(async () => {});
const { ctx } = adminCtx({ addCategoryTag, removeCategoryTag });
await expect(addGroupTag.toHandler(ctx)({ group_class: 1, name: '' })).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'name: must not be empty',
});
await expect(deleteGroupTag.toHandler(ctx)({ group_class: 1, name: '' })).resolves.toEqual({
status: 'failed',
retcode: 1400,
data: null,
wording: 'name: must not be empty',
});
expect(addCategoryTag).not.toHaveBeenCalled();
expect(removeCategoryTag).not.toHaveBeenCalled();
});
it('forwards the category and the exact name', async () => {
const addCategoryTag = vi.fn(async () => {});
const removeCategoryTag = vi.fn(async () => {});
const { ctx } = adminCtx({ addCategoryTag, removeCategoryTag });
await expect(
addGroupTag.toHandler(ctx)({ group_class: 0, name: ' 户外 ' }),
).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
await expect(
deleteGroupTag.toHandler(ctx)({ group_class: '2', name: '户外' }),
).resolves.toEqual({
status: 'ok',
retcode: 0,
data: null,
});
expect(addCategoryTag.mock.calls).toEqual([[0, ' 户外 ']]);
expect(removeCategoryTag.mock.calls).toEqual([[2, '户外']]);
});
});