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 = { fetchFriendList: 'contacts', fetchGroupList: 'contacts', fetchGroupMemberList: 'contacts', fetchUserProfile: 'contacts', fetchGroupRequests: 'contacts', fetchGroupRequestsByUid: 'contacts', fetchDownloadRKeys: 'contacts', groupInvites: 'contacts', findGroupInviteCardGroupBySequence: 'contacts', }; function fakeBridge(overrides: Record = {}): BridgeInterface { const apisSynth: Record> = {}; 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 { 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/, ); }); });