Files
SnowLuma/packages/onebot/tests/request-actions.test.ts
T
TanTan fa9c73363d
Dev Build / cleanup (push) Waiting to run
Dev Build / typecheck (push) Waiting to run
Publish MCP to npm / Build, test, and publish MCP (push) Waiting to run
Publish SDK to npm / Build, test, and publish SDK (push) Waiting to run
Sync action catalog to docs / sync (push) Waiting to run
Publish UI to npm / Build and publish UI (push) Waiting to run
Build and Release / preflight (push) Waiting to run
SnowLuma v2.4.2 source snapshot
2026-11-13 09:02:00 +08:00

379 lines
14 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { handleGroupAddRequest } from '../src/modules/request-actions';
import type { BridgeInterface } from '../../src/bridge/bridge-interface';
import type { GroupRequestInfo } from '@snowluma/protocol/qq-info';
// See `contact-actions.test.ts` for the auto-promotion rationale.
const APIS_ROUTING: Record<string, string> = {
fetchFriendList: 'contacts',
fetchGroupList: 'contacts',
fetchGroupMemberList: 'contacts',
fetchUserProfile: 'contacts',
fetchGroupRequests: 'contacts',
fetchGroupRequestsByUid: 'contacts',
fetchDownloadRKeys: 'contacts',
groupInvites: 'contacts',
findGroupInviteCardGroupBySequence: 'contacts',
};
function fakeBridge(overrides: Record<string, any> = {}): BridgeInterface {
const apisSynth: Record<string, Record<string, any>> = {};
for (const [k, v] of Object.entries(overrides)) {
const area = APIS_ROUTING[k];
if (area) {
if (!apisSynth[area]) {
apisSynth[area] = {};
}
apisSynth[area][k] = v;
}
}
const merged = { ...overrides, apis: { ...apisSynth, ...(overrides.apis ?? {}) } };
return new Proxy(merged as BridgeInterface, {
get(target, prop) {
if (prop in target) {
return (target as any)[prop];
}
throw new Error(`fakeBridge: '${String(prop)}' was not stubbed`);
},
});
}
function fakeRequest(overrides: Partial<GroupRequestInfo> = {}): GroupRequestInfo {
return {
groupId: 999,
groupName: 'g',
targetUid: 'u_t',
targetUin: 5555,
targetName: 'target',
invitorUid: 'u_i',
invitorUin: 7777,
invitorName: 'inviter',
operatorUid: 'u_o',
operatorUin: 8888,
operatorName: 'op',
sequence: 42,
state: 1,
eventType: 2,
comment: 'pls',
filtered: false,
...overrides,
};
}
describe('onebot/modules/request-actions / handleGroupAddRequest', () => {
it('uses a canonical flag and keeps the tuple when the inbox has no match', async () => {
const setAddRequest = vi.fn(async () => {});
const fetchGroupRequests = vi.fn(async () => []);
const bridge = fakeBridge({
fetchGroupRequests,
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, 'slreq:1:123456:999:22:1', true, 'ok');
expect(fetchGroupRequests).toHaveBeenCalled();
expect(setAddRequest).toHaveBeenCalledWith(999, 123456, 22, true, 'ok', true, undefined);
});
it('corrects a stale canonical eventType from the live inbox', async () => {
const setAddRequest = vi.fn(async () => {});
const trans = Uint8Array.from([0xaa]);
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async (filtered: boolean) =>
filtered
? []
: [
fakeRequest({
groupId: 999,
sequence: 123456,
notifyType: 7,
eventType: 1,
filtered: false,
operateTransInfo: trans,
}),
],
),
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, 'slreq:1:123456:999:2:0', true, '');
expect(setAddRequest).toHaveBeenCalledWith(999, 123456, 1, true, '', false, trans);
});
it('accepts a NapCat numeric sequence and resolves the exact main-inbox request', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async (filtered: boolean) =>
filtered
? []
: [
fakeRequest({
groupId: 999,
sequence: 261237407,
eventType: 1,
filtered: false,
}),
],
),
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, '261237407', false, 'no');
expect(setAddRequest).toHaveBeenCalledWith(
999,
261237407,
1,
false,
'no',
false,
undefined,
);
});
it('accepts a NapCat numeric sequence from the filtered inbox', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async (filtered: boolean) =>
filtered
? [fakeRequest({ groupId: 999, sequence: 55, eventType: 2, filtered: true })]
: [],
),
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, '55', true, 'ok');
expect(setAddRequest).toHaveBeenCalledWith(999, 55, 2, true, 'ok', true, undefined);
});
it('resolves a private invite-card msgseq by its cached group', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => 999),
fetchGroupRequests: vi.fn(async () => []),
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, '778899', true, 'ok');
expect(setAddRequest).toHaveBeenCalledWith(999, 778899, 2, true, 'ok', false, undefined);
});
it('matches add requests by groupId and targetUid', async () => {
const setAddRequest = vi.fn(async () => {});
const fetchGroupRequestsByUid = vi.fn(async () => [
fakeRequest({ sequence: 41, targetUid: 'u_t', eventType: 2 }),
fakeRequest({
groupId: 999,
targetUid: 'u_t',
sequence: 42,
eventType: 1,
filtered: false,
}),
]);
const bridge = fakeBridge({
fetchGroupRequestsByUid,
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, 'add:999:u_t', true, 'ok');
expect(setAddRequest).toHaveBeenCalledOnce();
expect(setAddRequest).toHaveBeenCalledWith(999, 42, 1, true, 'ok', false, undefined);
expect(fetchGroupRequestsByUid).toHaveBeenCalledWith(false, 100);
expect(fetchGroupRequestsByUid).toHaveBeenCalledWith(true, 100);
});
it('matches invite requests by groupId and invitorUid', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
fetchGroupRequestsByUid: vi.fn(async () => [
fakeRequest({ sequence: 96, invitorUid: 'u_i', eventType: 1 }),
fakeRequest({
groupId: 999,
invitorUid: 'u_i',
sequence: 97,
eventType: 22,
filtered: false,
}),
]),
groupInvites: { getSelfInviteNotice: vi.fn(() => undefined) },
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, 'invite:999:u_i', false, 'no');
expect(setAddRequest).toHaveBeenCalledOnce();
expect(setAddRequest).toHaveBeenCalledWith(999, 97, 22, false, 'no', false, undefined);
});
it('finds a request that only lives in the filtered inbox (#197)', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
// Main inbox empty; the invite sits only in the spam-filtered inbox.
fetchGroupRequestsByUid: vi.fn(async (filtered: boolean) =>
filtered
? [
fakeRequest({
groupId: 999,
invitorUid: 'u_i',
sequence: 55,
eventType: 2,
filtered: true,
}),
]
: [],
),
groupInvites: { getSelfInviteNotice: vi.fn(() => undefined) },
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, 'invite:999:u_i', true, 'ok');
// Approved through the filtered inbox (subCommand 2 → last arg true).
expect(setAddRequest).toHaveBeenCalledOnce();
expect(setAddRequest).toHaveBeenCalledWith(999, 55, 2, true, 'ok', true, undefined);
});
it('surfaces "not found" only when neither inbox has the request', async () => {
const bridge = fakeBridge({
fetchGroupRequestsByUid: vi.fn(async () => []),
groupInvites: { getSelfInviteNotice: vi.fn(() => undefined) },
apis: { groupAdmin: { setAddRequest: vi.fn(async () => {}) } } as any,
});
await expect(handleGroupAddRequest(bridge, 'invite:999:u_i', true, 'ok')).rejects.toThrow(
/matching group request not found/,
);
});
it('never picks one of two different pending invitations with the same legacy flag', async () => {
const setAddRequest = vi.fn();
const bridge = fakeBridge({
groupInvites: { getSelfInviteNotice: vi.fn(() => undefined) },
fetchGroupRequestsByUid: vi.fn(async (filtered: boolean) =>
filtered ? [] : [fakeRequest({ sequence: 123 }), fakeRequest({ sequence: 124 })],
),
apis: { groupAdmin: { setAddRequest } },
});
await expect(handleGroupAddRequest(bridge, 'invite:999:u_i', true, '')).rejects.toThrow(
'multiple pending group requests',
);
expect(setAddRequest).not.toHaveBeenCalled();
});
it('skips processed requests when approving a legacy notice', async () => {
const setAddRequest = vi.fn();
const bridge = fakeBridge({
groupInvites: { getSelfInviteNotice: vi.fn(() => undefined) },
fetchGroupRequestsByUid: vi.fn(async (filtered: boolean) =>
filtered
? []
: [fakeRequest({ sequence: 123, state: 2 }), fakeRequest({ sequence: 124 })],
),
apis: { groupAdmin: { setAddRequest } },
});
await handleGroupAddRequest(bridge, 'invite:999:u_i', true, '');
expect(setAddRequest).toHaveBeenCalledExactlyOnceWith(
999,
124,
2,
true,
'',
false,
undefined,
);
});
it('does not treat an inviter QQ number as a request sequence (#213)', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async () => [
fakeRequest({ groupId: 888, invitorUin: 261237407, sequence: 123456 }),
]),
apis: { groupAdmin: { setAddRequest } } as any,
});
await expect(handleGroupAddRequest(bridge, '261237407', true, 'ok')).rejects.toThrow(
/sequence 261237407 not found/,
);
expect(setAddRequest).not.toHaveBeenCalled();
});
it('does not fall back to another request from the same group', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
fetchGroupRequestsByUid: vi.fn(async () => [
fakeRequest({ groupId: 999, targetUid: 'u_someone_else' }),
]),
apis: { groupAdmin: { setAddRequest } } as any,
});
await expect(
handleGroupAddRequest(bridge, 'add:999:u_missing', true, 'ok'),
).rejects.toThrow(/matching group request not found/);
expect(setAddRequest).not.toHaveBeenCalled();
});
it('uses the surviving inbox when the other request lookup fails', async () => {
const setAddRequest = vi.fn(async () => {});
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async (filtered: boolean) => {
if (!filtered) {
throw new Error('main inbox down');
}
return [fakeRequest({ groupId: 999, sequence: 55, eventType: 2, filtered: true })];
}),
apis: { groupAdmin: { setAddRequest } } as any,
});
await handleGroupAddRequest(bridge, '55', true, 'ok');
expect(setAddRequest).toHaveBeenCalledWith(999, 55, 2, true, 'ok', true, undefined);
});
it('does not report not-found when an unavailable inbox may contain the request', async () => {
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async (filtered: boolean) => {
if (filtered) {
throw new Error('filtered inbox down');
}
return [];
}),
apis: { groupAdmin: { setAddRequest: vi.fn(async () => {}) } } as any,
});
await expect(handleGroupAddRequest(bridge, '55', true, 'ok')).rejects.toThrow(
/lookup incomplete/,
);
});
it('distinguishes request-queue failure from a genuine missing request', async () => {
const bridge = fakeBridge({
findGroupInviteCardGroupBySequence: vi.fn(() => undefined),
fetchGroupRequests: vi.fn(async () => {
throw new Error('OIDB unavailable');
}),
apis: { groupAdmin: { setAddRequest: vi.fn(async () => {}) } } as any,
});
await expect(handleGroupAddRequest(bridge, '55', true, 'ok')).rejects.toThrow(
/failed to fetch group requests/,
);
});
});