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
563 lines
22 KiB
TypeScript
563 lines
22 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
|
|
import type { OidbBase } from '@snowluma/proto-defs/oidb';
|
|
import type {
|
|
CustomFaceModifyResp,
|
|
CustomFaceMoveBody,
|
|
FaceroamOpResp,
|
|
GroupAvatarExtra,
|
|
Oidb0x7edResp,
|
|
Oidb0xe17Resp,
|
|
SetStatusReq,
|
|
SetStatusResp,
|
|
} from '@snowluma/proto-defs/oidb-actions/base';
|
|
|
|
vi.mock('@snowluma/protocol/bridge-oidb', async () => {
|
|
const actual = await vi.importActual<typeof import('@snowluma/protocol/bridge-oidb')>(
|
|
'@snowluma/protocol/bridge-oidb',
|
|
);
|
|
return {
|
|
...actual,
|
|
makeOidbEnvelope: vi.fn((_oidbCmd, _subCmd, body) => ({ body })),
|
|
};
|
|
});
|
|
|
|
vi.mock('@snowluma/protocol/highway', () => ({
|
|
fetchHighwaySession: vi.fn(async () => ({})),
|
|
uploadHighwayHttp: vi.fn(async () => undefined),
|
|
BufferChunkSource: class BufferChunkSource {
|
|
constructor(readonly bytes: Uint8Array) {}
|
|
},
|
|
}));
|
|
|
|
vi.mock('@snowluma/protocol/highway/utils', () => ({
|
|
loadBinarySource: vi.fn(async () => ({
|
|
bytes: new Uint8Array([1, 2, 3]),
|
|
fileName: 'avatar.bin',
|
|
})),
|
|
computeHashes: vi.fn(() => ({ md5: new Uint8Array(16), sha1: new Uint8Array(20) })),
|
|
computeMd5: vi.fn(() => new Uint8Array(16)),
|
|
}));
|
|
|
|
import * as oidb from '@snowluma/protocol/bridge-oidb';
|
|
import * as highwayClient from '@snowluma/protocol/highway';
|
|
import { ProfileApi } from '../../src/bridge/apis/profile';
|
|
import { mockBridge } from './_helpers';
|
|
|
|
describe('apis/profile', () => {
|
|
beforeEach(() => {
|
|
vi.mocked(oidb.makeOidbEnvelope).mockClear();
|
|
vi.mocked(highwayClient.fetchHighwaySession).mockClear();
|
|
vi.mocked(highwayClient.uploadHighwayHttp).mockClear();
|
|
});
|
|
|
|
it('setOnlineStatus sends to status_svc.SetStatus and accepts an empty response', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setOnlineStatus(11, 0, 100);
|
|
const [serviceCmd] = bridge.sendRawPacket.mock.calls[0]!;
|
|
expect(serviceCmd).toBe('trpc.qq_new_tech.status_svc.StatusService.SetStatus');
|
|
});
|
|
|
|
it('setOnlineStatus does NOT include the customExt field 4 (varint 0x22 absent in body)', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setOnlineStatus(11, 0, 100);
|
|
const [, body] = bridge.sendRawPacket.mock.calls[0]!;
|
|
// proto field 4 length-delimited tag = (4 << 3) | 2 = 0x22. The
|
|
// body should not contain that byte; if encoder ever leaks an
|
|
// empty customExt it'd appear here.
|
|
expect(Buffer.from(body).includes(0x22)).toBe(false);
|
|
});
|
|
|
|
it('setDiyOnlineStatus hardcodes status=10 / extStatus=2000 and packs faceId+wording+faceType into customExt', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setDiyOnlineStatus(1234, '摸鱼中', 2);
|
|
const [serviceCmd, body] = bridge.sendRawPacket.mock.calls[0]!;
|
|
expect(serviceCmd).toBe('trpc.qq_new_tech.status_svc.StatusService.SetStatus');
|
|
// Decode the wire bytes back through the same schema to assert
|
|
// every field landed in the right place.
|
|
const decoded = protobuf_decode<SetStatusReq>(body as Uint8Array);
|
|
expect(decoded).toMatchObject({
|
|
status: 10,
|
|
extStatus: 2000,
|
|
customExt: { faceId: 1234, text: '摸鱼中', faceType: 2 },
|
|
});
|
|
expect(decoded.batteryStatus ?? 0).toBe(0);
|
|
});
|
|
|
|
it("setOnlineStatus accepts QQ's success reply even when the reply code is non-zero (#405)", async () => {
|
|
const bridge = mockBridge();
|
|
const respBuf = Buffer.from(
|
|
protobuf_encode<SetStatusResp>({
|
|
errCode: 1,
|
|
errMsg: 'set status success',
|
|
}),
|
|
);
|
|
expect(respBuf.length).toBeGreaterThan(0);
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: respBuf,
|
|
} as any);
|
|
await expect(
|
|
new ProfileApi(bridge as any).setOnlineStatus(10, 0, 100),
|
|
).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('setOnlineStatus accepts a success-only reply body (#405)', async () => {
|
|
const bridge = mockBridge();
|
|
const respBuf = Buffer.from(
|
|
protobuf_encode<SetStatusResp>({ errMsg: 'set status success' }),
|
|
);
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: respBuf,
|
|
} as any);
|
|
await expect(new ProfileApi(bridge as any).setOnlineStatus(10, 0)).resolves.toBeUndefined();
|
|
});
|
|
|
|
it('setDiyOnlineStatus surfaces server errors via the same path as setOnlineStatus', async () => {
|
|
const bridge = mockBridge();
|
|
const respBuf = Buffer.from(
|
|
protobuf_encode<SetStatusResp>({ errCode: 1, errMsg: 'denied' }),
|
|
);
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: respBuf,
|
|
} as any);
|
|
await expect(new ProfileApi(bridge as any).setDiyOnlineStatus(1, 't', 1)).rejects.toThrow(
|
|
/denied/,
|
|
);
|
|
});
|
|
|
|
it('setDiyOnlineStatus rejects when the transport itself fails', async () => {
|
|
const bridge = mockBridge();
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: false,
|
|
gotResponse: false,
|
|
errorCode: -1,
|
|
errorMessage: 'pipe closed',
|
|
responseData: null,
|
|
} as any);
|
|
await expect(new ProfileApi(bridge as any).setDiyOnlineStatus(1, 't', 1)).rejects.toThrow(
|
|
/pipe closed/,
|
|
);
|
|
});
|
|
|
|
it('setProfile is a no-op when both arguments are undefined', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setProfile();
|
|
// Namespace migration: no SSO packet should fire at all.
|
|
expect(bridge.sendRawPacket).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('setProfile only sends non-undefined fields', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setProfile('New Nick');
|
|
expect(bridge.sendRawPacket).toHaveBeenCalledOnce();
|
|
expect(bridge.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0x112a_2');
|
|
});
|
|
|
|
it('setSelfLongNick targets 0x112a_2 wire cmd', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setSelfLongNick('hello world');
|
|
expect(bridge.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0x112a_2');
|
|
});
|
|
|
|
it('setInputStatus resolves UID first and sends 0xcd4_1', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setInputStatus(10001, 1);
|
|
expect(bridge.resolveUserUid).toHaveBeenCalledWith(10001);
|
|
expect(bridge.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0xcd4_1');
|
|
});
|
|
|
|
it('setAvatar loads bytes and pushes through the highway upload path (cmd 90)', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setAvatar('/some/avatar.png');
|
|
expect(highwayClient.fetchHighwaySession).toHaveBeenCalledOnce();
|
|
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
|
|
const [, , cmdId, , , extend] = vi.mocked(highwayClient.uploadHighwayHttp).mock.calls[0]!;
|
|
expect(cmdId).toBe(90);
|
|
expect((extend as Uint8Array).length).toBe(0); // personal avatar has no extra payload
|
|
});
|
|
|
|
it('setGroupAvatar uses cmdId 3000 and packs the Lagrange GroupAvatarExtra constants', async () => {
|
|
const bridge = mockBridge();
|
|
await new ProfileApi(bridge as any).setGroupAvatar(12345, '/some/group-avatar.png');
|
|
expect(highwayClient.fetchHighwaySession).toHaveBeenCalledOnce();
|
|
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
|
|
const [, , cmdId, , , extend] = vi.mocked(highwayClient.uploadHighwayHttp).mock.calls[0]!;
|
|
expect(cmdId).toBe(3000);
|
|
// Decode the extra blob back through the schema and assert every
|
|
// protocol-prescribed constant lands where it should.
|
|
const decoded = protobuf_decode<GroupAvatarExtra>(extend as Uint8Array);
|
|
expect(decoded).toEqual({
|
|
type: 101,
|
|
groupUin: 12345,
|
|
field3: { field1: 1 },
|
|
field5: 3,
|
|
field6: 1,
|
|
});
|
|
});
|
|
|
|
it('setGroupAvatar rejects an empty file before hitting highway', async () => {
|
|
const bridge = mockBridge();
|
|
const { loadBinarySource } = await import('@snowluma/protocol/highway/utils');
|
|
vi.mocked(loadBinarySource).mockResolvedValueOnce({
|
|
bytes: new Uint8Array(0),
|
|
fileName: 'empty.png',
|
|
} as any);
|
|
await expect(new ProfileApi(bridge as any).setGroupAvatar(1, 'empty.png')).rejects.toThrow(
|
|
/empty/,
|
|
);
|
|
expect(highwayClient.fetchHighwaySession).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('getProfileLike (self): resolves self UID, returns formatted favorite + vote info', async () => {
|
|
const bridge = mockBridge();
|
|
bridge.identity.findUinByUid.mockImplementation((uid: string) =>
|
|
uid === 'liker-uid' ? 20002 : 0,
|
|
);
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<OidbBase<Oidb0x7edResp>>({
|
|
body: {
|
|
userLikeInfos: [
|
|
{
|
|
uid: 'u',
|
|
time: 1700000000,
|
|
favoriteInfo: { totalCount: 5, lastTime: 1, todayCount: 1 },
|
|
voteInfo: {
|
|
totalCount: 7,
|
|
newCount: 2,
|
|
newNearbyCount: 1,
|
|
lastVisitTime: 2,
|
|
userInfos: [{ uid: 'liker-uid', nick: '点赞者', count: 3 }],
|
|
},
|
|
},
|
|
],
|
|
} as any,
|
|
}),
|
|
),
|
|
});
|
|
const out = await new ProfileApi(bridge as any).getLike();
|
|
expect(bridge.sendRawPacket.mock.calls[0]![0]).toBe('OidbSvcTrpcTcp.0x7ed_13');
|
|
expect(out.favoriteInfo.total_count).toBe(5);
|
|
expect(out.voteInfo.total_count).toBe(7);
|
|
expect(out.voteInfo.userInfos).toEqual([
|
|
expect.objectContaining({ uid: 'liker-uid', uin: 20002, nick: '点赞者', count: 3 }),
|
|
]);
|
|
});
|
|
|
|
it('getProfileLike throws on empty result', async () => {
|
|
const bridge = mockBridge();
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<OidbBase<Oidb0x7edResp>>({ body: { userLikeInfos: [] } as any }),
|
|
),
|
|
});
|
|
await expect(new ProfileApi(bridge as any).getLike()).rejects.toThrow(/empty/);
|
|
});
|
|
|
|
it('getProfileLike resolves uncached liker uins through Identity.resolveUin', async () => {
|
|
const bridge = mockBridge();
|
|
bridge.identity.findUinByUid.mockReturnValue(null);
|
|
bridge.identity.resolveUin.mockImplementation(async (uid: string) =>
|
|
uid === 'stranger-uid' ? 30003 : null,
|
|
);
|
|
const fetchUserProfileByUid = vi.fn();
|
|
(bridge.apis.contacts as any).fetchUserProfileByUid = fetchUserProfileByUid;
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<OidbBase<Oidb0x7edResp>>({
|
|
body: {
|
|
userLikeInfos: [
|
|
{
|
|
uid: 'self-uid',
|
|
voteInfo: {
|
|
userInfos: [{ uid: 'stranger-uid', nick: '陌生点赞者' }],
|
|
},
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
|
|
const out = await new ProfileApi(bridge as any).getLike();
|
|
|
|
expect(bridge.identity.resolveUin).toHaveBeenCalledOnce();
|
|
expect(bridge.identity.resolveUin).toHaveBeenCalledWith('stranger-uid');
|
|
expect(fetchUserProfileByUid).not.toHaveBeenCalled();
|
|
expect(out.voteInfo.userInfos[0]?.uin).toBe(30003);
|
|
});
|
|
|
|
it('getProfileLike keeps uin=0 when Identity.resolveUin misses', async () => {
|
|
const bridge = mockBridge();
|
|
bridge.identity.findUinByUid.mockReturnValue(null);
|
|
bridge.identity.resolveUin.mockResolvedValue(null);
|
|
const fetchUserProfileByUid = vi.fn(async () => {
|
|
throw new Error('profile lookup failed');
|
|
});
|
|
(bridge.apis.contacts as any).fetchUserProfileByUid = fetchUserProfileByUid;
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<OidbBase<Oidb0x7edResp>>({
|
|
body: {
|
|
userLikeInfos: [
|
|
{
|
|
uid: 'self-uid',
|
|
voteInfo: { userInfos: [{ uid: 'stranger-uid' }] },
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
),
|
|
});
|
|
|
|
const out = await new ProfileApi(bridge as any).getLike();
|
|
|
|
expect(out.voteInfo.userInfos[0]?.uin).toBe(0);
|
|
expect(fetchUserProfileByUid).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('getUnidirectionalFriendList parses the embedded JSON body', async () => {
|
|
const bridge = mockBridge();
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<Oidb0xe17Resp>({
|
|
jsonBody: JSON.stringify({
|
|
ActionStatus: 'OK',
|
|
ErrorCode: 0,
|
|
rpt_block_list: [{ uin: 10001 }, { uin: 10002 }],
|
|
}),
|
|
}),
|
|
),
|
|
});
|
|
const out = await new ProfileApi(bridge as any).getUnidirectionalFriendList();
|
|
expect(out).toEqual([{ uin: 10001 }, { uin: 10002 }]);
|
|
});
|
|
|
|
it('fetchCustomFaceIds slices the Faceroam list and skips the packet for count=0', async () => {
|
|
const bridge = mockBridge();
|
|
const emojiA = '10001_0_0_0_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0_0';
|
|
const emojiB = '10001_0_0_0_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB_0_0';
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<FaceroamOpResp>({
|
|
retCode: 0,
|
|
item: { faceIds: [emojiA, emojiB] },
|
|
}),
|
|
),
|
|
} as any);
|
|
|
|
const api = new ProfileApi(bridge as any);
|
|
await expect(api.fetchCustomFaceIds(1)).resolves.toEqual([emojiA]);
|
|
expect(bridge.sendRawPacket.mock.calls[0]![0]).toBe('Faceroam.OpReq');
|
|
|
|
await expect(api.fetchCustomFaceIds(0)).resolves.toEqual([]);
|
|
expect(bridge.sendRawPacket).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it('fetchCustomFace maps ids to image urls', async () => {
|
|
const bridge = mockBridge();
|
|
const emojiA = '10001_0_0_0_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0_0';
|
|
bridge.sendRawPacket.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<FaceroamOpResp>({
|
|
retCode: 0,
|
|
item: { faceIds: [emojiA] },
|
|
}),
|
|
),
|
|
} as any);
|
|
|
|
await expect(new ProfileApi(bridge as any).fetchCustomFace(1)).resolves.toEqual([
|
|
`https://p.qpic.cn/qq_expression/10001/${emojiA}/0`,
|
|
]);
|
|
});
|
|
|
|
it.each(['resource', 'md5'])(
|
|
'resolves a saved sticker by %s without fetching every description',
|
|
async (kind) => {
|
|
const bridge = mockBridge();
|
|
const md5 = 'ABCDEF0123456789ABCDEF0123456789';
|
|
const emojiId = `10001_0_0_0_${md5}_0_0`;
|
|
bridge.sendRawPacket.mockResolvedValue({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<FaceroamOpResp>({ item: { faceIds: [emojiId] } }),
|
|
),
|
|
} as any);
|
|
const api = new ProfileApi(bridge as any);
|
|
expect(
|
|
await api.resolveCustomFace(kind === 'md5' ? md5.toLowerCase() : emojiId),
|
|
).toEqual({
|
|
emojiId,
|
|
md5,
|
|
url: `https://p.qpic.cn/qq_expression/10001/${emojiId}/0`,
|
|
});
|
|
expect(bridge.sendRawPacket).toHaveBeenCalledTimes(1);
|
|
await expect(api.resolveCustomFace('missing')).rejects.toThrow(
|
|
/not in the saved sticker list/,
|
|
);
|
|
await expect(api.resolveCustomFace(' ')).rejects.toThrow(/identifier is empty/);
|
|
expect(bridge.sendRawPacket).toHaveBeenCalledTimes(2);
|
|
},
|
|
);
|
|
|
|
it('rejects a negative custom-face count before sending', async () => {
|
|
const bridge = mockBridge();
|
|
await expect(new ProfileApi(bridge as any).fetchCustomFaceIds(-1)).rejects.toThrow(
|
|
/non-negative integer/,
|
|
);
|
|
expect(bridge.sendRawPacket).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('moveCustomFaceToFront reorders from the id list without parsing urls', async () => {
|
|
const bridge = mockBridge();
|
|
const emojiA = '10001_0_0_0_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0_0';
|
|
const emojiB = '10001_0_0_0_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB_0_0';
|
|
bridge.sendRawPacket
|
|
.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<FaceroamOpResp>({
|
|
retCode: 0,
|
|
item: { faceIds: [emojiA, emojiB] },
|
|
}),
|
|
),
|
|
} as any)
|
|
.mockResolvedValue({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<OidbBase<CustomFaceModifyResp>>({
|
|
body: { retCode: 0 },
|
|
}),
|
|
),
|
|
} as any);
|
|
|
|
await new ProfileApi(bridge as any).moveCustomFaceToFront(emojiB);
|
|
|
|
expect(bridge.sendRawPacket.mock.calls.map((call) => call[0])).toEqual([
|
|
'Faceroam.OpReq',
|
|
'OidbSvcTrpcTcp.0x902f_1',
|
|
'OidbSvcTrpcTcp.0x902e_1',
|
|
]);
|
|
const moveBody = protobuf_decode<OidbBase<CustomFaceMoveBody>>(
|
|
bridge.sendRawPacket.mock.calls[2]![1],
|
|
);
|
|
expect(moveBody.body?.emojis?.map((emoji) => emoji.emojiId)).toEqual([emojiB, emojiA]);
|
|
});
|
|
|
|
it('fetchCustomFaceDetails supplements the Faceroam ids with server descriptions (#333)', async () => {
|
|
const bridge = mockBridge();
|
|
const emojiA = '10001_0_0_0_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA_0_0';
|
|
const emojiB = '10001_0_0_0_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB_0_0';
|
|
bridge.sendRawPacket
|
|
.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<FaceroamOpResp>({
|
|
retCode: 0,
|
|
item: { faceIds: [emojiA, emojiB] },
|
|
}),
|
|
),
|
|
} as any)
|
|
.mockResolvedValueOnce({
|
|
success: true,
|
|
gotResponse: true,
|
|
errorCode: 0,
|
|
errorMessage: '',
|
|
responseData: Buffer.from(
|
|
protobuf_encode<OidbBase<CustomFaceModifyResp>>({
|
|
body: {
|
|
retCode: 0,
|
|
entries: [{ emoji: { emojiId: emojiB }, desc: '第二个表情' }],
|
|
},
|
|
}),
|
|
),
|
|
} as any);
|
|
|
|
const out = await new ProfileApi(bridge as any).fetchCustomFaceDetails(2);
|
|
|
|
expect(bridge.sendRawPacket.mock.calls.map((call) => call[0])).toEqual([
|
|
'Faceroam.OpReq',
|
|
'OidbSvcTrpcTcp.0x902e_1',
|
|
]);
|
|
const detailRequest = protobuf_decode<OidbBase<CustomFaceMoveBody>>(
|
|
bridge.sendRawPacket.mock.calls[1]![1],
|
|
);
|
|
expect(detailRequest.body?.emojis).toEqual([
|
|
{ emojiId: emojiA, md5: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' },
|
|
{ emojiId: emojiB, md5: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB' },
|
|
]);
|
|
expect(out).toEqual([
|
|
{
|
|
emojiId: emojiA,
|
|
url: `https://p.qpic.cn/qq_expression/10001/${emojiA}/0`,
|
|
md5: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
|
|
desc: '',
|
|
},
|
|
{
|
|
emojiId: emojiB,
|
|
url: `https://p.qpic.cn/qq_expression/10001/${emojiB}/0`,
|
|
md5: 'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
|
|
desc: '第二个表情',
|
|
},
|
|
]);
|
|
});
|
|
|
|
it('fetchCustomFaceDetails returns immediately for count=0', async () => {
|
|
const bridge = mockBridge();
|
|
await expect(new ProfileApi(bridge as any).fetchCustomFaceDetails(0)).resolves.toEqual([]);
|
|
expect(bridge.sendRawPacket).not.toHaveBeenCalled();
|
|
});
|
|
});
|