import { describe, expect, it, vi } from 'vitest'; import { protobuf_decode } from '@snowluma/proton'; import type { SendPacketResult } from '@snowluma/common/packet-sender'; import type { OidbBase } from '@snowluma/proto-defs/oidb'; import type { Oidb0x89a_0MemberPermission } from '@snowluma/proto-defs/oidb-actions/base'; import { decodeGroupMemberPermissions, GROUP_MEMBER_PERMISSION_MASKS, mergeGroupMemberPermission, SetMemberPermission, type GroupMemberPermission, } from '../../../src/oidb-services/group-admin/set-member-permission'; function makeSender() { const result: SendPacketResult = { success: true, gotResponse: true, errorCode: 0, errorMessage: '', responseData: Buffer.alloc(0), }; return { sendRawPacket: vi.fn(async () => result) }; } describe('SetMemberPermission namespace', () => { it('declares 0x89A_0', () => { expect(SetMemberPermission.command).toBe(0x89a); expect(SetMemberPermission.subCommand).toBe(0); }); it.each<[GroupMemberPermission, number]>([ ['upload_album', 0x1], ['temporary_session', 0x10000], ['create_group', 0x8000], ])('uses the verified deny-bit mask for %s', async (permission, mask) => { const sender = makeSender(); await SetMemberPermission.invoke(sender, { groupId: 12345, currentPrivilegeFlag: 0x80018001, permission, allow: true, }); const [command, bytes] = sender.sendRawPacket.mock.calls[0]!; expect(command).toBe('OidbSvcTrpcTcp.0x89a_0'); const envelope = protobuf_decode>(bytes); expect(envelope.command).toBe(0x89a); expect(envelope.subCommand ?? 0).toBe(0); expect(envelope.body).toMatchObject({ groupUin: 12345n, settings: { appPrivilegeFlag: Number(BigInt(0x80018001) & ~BigInt(mask)), appPrivilegeMask: mask, }, }); }); it('sets a deny bit when a capability is disabled', async () => { const sender = makeSender(); await SetMemberPermission.invoke(sender, { groupId: 12345, currentPrivilegeFlag: 0x80000000, permission: 'temporary_session', allow: false, }); const envelope = protobuf_decode>( sender.sendRawPacket.mock.calls[0]![1], ); expect(envelope.body?.settings).toEqual({ appPrivilegeFlag: 0x80010000, appPrivilegeMask: GROUP_MEMBER_PERMISSION_MASKS.temporary_session, }); }); it('preserves unrelated bits while changing only the selected capability', () => { expect(mergeGroupMemberPermission(0xf0018001, 'upload_album', true)).toBe(0xf0018000); expect(mergeGroupMemberPermission(0xf0000000, 'create_group', false)).toBe(0xf0008000); }); it('treats permission bits as deny flags when decoding', () => { expect(decodeGroupMemberPermissions(0)).toEqual({ allowMemberUploadAlbum: true, allowMemberTemporarySession: true, allowMemberCreateGroup: true, }); expect(decodeGroupMemberPermissions(0x80018001)).toEqual({ allowMemberUploadAlbum: false, allowMemberTemporarySession: false, allowMemberCreateGroup: false, }); expect(decodeGroupMemberPermissions(0x80000000)).toEqual({ allowMemberUploadAlbum: true, allowMemberTemporarySession: true, allowMemberCreateGroup: true, }); }); it('round-trips each permission through merge then decode', () => { let flag = 0x80000000; flag = mergeGroupMemberPermission(flag, 'upload_album', false); flag = mergeGroupMemberPermission(flag, 'temporary_session', true); flag = mergeGroupMemberPermission(flag, 'create_group', false); expect(decodeGroupMemberPermissions(flag)).toEqual({ allowMemberUploadAlbum: false, allowMemberTemporarySession: true, allowMemberCreateGroup: false, }); }); it('keeps explicit zero fields on the wire', async () => { const sender = makeSender(); await SetMemberPermission.invoke(sender, { groupId: 1, currentPrivilegeFlag: 1, permission: 'upload_album', allow: true, }); const envelope = protobuf_decode>( sender.sendRawPacket.mock.calls[0]![1], ); expect(envelope.body?.settings?.appPrivilegeFlag).toBe(0); expect(envelope.body?.settings?.appPrivilegeMask).toBe(1); }); it('rejects an invalid current flag before sending', async () => { const sender = makeSender(); await expect( SetMemberPermission.invoke(sender, { groupId: 1, currentPrivilegeFlag: 0x1_0000_0000, permission: 'upload_album', allow: true, }), ).rejects.toThrow(/unsigned 32-bit/); expect(sender.sendRawPacket).not.toHaveBeenCalled(); }); });