SnowLuma v2.4.2 source snapshot
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
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
This commit is contained in:
commit
fa9c73363d
1367 files changed
+355735
No files matched your search
@@ -0,0 +1,129 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
import { ApproveDoubtBuddyReq } from '../../../src/oidb-services/friend/approve-doubt-buddy-req';
|
||||
import { env, s, v } from '../_pb-oracle';
|
||||
|
||||
function makeSender(selfUid = 'u_self', selfUin = '10001') {
|
||||
const respEnv: OidbBase<OidbEmpty> = { command: 0xd72, subCommand: 0, body: {} };
|
||||
const r: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(protobuf_encode<OidbBase<OidbEmpty>>(respEnv)),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async (_cmd: string, _bytes: Uint8Array) => r),
|
||||
identity: { selfUid, uin: selfUin } as ApproveDoubtBuddyReq.Deps['identity'],
|
||||
resolveUserUid: vi.fn(async () => 'u_lookup_self'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ApproveDoubtBuddyReq namespace', () => {
|
||||
it('keeps the approving account separate from the applicant', async () => {
|
||||
const sender = makeSender();
|
||||
expect(await ApproveDoubtBuddyReq.serialize(sender, { uid: 'u_applicant' })).toEqual({
|
||||
selfUid: 'u_self',
|
||||
targetUid: 'u_applicant',
|
||||
field3: 0,
|
||||
field4: '',
|
||||
});
|
||||
expect(sender.resolveUserUid).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('matches the approval request bytes including explicit default options', async () => {
|
||||
const sender = makeSender();
|
||||
await ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' });
|
||||
|
||||
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
|
||||
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
|
||||
expect(cmd).toBe('OidbSvcTrpcTcp.0xd72_0');
|
||||
const body = [...s(1, 'u_self'), ...s(2, 'u_applicant'), ...v(3, 0), ...s(4, '')];
|
||||
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xd72, 0, body, false));
|
||||
});
|
||||
|
||||
it('resolves the current account when its self profile is not cached', async () => {
|
||||
const sender = makeSender('');
|
||||
await ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' });
|
||||
expect(sender.resolveUserUid).toHaveBeenCalledWith(10001);
|
||||
const body = [...s(1, 'u_lookup_self'), ...s(2, 'u_applicant'), ...v(3, 0), ...s(4, '')];
|
||||
expect(Buffer.from(sender.sendRawPacket.mock.calls[0]![1]).toString('hex')).toBe(
|
||||
env(0xd72, 0, body, false),
|
||||
);
|
||||
});
|
||||
|
||||
it('does not send when the current account cannot be identified', async () => {
|
||||
const sender = makeSender('', '0');
|
||||
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
|
||||
'self uid is unavailable',
|
||||
);
|
||||
expect(sender.sendRawPacket).not.toHaveBeenCalled();
|
||||
expect(sender.resolveUserUid).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates self identity lookup errors without sending', async () => {
|
||||
const sender = makeSender('');
|
||||
sender.resolveUserUid.mockRejectedValue(new Error('profile lookup failed'));
|
||||
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
|
||||
'profile lookup failed',
|
||||
);
|
||||
expect(sender.sendRawPacket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects an empty resolved self identity before sending', async () => {
|
||||
const sender = makeSender('');
|
||||
sender.resolveUserUid.mockResolvedValue('');
|
||||
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
|
||||
'self uid is unavailable',
|
||||
);
|
||||
expect(sender.sendRawPacket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(['', ' '])('rejects an absent applicant identity (%j)', async (uid) => {
|
||||
const sender = makeSender();
|
||||
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid })).rejects.toThrow(
|
||||
'applicant uid is unavailable',
|
||||
);
|
||||
expect(sender.sendRawPacket).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('propagates a rejected approval without retrying a different operation', async () => {
|
||||
const sender = makeSender();
|
||||
sender.sendRawPacket.mockResolvedValue({
|
||||
success: false,
|
||||
gotResponse: true,
|
||||
errorCode: 1201,
|
||||
errorMessage: 'request rejected',
|
||||
responseData: Buffer.alloc(0),
|
||||
});
|
||||
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
|
||||
'request rejected',
|
||||
);
|
||||
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('does not mistake a successful transport for an accepted approval', async () => {
|
||||
const sender = makeSender();
|
||||
sender.sendRawPacket.mockResolvedValue({
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(
|
||||
protobuf_encode<OidbBase<OidbEmpty>>({
|
||||
command: 0xd72,
|
||||
subCommand: 0,
|
||||
errorCode: 1201,
|
||||
errorMsg: 'approval denied',
|
||||
body: {},
|
||||
}),
|
||||
),
|
||||
});
|
||||
await expect(ApproveDoubtBuddyReq.invoke(sender, { uid: 'u_applicant' })).rejects.toThrow(
|
||||
'approval denied',
|
||||
);
|
||||
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { ClearFriendRemark } from '../../../src/oidb-services/friend/clear-friend-remark';
|
||||
import { env, m, s } from '../_pb-oracle';
|
||||
|
||||
function makeDeps() {
|
||||
const result: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.alloc(0),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async () => result),
|
||||
resolveUserUid: vi.fn(async () => 'resolved-uid'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('ClearFriendRemark namespace', () => {
|
||||
it('sends the current QQ 0x912F_0 wire shape', async () => {
|
||||
const deps = makeDeps();
|
||||
await ClearFriendRemark.invoke(deps, { userId: 10001 });
|
||||
const [wireName, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const body = m(1, s(7, 'resolved-uid'));
|
||||
expect({
|
||||
wireName,
|
||||
hex: Buffer.from(bytes).toString('hex'),
|
||||
}).toEqual({
|
||||
wireName: 'OidbSvcTrpcTcp.0x912f_0',
|
||||
hex: env(0x912f, 0, body, false),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { protobuf_decode } from '@snowluma/proton';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type { OidbDeleteFriend } from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
|
||||
import { DeleteFriend } from '../../../src/oidb-services/friend/delete-friend';
|
||||
import { env, m, s, v } from '../_pb-oracle';
|
||||
|
||||
function makeDeps() {
|
||||
const r: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.alloc(0),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async () => r),
|
||||
resolveUserUid: vi.fn(async () => 'resolved-uid'),
|
||||
};
|
||||
}
|
||||
|
||||
describe('DeleteFriend namespace', () => {
|
||||
it('declares 0x126B_0', () => {
|
||||
expect(DeleteFriend.command).toBe(0x126b);
|
||||
expect(DeleteFriend.subCommand).toBe(0);
|
||||
});
|
||||
|
||||
describe('invoke', () => {
|
||||
it('routes to OidbSvcTrpcTcp.0x126b_0', async () => {
|
||||
const deps = makeDeps();
|
||||
await DeleteFriend.invoke(deps, { userId: 10001 });
|
||||
expect(deps.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0x126b_0');
|
||||
});
|
||||
|
||||
it('resolves the target uid before encoding', async () => {
|
||||
const deps = makeDeps();
|
||||
await DeleteFriend.invoke(deps, { userId: 10001 });
|
||||
expect(deps.resolveUserUid).toHaveBeenCalledWith(10001);
|
||||
});
|
||||
|
||||
it('packages the nested envelope with verbatim magic constants', async () => {
|
||||
const deps = makeDeps();
|
||||
await DeleteFriend.invoke(deps, { userId: 10001, block: true });
|
||||
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const env = protobuf_decode<OidbBase<OidbDeleteFriend>>(bytes);
|
||||
expect(env.body?.field1).toMatchObject({
|
||||
targetUid: 'resolved-uid',
|
||||
block: true,
|
||||
field2: {
|
||||
field1: 130,
|
||||
field2: 109,
|
||||
field3: { field1: 8, field2: 8, field3: 50 },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('matches the current QQ wire for a non-blocking delete', async () => {
|
||||
const deps = makeDeps();
|
||||
await DeleteFriend.invoke(deps, { userId: 10001 });
|
||||
const [wireName, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const discriminator = [
|
||||
...v(1, 130),
|
||||
...v(2, 109),
|
||||
...m(3, [...v(1, 8), ...v(2, 8), ...v(3, 50)]),
|
||||
];
|
||||
const body = m(1, [
|
||||
...s(1, 'resolved-uid'),
|
||||
...m(2, discriminator),
|
||||
// Current QQ explicitly emits both booleans.
|
||||
...v(3, 0),
|
||||
...v(4, 1),
|
||||
]);
|
||||
expect({
|
||||
wireName,
|
||||
hex: Buffer.from(bytes).toString('hex'),
|
||||
}).toEqual({
|
||||
wireName: 'OidbSvcTrpcTcp.0x126b_0',
|
||||
hex: env(0x126b, 0, body, false),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
08e91a22a60c080112a10c0a7e0880c2d72f12066e69636b3030180020fe012a056d73673030320851512067726f75703a1766696c746572656420667269656e6420726571756573744080c3bbc2064880cee4cd02500062006a30abc8a360ea075bcf8bbfa1e1e0e010beb1ac5228138eb82586704b41691e9efea640392a2a65e9dd50de82f154e0201b0a7d08c7aa9b3012066e69636b3031180020012a056d73673031320851512067726f75703a1766696c746572656420667269656e6420726571756573744080e6c0c206488dcee4cd02500162006a30ee591d017a9bb6807f7db7c8d3665e34923c7db6f6252d672a8f1f8986c7c9e3d21415ea86987a1b689c8d675d2a26800a7d088e93df3012066e69636b3032180020012a056d73673032320851512067726f75703a1766696c746572656420667269656e642072657175657374408089c6c206489acee4cd02500062006a305fe9e2cce8e6bc04c4f76fe8872347907e73a73b9184c3a7cbbd9c85a1d319c252129d950d06920d50732964df4afcf20a810108d5fba23112066e69636b3033180020ffffffff0f2a056d73673033320851512067726f75703a1766696c746572656420667269656e6420726571756573744080accbc20648a7cee4cd02500062006a3006b881debd01f92c98ef3c1726406ebeb88cae9d12d89e9fa36c20a77495dd0a3eb9fbbe7b14f65d657e979adfda5c600a7d089ce4e63112066e69636b3034180020012a056d73673034320851512067726f75703a1766696c746572656420667269656e6420726571756573744080cfd0c20648b4cee4cd02500062006a30316f76c654c33c8f6f3689a9475d8b871f74654789edb72d3f1a87bb1ba11cf07286b8d748f4d08d6fbe5afee8208e010a870108e3ccaa3212066e69636b3035185920ffffffff0f2a056d73673035320851512067726f75703a1766696c746572656420667269656e6420726571756573744080f2d5c20648c1cee4cd0250006206e4b89ce4baac6a30a3038a382c30ee6a8686410089e1c618f0b11bf92fdf2726da2910d2082a147a5569d04815e7c42287b424ef45b00ff70a860108aab5ee3212066e69636b3036180120002a056d73673036320851512067726f75703a1766696c746572656420667269656e642072657175657374408095dbc20648cecee4cd0250006209e7a59ee5a588e5b79d6a3097d6cc1f21ca290ab424147c3181e68490c49de47eda96eb422ec861eefdc8f580578655ac4f874ab7e8467cfc0e9fd30a810108f19db23312066e69636b3037180020ffffffff0f2a056d73673037320851512067726f75703a1766696c746572656420667269656e6420726571756573744080b8e0c20648dbcee4cd02500062006a303c1421acb295e3a5a4c914625ef1dbee99b552716ed28046cfa3b9171518dab5c247e0ddd3a24cb5f318a1cb60d6c9b30a810108b886f63312066e69636b3038180020ffffffff0f2a056d73673038320851512067726f75703a1766696c746572656420667269656e6420726571756573744080dbe5c20648e8cee4cd02500162006a3020ac3ddb705e3246f8fe7d266becea5bf92ec584a2cc12d4a83197d782047ca61d0ed90c5c4419e0866940f22a3eb1420a7d08ffeeb93412066e69636b3039180020012a056d73673039320851512067726f75703a1766696c746572656420667269656e6420726571756573744080feeac20648f5cee4cd02500162006a3047da9cf4f576624c5edd21b6bcfda8898c91143711f0bd251e00dfb1c273738290fa190cf6dc231c41dc31ad2086c6050a7d08c6d7fd3412066e69636b3130181420002a056d73673130320851512067726f75703a1766696c746572656420667269656e6420726571756573744080a1f0c2064882cfe4cd02500062006a300ab4598cfe36f3f340ec5237766213a06ddd99d900c3b3ac4d18530d4308f17943a22ef6b85d4d3f08086dcaf36465550a8401088dc0c13512066e69636b3131181220fe012a056d73673131320851512067726f75703a1766696c746572656420667269656e6420726571756573744080c4f5c206488fcfe4cd0250016206e697a0e994a16a3006e504c8f85b64fa8d5cb196d4b8cebc8098c752d879880bd8ab0108a829a1b29f952ff4e4b3807af4d54f70fb0dbf50
|
||||
@@ -0,0 +1,183 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type { OidbDoubtGetResp } from '@snowluma/proto-defs/oidb-actions/doubt-buddy';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
import { GetDoubtBuddyReq } from '../../../src/oidb-services/friend/get-doubt-buddy-req';
|
||||
import { env, v, s, m } from '../_pb-oracle';
|
||||
|
||||
function makeSender(): { sendRawPacket: ReturnType<typeof vi.fn> } {
|
||||
const respEnv: OidbBase<OidbDoubtGetResp> = {
|
||||
command: 0xd69,
|
||||
subCommand: 0,
|
||||
body: {
|
||||
status: 1,
|
||||
body: {
|
||||
list: [
|
||||
{
|
||||
uid: 'u_alice',
|
||||
nick: 'Alice',
|
||||
source: '可能认识',
|
||||
reason: '',
|
||||
msg: 'hi',
|
||||
reqTime: 1700000000n,
|
||||
},
|
||||
],
|
||||
reason: '',
|
||||
},
|
||||
},
|
||||
};
|
||||
const r: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(protobuf_encode<OidbBase<OidbDoubtGetResp>>(respEnv)),
|
||||
};
|
||||
return { sendRawPacket: vi.fn(async () => r) };
|
||||
}
|
||||
|
||||
describe('GetDoubtBuddyReq namespace', () => {
|
||||
it('declares command 0xD69 sub 0, uin form', () => {
|
||||
expect(GetDoubtBuddyReq.command).toBe(0xd69);
|
||||
expect(GetDoubtBuddyReq.subCommand).toBe(0);
|
||||
expect(GetDoubtBuddyReq.uinForm).toBe(true);
|
||||
});
|
||||
|
||||
it('serializes {1:1, 2:{1:count, 2:uk}} (reqId is not on the wire)', () => {
|
||||
expect(GetDoubtBuddyReq.serialize({} as any, { count: 50 })).toEqual({
|
||||
field1: 1,
|
||||
inner: { num: 50, uk: '' },
|
||||
});
|
||||
});
|
||||
|
||||
it('byte-oracle: routes to 0xd69_0 and locks nested req tags {1:const1, 2:{1:num, 2:uk}}', async () => {
|
||||
const sender = makeSender();
|
||||
await GetDoubtBuddyReq.invoke(sender, { count: 50 });
|
||||
|
||||
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
|
||||
expect(cmd).toBe('OidbSvcTrpcTcp.0xd69_0');
|
||||
// uk '' omitted (proto3 default); inner carries only num.
|
||||
const inner = [...v(1, 50)];
|
||||
const body = [...v(1, 1), ...m(2, inner)];
|
||||
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xd69, 0, body, true));
|
||||
});
|
||||
|
||||
it('deserializes the string uid item to the OneBot shape', async () => {
|
||||
const list = await GetDoubtBuddyReq.invoke(makeSender(), { count: 10 });
|
||||
expect(list).toEqual([
|
||||
{
|
||||
uid: 'u_alice',
|
||||
user_id: 0,
|
||||
nick: 'Alice',
|
||||
source: '可能认识',
|
||||
reason: '',
|
||||
msg: 'hi',
|
||||
group_code: '',
|
||||
reqTime: 1700000000,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('deserialize returns [] when the body has no list', () => {
|
||||
expect(GetDoubtBuddyReq.deserialize({} as any, { status: 1 })).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps an empty uid when the reply only carries the account number', () => {
|
||||
expect(
|
||||
GetDoubtBuddyReq.deserialize({} as any, {
|
||||
status: 1,
|
||||
body: { list: [{ nick: 'Alice', uin: 12345n, reqTime: 1700000000n }] },
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
uid: '',
|
||||
user_id: 12345,
|
||||
nick: 'Alice',
|
||||
source: '',
|
||||
reason: '',
|
||||
msg: '',
|
||||
group_code: '',
|
||||
reqTime: 1700000000,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps the uid string and does not reread it as the account number', async () => {
|
||||
const item = [
|
||||
...s(1, 'u_alice'),
|
||||
...s(2, 'Alice'),
|
||||
...s(5, 'hi'),
|
||||
...s(6, 'QQ号查找'),
|
||||
...s(7, 'caution'),
|
||||
];
|
||||
const sender = {
|
||||
sendRawPacket: vi.fn(async () => ({
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(
|
||||
env(0xd69, 0, [...v(1, 1), ...m(2, m(1, item))], false),
|
||||
'hex',
|
||||
),
|
||||
})),
|
||||
};
|
||||
const list = await GetDoubtBuddyReq.invoke(sender, { count: 10 });
|
||||
expect(list).toEqual([
|
||||
{
|
||||
uid: 'u_alice',
|
||||
user_id: 0,
|
||||
nick: 'Alice',
|
||||
source: 'QQ号查找',
|
||||
reason: 'caution',
|
||||
msg: 'hi',
|
||||
group_code: '',
|
||||
reqTime: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
// Live capture from QQ 3.2.32-52194 on Linux. Locks the real wire shape:
|
||||
// tag1 is the applicant's account number (not a string uid), tag8 is the
|
||||
// request unix time, and tag9 is the source group. Before this, a tag8-as-uin
|
||||
// read made the uin->uid fallback query the request time as an account.
|
||||
it('decodes a captured Linux reply: account at tag1, time at tag8, group at tag9', () => {
|
||||
const hex = readFileSync(
|
||||
fileURLToPath(new URL('./fixtures/d69-linux-3.2.32.hex', import.meta.url)),
|
||||
'utf8',
|
||||
).trim();
|
||||
const list = GetDoubtBuddyReq.deserialize(
|
||||
{} as any,
|
||||
GetDoubtBuddyReq.decode(Buffer.from(hex, 'hex')).body!,
|
||||
);
|
||||
|
||||
expect(list).toHaveLength(12);
|
||||
expect(list[0]).toMatchObject({
|
||||
uid: '',
|
||||
user_id: 100000000,
|
||||
nick: 'nick00',
|
||||
source: 'QQ group',
|
||||
reason: 'filtered friend request',
|
||||
msg: 'msg00',
|
||||
group_code: '700000000',
|
||||
reqTime: 1750000000,
|
||||
});
|
||||
expect(list[1]).toMatchObject({
|
||||
uid: '',
|
||||
user_id: 101111111,
|
||||
group_code: '700000013',
|
||||
reqTime: 1750086400,
|
||||
});
|
||||
|
||||
// Account numbers stay in the QQ range; request times land in 2025-2026.
|
||||
for (const item of list) {
|
||||
expect(item.user_id).toBeGreaterThan(100000);
|
||||
expect(item.user_id).toBeLessThan(4294967296);
|
||||
expect(item.reqTime).toBeGreaterThan(1700000000);
|
||||
expect(item.reqTime).toBeLessThan(2000000000);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { protobuf_decode } from '@snowluma/proton';
|
||||
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
||||
import type { OidbFriendRequestAction } from '@snowluma/proto-defs/oidb-actions/base';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
|
||||
import { HandleFriendRequest } from '../../../src/oidb-services/friend/handle-friend-request';
|
||||
|
||||
function makeDeps(resolvedUid = 'resolved-uid') {
|
||||
const r: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.alloc(0),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async () => r),
|
||||
resolveUserUid: vi.fn(async () => resolvedUid),
|
||||
};
|
||||
}
|
||||
|
||||
describe('HandleFriendRequest namespace', () => {
|
||||
it('declares 0xB5D_44', () => {
|
||||
expect(HandleFriendRequest.command).toBe(0xb5d);
|
||||
expect(HandleFriendRequest.subCommand).toBe(44);
|
||||
});
|
||||
|
||||
describe('invoke', () => {
|
||||
it('routes to OidbSvcTrpcTcp.0xb5d_44', async () => {
|
||||
const deps = makeDeps();
|
||||
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'uid', approve: true });
|
||||
expect(deps.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0xb5d_44');
|
||||
});
|
||||
|
||||
it('detects digit-only flag → resolves via resolveUserUid', async () => {
|
||||
const deps = makeDeps('resolved-from-uin');
|
||||
await HandleFriendRequest.invoke(deps, { uidOrFlag: '10001', approve: true });
|
||||
expect(deps.resolveUserUid).toHaveBeenCalledWith(10001);
|
||||
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
|
||||
expect(env.body?.targetUid).toBe('resolved-from-uin');
|
||||
});
|
||||
|
||||
it('passes non-numeric flag through verbatim (no resolveUserUid call)', async () => {
|
||||
const deps = makeDeps();
|
||||
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'flag-abc', approve: false });
|
||||
expect(deps.resolveUserUid).not.toHaveBeenCalled();
|
||||
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
|
||||
expect(env.body?.targetUid).toBe('flag-abc');
|
||||
});
|
||||
|
||||
it('encodes accept=3 when approve=true', async () => {
|
||||
const deps = makeDeps();
|
||||
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'u', approve: true });
|
||||
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
|
||||
expect(env.body?.accept).toBe(3);
|
||||
});
|
||||
|
||||
it('encodes accept=5 when approve=false', async () => {
|
||||
const deps = makeDeps();
|
||||
await HandleFriendRequest.invoke(deps, { uidOrFlag: 'u', approve: false });
|
||||
const [, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const env = protobuf_decode<OidbBase<OidbFriendRequestAction>>(bytes);
|
||||
expect(env.body?.accept).toBe(5);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { protobuf_encode } from '@snowluma/proton';
|
||||
import type { OidbBase, OidbEmpty } from '@snowluma/proto-defs/oidb';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
import { RejectDoubtBuddyReq } from '../../../src/oidb-services/friend/reject-doubt-buddy-req';
|
||||
import { env, v, s, m } from '../_pb-oracle';
|
||||
|
||||
function makeSender() {
|
||||
const respEnv: OidbBase<OidbEmpty> = { command: 0xd69, subCommand: 0, body: {} };
|
||||
const r: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(protobuf_encode<OidbBase<OidbEmpty>>(respEnv)),
|
||||
};
|
||||
return { sendRawPacket: vi.fn(async () => r) };
|
||||
}
|
||||
|
||||
describe('RejectDoubtBuddyReq namespace', () => {
|
||||
it('declares command 0xD69 sub 0, uin form', () => {
|
||||
expect(RejectDoubtBuddyReq.command).toBe(0xd69);
|
||||
expect(RejectDoubtBuddyReq.subCommand).toBe(0);
|
||||
expect(RejectDoubtBuddyReq.uinForm).toBe(true);
|
||||
});
|
||||
|
||||
it('serializes {1: const 3 (op discriminator), 3: {1: uid}}', () => {
|
||||
expect(RejectDoubtBuddyReq.serialize({} as any, { uid: 'u_x' })).toEqual({
|
||||
field1: 3,
|
||||
inner: { uid: 'u_x' },
|
||||
});
|
||||
});
|
||||
|
||||
it('byte-oracle: routes to 0xd69_0 and locks req tags {1:const3, 3:{1:uid}}', async () => {
|
||||
const sender = makeSender();
|
||||
await RejectDoubtBuddyReq.invoke(sender, { uid: 'u_abc' });
|
||||
|
||||
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
|
||||
expect(cmd).toBe('OidbSvcTrpcTcp.0xd69_0');
|
||||
const inner = [...s(1, 'u_abc')];
|
||||
const body = [...v(1, 3), ...m(3, inner)];
|
||||
expect(Buffer.from(bytes).toString('hex')).toBe(env(0xd69, 0, body, true));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { subscribeLogs, type LogEntry } from '@snowluma/common/logger';
|
||||
import type { SendPacketResult } from '@snowluma/common/packet-sender';
|
||||
|
||||
import { SetFriendRemark } from '../../../src/oidb-services/friend/set-friend-remark';
|
||||
import { env, m, s, v } from '../_pb-oracle';
|
||||
|
||||
function successBody(uid = 'resolved-uid', remark = 'best-friend', uin = 0): number[] {
|
||||
return m(1, [...m(1, [...(uin > 0 ? v(3, uin) : []), ...s(7, uid)]), ...s(2, remark)]);
|
||||
}
|
||||
|
||||
function makeDeps(responseBody = successBody()) {
|
||||
const r: SendPacketResult = {
|
||||
success: true,
|
||||
gotResponse: true,
|
||||
errorCode: 0,
|
||||
errorMessage: '',
|
||||
responseData: Buffer.from(env(0x912e, 0, responseBody, false), 'hex'),
|
||||
};
|
||||
return {
|
||||
sendRawPacket: vi.fn(async () => r),
|
||||
resolveUserUid: vi.fn(async () => 'resolved-uid'),
|
||||
identity: { updateFriendRemark: vi.fn() },
|
||||
};
|
||||
}
|
||||
|
||||
describe('SetFriendRemark namespace', () => {
|
||||
it('sends the current QQ 0x912E_0 wire shape for a non-empty remark', async () => {
|
||||
const deps = makeDeps();
|
||||
await SetFriendRemark.invoke(deps, { userId: 10001, remark: 'best-friend' });
|
||||
const [wireName, bytes] = deps.sendRawPacket.mock.calls[0]!;
|
||||
const body = [
|
||||
...m(1, [...m(1, s(7, 'resolved-uid')), ...s(2, 'best-friend')]),
|
||||
// QQ explicitly emits scene=0 for an ordinary friend.
|
||||
...v(2, 0),
|
||||
];
|
||||
expect({
|
||||
wireName,
|
||||
hex: Buffer.from(bytes).toString('hex'),
|
||||
}).toEqual({
|
||||
wireName: 'OidbSvcTrpcTcp.0x912e_0',
|
||||
hex: env(0x912e, 0, body, false),
|
||||
});
|
||||
});
|
||||
|
||||
it('resolves the target UID before encoding', async () => {
|
||||
const deps = makeDeps();
|
||||
await SetFriendRemark.invoke(deps, { userId: 10001, remark: 'best-friend' });
|
||||
expect(deps.resolveUserUid).toHaveBeenCalledWith(10001);
|
||||
});
|
||||
|
||||
it('updates identity only after the server confirms the requested value', async () => {
|
||||
const deps = makeDeps(successBody('resolved-uid', 'best-friend', 10001));
|
||||
|
||||
await SetFriendRemark.invoke(deps, { userId: 10001, remark: 'best-friend' });
|
||||
|
||||
expect(deps.identity.updateFriendRemark).toHaveBeenCalledWith(
|
||||
'resolved-uid',
|
||||
10001,
|
||||
'best-friend',
|
||||
);
|
||||
});
|
||||
|
||||
it('reports local synchronization failures without retrying a confirmed remote update', async () => {
|
||||
const deps = makeDeps(successBody('resolved-uid', 'best-friend', 10001));
|
||||
deps.identity.updateFriendRemark.mockImplementation(() => {
|
||||
throw new Error('identity cache unavailable');
|
||||
});
|
||||
const captured: LogEntry[] = [];
|
||||
const unsubscribe = subscribeLogs((entry) => {
|
||||
if (entry.scope === 'Bridge.Friend') {
|
||||
captured.push(entry);
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await expect(
|
||||
SetFriendRemark.invoke(deps, {
|
||||
userId: 10001,
|
||||
remark: 'best-friend',
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
} finally {
|
||||
unsubscribe();
|
||||
}
|
||||
|
||||
expect(deps.sendRawPacket).toHaveBeenCalledTimes(1);
|
||||
expect(captured).toContainEqual(
|
||||
expect.objectContaining({
|
||||
level: 'error',
|
||||
message: expect.stringContaining('identity cache unavailable'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects a business failure carried inside a successful OIDB response', async () => {
|
||||
const deps = makeDeps(m(3, [...v(2, 120), ...s(3, 'remark rejected')]));
|
||||
|
||||
await expect(
|
||||
SetFriendRemark.invoke(deps, {
|
||||
userId: 10001,
|
||||
remark: 'best-friend',
|
||||
}),
|
||||
).rejects.toThrow(/remark rejected/);
|
||||
expect(deps.identity.updateFriendRemark).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects a response that contains neither a result nor a business error', async () => {
|
||||
const deps = makeDeps([]);
|
||||
|
||||
await expect(
|
||||
SetFriendRemark.invoke(deps, {
|
||||
userId: 10001,
|
||||
remark: 'best-friend',
|
||||
}),
|
||||
).rejects.toThrow(/missing result/);
|
||||
});
|
||||
|
||||
it('rejects a success result for a different target', async () => {
|
||||
const deps = makeDeps(successBody('another-uid'));
|
||||
|
||||
await expect(
|
||||
SetFriendRemark.invoke(deps, {
|
||||
userId: 10001,
|
||||
remark: 'best-friend',
|
||||
}),
|
||||
).rejects.toThrow(/target mismatch/);
|
||||
});
|
||||
|
||||
it('rejects a success result that does not confirm the requested remark', async () => {
|
||||
const deps = makeDeps(successBody('resolved-uid', 'old-remark'));
|
||||
|
||||
await expect(
|
||||
SetFriendRemark.invoke(deps, {
|
||||
userId: 10001,
|
||||
remark: 'best-friend',
|
||||
}),
|
||||
).rejects.toThrow(/does not match/);
|
||||
});
|
||||
|
||||
it('rejects a success result carrying a different numeric account', async () => {
|
||||
const deps = makeDeps(successBody('resolved-uid', 'best-friend', 20002));
|
||||
|
||||
await expect(
|
||||
SetFriendRemark.invoke(deps, {
|
||||
userId: 10001,
|
||||
remark: 'best-friend',
|
||||
}),
|
||||
).rejects.toThrow(/expected UIN 10001, got 20002/);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user