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
152 lines
5.3 KiB
TypeScript
152 lines
5.3 KiB
TypeScript
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/);
|
|
});
|
|
});
|