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
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, '户外']]);
|
|
});
|
|
});
|