Include verification message reception in administrator permission results. Preserve unavailable states and surface failed reads. Update SDK contracts, action metadata, documentation and isolated behavior coverage.
1587 lines
52 KiB
TypeScript
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, '户外']]);
|
|
});
|
|
});
|