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

This commit is contained in:
TanTan committed 2026-11-13 09:02:00 +08:00
commit fa9c73363d
1367 files changed
+355735

No files matched your search

@@ -0,0 +1,102 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileFolderReq,
OidbGroupFileFolderResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { CreateGroupFolder } from '../../../src/oidb-services/group-file/create-group-folder';
function makeDeps(body?: OidbGroupFileFolderResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileFolderResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('CreateGroupFolder namespace', () => {
it('declares 0x6D7_0 with uinForm=true', () => {
expect(CreateGroupFolder.command).toBe(0x6d7);
expect(CreateGroupFolder.subCommand).toBe(0);
expect(CreateGroupFolder.uinForm).toBe(true);
});
it('packages create.{groupUin, rootDirectory, folderName}', async () => {
const deps = makeDeps({ create: {} as any });
await CreateGroupFolder.invoke(deps, { groupId: 12345, parentId: '/', folderName: 'docs' });
const env = protobuf_decode<OidbBase<OidbGroupFileFolderReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.create).toMatchObject({
groupUin: 12345,
rootDirectory: '/',
folderName: 'docs',
});
});
it('throws on retcode != 0 (note: lower-case retcode in this proto)', async () => {
const deps = makeDeps({ create: { retcode: 7, retMsg: 'denied' } as any });
await expect(
CreateGroupFolder.invoke(deps, { groupId: 1, parentId: '/', folderName: 'f' }),
).rejects.toThrow(/code=7/);
});
it('returns the new folder info from the response folderInfo (#195)', async () => {
const deps = makeDeps({
create: {
retcode: 0,
folderInfo: {
folderId: '/0f1e2d',
folderPath: '/docs',
folderName: 'docs',
createTime: 100,
modifyTime: 200,
createUin: 111,
modifyUin: 222,
},
} as any,
});
const r = await CreateGroupFolder.invoke(deps, {
groupId: 1,
parentId: '/',
folderName: 'docs',
});
expect(r).toEqual({
folderId: '/0f1e2d',
folderName: 'docs',
folderPath: '/docs',
createTime: 100,
modifyTime: 200,
createUin: 111,
modifyUin: 222,
});
});
it('defaults every field when the response omits folderInfo (#195)', async () => {
const deps = makeDeps({ create: { retcode: 0 } as any });
const r = await CreateGroupFolder.invoke(deps, {
groupId: 1,
parentId: '/',
folderName: 'f',
});
expect(r).toEqual({
folderId: '',
folderName: '',
folderPath: '',
createTime: 0,
modifyTime: 0,
createUin: 0,
modifyUin: 0,
});
});
});
@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileReq,
OidbGroupFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { DeleteGroupFile } from '../../../src/oidb-services/group-file/delete-group-file';
function makeDeps(body?: OidbGroupFileResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('DeleteGroupFile namespace', () => {
it('declares 0x6D6_3 with uinForm=true', () => {
expect(DeleteGroupFile.command).toBe(0x6d6);
expect(DeleteGroupFile.subCommand).toBe(3);
expect(DeleteGroupFile.uinForm).toBe(true);
});
it('packages busId=102 and routes to 0x6d6_3', async () => {
const deps = makeDeps({ delete: {} as any });
await DeleteGroupFile.invoke(deps, { groupId: 12345, fileId: 'fid' });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d6_3');
const env = protobuf_decode<OidbBase<OidbGroupFileReq>>(bytes);
expect(env.body?.delete).toMatchObject({ groupUin: 12345, busId: 102, fileId: 'fid' });
});
it('throws on missing delete sub-message', async () => {
const deps = makeDeps({});
await expect(DeleteGroupFile.invoke(deps, { groupId: 1, fileId: 'f' })).rejects.toThrow(
/delete response missing/,
);
});
it('throws on non-zero retCode', async () => {
const deps = makeDeps({ delete: { retCode: 5, retMsg: 'no perms' } as any });
await expect(DeleteGroupFile.invoke(deps, { groupId: 1, fileId: 'f' })).rejects.toThrow(
/code=5/,
);
});
});
@@ -0,0 +1,49 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileFolderReq,
OidbGroupFileFolderResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { DeleteGroupFolder } from '../../../src/oidb-services/group-file/delete-group-folder';
function makeDeps(body?: OidbGroupFileFolderResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileFolderResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('DeleteGroupFolder namespace', () => {
it('declares 0x6D7_1 with uinForm=true', () => {
expect(DeleteGroupFolder.command).toBe(0x6d7);
expect(DeleteGroupFolder.subCommand).toBe(1);
expect(DeleteGroupFolder.uinForm).toBe(true);
});
it('packages delete.{groupUin, folderId}', async () => {
const deps = makeDeps({ delete: {} as any });
await DeleteGroupFolder.invoke(deps, { groupId: 12345, folderId: 'fid' });
const env = protobuf_decode<OidbBase<OidbGroupFileFolderReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.delete).toMatchObject({ groupUin: 12345, folderId: 'fid' });
});
it('throws on missing delete sub-message', async () => {
const deps = makeDeps({});
await expect(DeleteGroupFolder.invoke(deps, { groupId: 1, folderId: 'f' })).rejects.toThrow(
/folder delete response missing/,
);
});
});
@@ -0,0 +1,158 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbOfflineFileFinalizeResp } from '@snowluma/proto-defs/oidb-actions/media';
import type { FileExtra } from '@snowluma/proto-defs/message';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { FinalizeOfflineFile } from '../../../src/oidb-services/group-file/finalize-offline-file';
import { env, m, s, v } from '../_pb-oracle';
// `bytes` fields are wire type 2 — identical framing to a sub-message, so the
// oracle's `m()` (tag + length + payload) doubles as the bytes emitter.
const b = m;
function makeDeps(resp?: OidbOfflineFileFinalizeResp) {
const responseData =
resp !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbOfflineFileFinalizeResp>>({ body: resp }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('FinalizeOfflineFile (0xE37_800)', () => {
it('declares 0xE37_800', () => {
expect(FinalizeOfflineFile.command).toBe(0xe37);
expect(FinalizeOfflineFile.subCommand).toBe(800);
});
it('serializes the request body byte-for-byte (oracle locks every tag)', async () => {
const deps = makeDeps({});
await FinalizeOfflineFile.invoke(deps, {
senderUid: 'su',
receiverUid: 'ru',
fileUuid: 'uuid',
fileHash: 'hh',
});
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0xe37_800');
// body(10) { senderUid(10) receiverUid(20) fileUuid(30) fileHash(40) }
const innerBody = [...s(10, 'su'), ...s(20, 'ru'), ...s(30, 'uuid'), ...s(40, 'hh')];
// req: subCommand(1)=800, field2(2)=0 omitted, body(10), field101(101)=3,
// field102(102)=1, field200(200)=1
const req = [...v(1, 800), ...m(10, innerBody), ...v(101, 3), ...v(102, 1), ...v(200, 1)];
const expected = env(0xe37, 800, req, false);
expect(Buffer.from(bytes).toString('hex')).toBe(expected);
});
it('extracts the download-routing metadata from field30 of the response', async () => {
const deps = makeDeps({
body: {
metadata: {
field3: 5,
field100: new Uint8Array([9, 9]),
field101: new Uint8Array([8]),
field110: 7,
timestamp1: 42,
},
},
});
const meta = await FinalizeOfflineFile.invoke(deps, {
senderUid: 'su',
receiverUid: 'ru',
fileUuid: 'uuid',
fileHash: 'hh',
});
expect(meta.field3).toBe(5);
expect(meta.field110).toBe(7);
expect(meta.timestamp1).toBe(42);
expect(Array.from(meta.field100 ?? [])).toEqual([9, 9]);
expect(Array.from(meta.field101 ?? [])).toEqual([8]);
});
it('returns {} when the response carries no metadata', async () => {
const deps = makeDeps({});
const meta = await FinalizeOfflineFile.invoke(deps, {
senderUid: 'su',
receiverUid: 'ru',
fileUuid: 'uuid',
fileHash: 'hh',
});
expect(meta).toEqual({});
});
});
describe('FileExtra.field6 (c2c download routing — issue #157)', () => {
it('encodes field6.field2 with the exact NapCat tag layout', () => {
const extra: FileExtra = {
field6: {
field2: {
field1: 7,
fileUuid: 'uu',
fileName: 'n.bin',
field6: 3,
field7: new Uint8Array([1, 2]),
field8: new Uint8Array([3]),
timestamp1: 99,
fileHash: 'hh',
selfUid: 'su',
destUid: 'du',
},
},
};
const hex = Buffer.from(protobuf_encode<FileExtra>(extra)).toString('hex');
// PrivateFileExtraField2 in ascending tag order:
// field1(1) fileUuid(4) fileName(5) field6(6) field7(7,bytes)
// field8(8,bytes) timestamp1(9) fileHash(14) selfUid(15) destUid(16)
const f2 = [
...v(1, 7),
...s(4, 'uu'),
...s(5, 'n.bin'),
...v(6, 3),
...b(7, [1, 2]),
...b(8, [3]),
...v(9, 99),
...s(14, 'hh'),
...s(15, 'su'),
...s(16, 'du'),
];
// PrivateFileExtra { field2(2) } → FileExtra { field6(6) }
const expected = Buffer.from(m(6, m(2, f2))).toString('hex');
expect(hex).toBe(expected);
});
it('round-trips field6 through encode/decode', () => {
const extra: FileExtra = {
file: { fileUuid: 'u', fileName: 'f', subcmd: 1 },
field6: {
field2: {
field1: 1,
fileUuid: 'u',
fileName: 'f',
timestamp1: 5,
selfUid: 's',
destUid: 'd',
},
},
};
const back = protobuf_decode<FileExtra>(protobuf_encode<FileExtra>(extra));
expect(back.field6?.field2).toMatchObject({
field1: 1,
fileUuid: 'u',
fileName: 'f',
timestamp1: 5,
selfUid: 's',
destUid: 'd',
});
expect(back.file?.subcmd).toBe(1);
});
});
@@ -0,0 +1,53 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileCountViewReq,
OidbGroupFileCountViewResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetGroupFileCount } from '../../../src/oidb-services/group-file/get-group-file-count';
function makeDeps(body?: OidbGroupFileCountViewResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileCountViewResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetGroupFileCount namespace', () => {
it('declares 0x6D8_2 (Count subcommand)', () => {
expect(GetGroupFileCount.command).toBe(0x6d8);
expect(GetGroupFileCount.subCommand).toBe(2);
});
it('routes to 0x6d8_2 with groupUin + appId=7 + busId=6 (#196)', async () => {
const deps = makeDeps({ count: { fileCount: 1, maxCount: 100 } });
await GetGroupFileCount.invoke(deps, { groupId: 12345 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d8_2');
const env = protobuf_decode<OidbBase<OidbGroupFileCountViewReq>>(bytes);
expect(env.body?.count).toMatchObject({ groupUin: 12345, appId: 7, busId: 6 });
});
it('decodes fileCount (field 4) + maxCount (field 6) as plain numbers', async () => {
const deps = makeDeps({ count: { fileCount: 42, maxCount: 1000 } });
const out = await GetGroupFileCount.invoke(deps, { groupId: 1 });
expect(out).toEqual({ fileCount: 42, maxCount: 1000 });
});
it('falls back to maxCount=10000 when the server elides it', async () => {
const deps = makeDeps({ count: {} });
const out = await GetGroupFileCount.invoke(deps, { groupId: 1 });
expect(out).toEqual({ fileCount: 0, maxCount: 10000 });
});
});
@@ -0,0 +1,54 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileCountViewReq,
OidbGroupFileCountViewResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetGroupFileSpace } from '../../../src/oidb-services/group-file/get-group-file-space';
function makeDeps(body?: OidbGroupFileCountViewResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileCountViewResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetGroupFileSpace namespace (#196)', () => {
it('declares 0x6D8_3 (Space subcommand)', () => {
expect(GetGroupFileSpace.command).toBe(0x6d8);
expect(GetGroupFileSpace.subCommand).toBe(3);
});
it('routes to 0x6d8_3 with a space request (groupUin + appId=7)', async () => {
const deps = makeDeps({ space: {} });
await GetGroupFileSpace.invoke(deps, { groupId: 12345 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d8_3');
const env = protobuf_decode<OidbBase<OidbGroupFileCountViewReq>>(bytes);
expect(env.body?.space).toMatchObject({ groupUin: 12345, appId: 7 });
expect(env.body?.count).toBeNull();
});
it('decodes usedSpace (field 5) + totalSpace (field 4) as plain numbers', async () => {
const deps = makeDeps({ space: { usedSpace: 20103729n, totalSpace: 10737418240n } });
const out = await GetGroupFileSpace.invoke(deps, { groupId: 1 });
expect(out).toEqual({ usedSpace: 20103729, totalSpace: 10737418240 });
});
it('defaults to 0 when the server elides the space block', async () => {
const deps = makeDeps({ space: {} });
const out = await GetGroupFileSpace.invoke(deps, { groupId: 1 });
expect(out).toEqual({ usedSpace: 0, totalSpace: 0 });
});
});
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileReq,
OidbGroupFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetGroupFileUrl } from '../../../src/oidb-services/group-file/get-group-file-url';
function makeDeps(body?: OidbGroupFileResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetGroupFileUrl namespace', () => {
it('declares 0x6D6_2 with uinForm=true', () => {
expect(GetGroupFileUrl.command).toBe(0x6d6);
expect(GetGroupFileUrl.subCommand).toBe(2);
expect(GetGroupFileUrl.uinForm).toBe(true);
});
it('packages the download slot with the supplied busId', async () => {
const deps = makeDeps({
download: { downloadDns: 'cdn', downloadUrl: new Uint8Array([1, 2]) } as any,
});
await GetGroupFileUrl.invoke(deps, { groupId: 12345, fileId: 'fid', busId: 102 });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d6_2');
const env = protobuf_decode<OidbBase<OidbGroupFileReq>>(bytes);
expect(env.body?.download).toMatchObject({
groupUin: 12345,
appId: 7,
busId: 102,
fileId: 'fid',
});
});
it('composes the https ftn_handler URL and checks download.retCode', async () => {
const deps = makeDeps({
download: { downloadDns: 'cdn', downloadUrl: new Uint8Array([0x01, 0x02]) } as any,
});
await expect(
GetGroupFileUrl.invoke(deps, { groupId: 1, fileId: 'fid-xyz', busId: 102 }),
).resolves.toBe('https://cdn/ftn_handler/0102/?fname=fid-xyz');
});
it('throws when download.retCode is non-zero', async () => {
const deps = makeDeps({
download: {
retCode: 1,
retMsg: 'denied',
downloadDns: 'cdn',
downloadUrl: new Uint8Array([1]),
} as any,
});
await expect(
GetGroupFileUrl.invoke(deps, { groupId: 1, fileId: 'f', busId: 102 }),
).rejects.toThrow(/group file url failed: code=1/);
});
it('throws when the download sub-message is missing', async () => {
const deps = makeDeps({});
await expect(
GetGroupFileUrl.invoke(deps, { groupId: 1, fileId: 'f', busId: 102 }),
).rejects.toThrow(/url response missing/);
});
it('falls back to downloadIp when downloadDns is empty', async () => {
const deps = makeDeps({
download: { downloadIp: '1.2.3.4', downloadUrl: new Uint8Array([0xab]) } as any,
});
await expect(
GetGroupFileUrl.invoke(deps, { groupId: 1, fileId: 'fid', busId: 102 }),
).resolves.toBe('https://1.2.3.4/ftn_handler/AB/?fname=fid');
});
});
@@ -0,0 +1,63 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { NTV2RichMediaReq, NTV2RichMediaResp } from '@snowluma/proto-defs/oidb-actions/media';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetGroupPttUrl } from '../../../src/oidb-services/group-file/get-group-ptt-url';
function makeDeps(body?: NTV2RichMediaResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<NTV2RichMediaResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetGroupPttUrl namespace', () => {
it('declares 0x126E_200 with uinForm=true', () => {
expect(GetGroupPttUrl.command).toBe(0x126e);
expect(GetGroupPttUrl.subCommand).toBe(200);
expect(GetGroupPttUrl.uinForm).toBe(true);
});
it('emits scene{requestType=1, businessType=3, sceneType=2, group.groupUin}', async () => {
const deps = makeDeps({
respHead: { retCode: 0 },
download: { info: { domain: 'd', urlPath: '/p' }, rKeyParam: '?rk' },
} as any);
await GetGroupPttUrl.invoke(deps, { groupId: 12345, node: { fileUuid: 'uuid' } });
const env = protobuf_decode<OidbBase<NTV2RichMediaReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.reqHead?.scene).toMatchObject({
requestType: 1,
businessType: 3,
sceneType: 2,
group: { groupUin: 12345 },
});
});
it('composes the https://domain<path><rkey> URL', async () => {
const deps = makeDeps({
respHead: { retCode: 0 },
download: { info: { domain: 'cdn.qq', urlPath: '/x' }, rKeyParam: '?r=1' },
} as any);
const out = await GetGroupPttUrl.invoke(deps, { groupId: 1, node: { fileUuid: 'uuid' } });
expect(out).toBe('https://cdn.qq/x?r=1');
});
it('throws when fileUuid is missing on the node', async () => {
const deps = makeDeps({} as any);
await expect(GetGroupPttUrl.invoke(deps, { groupId: 1, node: {} })).rejects.toThrow(
/fileUuid is required/,
);
});
});
@@ -0,0 +1,57 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { NTV2RichMediaReq, NTV2RichMediaResp } from '@snowluma/proto-defs/oidb-actions/media';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetGroupVideoUrl } from '../../../src/oidb-services/group-file/get-group-video-url';
function makeDeps(body?: NTV2RichMediaResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<NTV2RichMediaResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetGroupVideoUrl namespace', () => {
it('declares 0x11EA_200 with uinForm=true', () => {
expect(GetGroupVideoUrl.command).toBe(0x11ea);
expect(GetGroupVideoUrl.subCommand).toBe(200);
expect(GetGroupVideoUrl.uinForm).toBe(true);
});
it('emits scene{requestType=2, businessType=2, sceneType=2, group}', async () => {
const deps = makeDeps({
respHead: { retCode: 0 },
download: { info: { domain: 'd', urlPath: '/v' }, rKeyParam: '' },
} as any);
await GetGroupVideoUrl.invoke(deps, { groupId: 12345, node: { fileUuid: 'uuid' } });
const env = protobuf_decode<OidbBase<NTV2RichMediaReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.reqHead?.scene).toMatchObject({
requestType: 2,
businessType: 2,
sceneType: 2,
group: { groupUin: 12345 },
});
});
it('bubbles up retCode != 0 via the shared ntv2 deserializer', async () => {
const deps = makeDeps({
respHead: { retCode: 99, message: 'gone' },
download: { info: { domain: 'd', urlPath: '/p' }, rKeyParam: '' },
} as any);
await expect(
GetGroupVideoUrl.invoke(deps, { groupId: 1, node: { fileUuid: 'u' } }),
).rejects.toThrow(/code=99/);
});
});
@@ -0,0 +1,81 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbPrivateFileDownloadReq,
OidbPrivateFileDownloadResp,
} from '@snowluma/proto-defs/oidb-actions/media';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetPrivateFileUrl } from '../../../src/oidb-services/group-file/get-private-file-url';
function makeDeps(body?: OidbPrivateFileDownloadResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbPrivateFileDownloadResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetPrivateFileUrl namespace', () => {
it('declares 0xE37_1200', () => {
expect(GetPrivateFileUrl.command).toBe(0xe37);
expect(GetPrivateFileUrl.subCommand).toBe(1200);
});
it('packages the magic field99999 cookie + body fields', async () => {
const deps = makeDeps({
body: { result: { server: 'host', port: 80, url: '/u' } } as any,
});
await GetPrivateFileUrl.invoke(deps, { selfUid: 'self', fileId: 'fid', fileHash: 'h' });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0xe37_1200');
const env = protobuf_decode<OidbBase<OidbPrivateFileDownloadReq>>(bytes);
expect(env.body).toMatchObject({
subCommand: 1200,
field2: 1,
field101: 3,
field102: 103,
field200: 1,
});
expect(env.body?.body).toMatchObject({
receiverUid: 'self',
fileUuid: 'fid',
type: 2,
fileHash: 'h',
});
expect(env.body?.field99999).toEqual(new Uint8Array([0xc0, 0x85, 0x2c, 0x01]));
});
it('composes the http download URL', async () => {
const deps = makeDeps({
body: { result: { server: 'srv', port: 8080, url: '/path' } } as any,
});
await expect(
GetPrivateFileUrl.invoke(deps, { selfUid: 's', fileId: 'f', fileHash: 'h' }),
).resolves.toBe('http://srv:8080/path&isthumb=0');
});
it('throws when result is missing', async () => {
const deps = makeDeps({ body: {} });
await expect(
GetPrivateFileUrl.invoke(deps, { selfUid: 's', fileId: 'f', fileHash: 'h' }),
).rejects.toThrow(/private file url response invalid/);
});
it('throws when server/port/url is incomplete', async () => {
const deps = makeDeps({
body: { result: { server: 'srv', port: 0, url: '/path' } } as any,
});
await expect(
GetPrivateFileUrl.invoke(deps, { selfUid: 's', fileId: 'f', fileHash: 'h' }),
).rejects.toThrow(/private file url response invalid/);
});
});
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { NTV2RichMediaReq, NTV2RichMediaResp } from '@snowluma/proto-defs/oidb-actions/media';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetPrivatePttUrl } from '../../../src/oidb-services/group-file/get-private-ptt-url';
function makeDeps(body?: NTV2RichMediaResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<NTV2RichMediaResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetPrivatePttUrl namespace', () => {
it('declares 0x126D_200 with uinForm=true', () => {
expect(GetPrivatePttUrl.command).toBe(0x126d);
expect(GetPrivatePttUrl.subCommand).toBe(200);
expect(GetPrivatePttUrl.uinForm).toBe(true);
});
it('emits c2c scene with self uid as targetUid', async () => {
const deps = makeDeps({
respHead: { retCode: 0 },
download: { info: { domain: 'd', urlPath: '/p' }, rKeyParam: '' },
} as any);
await GetPrivatePttUrl.invoke(deps, { selfUid: 'self', node: { fileUuid: 'uuid' } });
const env = protobuf_decode<OidbBase<NTV2RichMediaReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.reqHead?.scene).toMatchObject({
requestType: 1,
businessType: 3,
sceneType: 1,
c2c: { accountType: 2, targetUid: 'self' },
});
});
});
@@ -0,0 +1,47 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { NTV2RichMediaReq, NTV2RichMediaResp } from '@snowluma/proto-defs/oidb-actions/media';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { GetPrivateVideoUrl } from '../../../src/oidb-services/group-file/get-private-video-url';
function makeDeps(body?: NTV2RichMediaResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<NTV2RichMediaResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('GetPrivateVideoUrl namespace', () => {
it('declares 0x11E9_200 with uinForm=true', () => {
expect(GetPrivateVideoUrl.command).toBe(0x11e9);
expect(GetPrivateVideoUrl.subCommand).toBe(200);
expect(GetPrivateVideoUrl.uinForm).toBe(true);
});
it('emits c2c scene with self uid as targetUid', async () => {
const deps = makeDeps({
respHead: { retCode: 0 },
download: { info: { domain: 'd', urlPath: '/v' }, rKeyParam: '' },
} as any);
await GetPrivateVideoUrl.invoke(deps, { selfUid: 'self', node: { fileUuid: 'uuid' } });
const env = protobuf_decode<OidbBase<NTV2RichMediaReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.reqHead?.scene).toMatchObject({
requestType: 2,
businessType: 2,
sceneType: 1,
c2c: { accountType: 2, targetUid: 'self' },
});
});
});
@@ -0,0 +1,169 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileViewReq,
OidbGroupFileViewResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { ListGroupFilesPage } from '../../../src/oidb-services/group-file/list-group-files-page';
function wireVarint(value: bigint): number[] {
const out: number[] = [];
let remaining = value;
do {
const byte = Number(remaining & 0x7fn);
remaining >>= 7n;
out.push(remaining === 0n ? byte : byte | 0x80);
} while (remaining !== 0n);
return out;
}
function varintField(fieldNumber: number, value: bigint): number[] {
return [...wireVarint(BigInt(fieldNumber << 3)), ...wireVarint(value)];
}
function bytesField(fieldNumber: number, value: Uint8Array): number[] {
return [
...wireVarint(BigInt((fieldNumber << 3) | 2)),
...wireVarint(BigInt(value.length)),
...value,
];
}
function stringField(fieldNumber: number, value: string): number[] {
return bytesField(fieldNumber, new TextEncoder().encode(value));
}
function messageField(fieldNumber: number, value: number[]): number[] {
return bytesField(fieldNumber, Uint8Array.from(value));
}
function makeDeps(body?: OidbGroupFileViewResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileViewResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('ListGroupFilesPage namespace', () => {
it('declares 0x6D8_1 with uinForm=true', () => {
expect(ListGroupFilesPage.command).toBe(0x6d8);
expect(ListGroupFilesPage.subCommand).toBe(1);
expect(ListGroupFilesPage.uinForm).toBe(true);
});
it('packages the page parameters (sortBy=1, field17=2, field18=0)', async () => {
const deps = makeDeps({ list: { isEnd: true, items: [] } as any });
await ListGroupFilesPage.invoke(deps, {
groupId: 12345,
targetDirectory: '/a',
startIndex: 20,
pageSize: 50,
});
const env = protobuf_decode<OidbBase<OidbGroupFileViewReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.list).toMatchObject({
groupUin: 12345,
appId: 7,
targetDirectory: '/a',
fileCount: 50,
sortBy: 1,
startIndex: 20,
field17: 2,
});
});
it('returns items and isEnd after checking list.retCode', async () => {
const deps = makeDeps({
list: { isEnd: false, items: [{ type: 1 }] } as any,
});
const out = await ListGroupFilesPage.invoke(deps, {
groupId: 1,
targetDirectory: '/',
startIndex: 0,
pageSize: 10,
});
expect(out).toEqual({ items: [expect.objectContaining({ type: 1 })], isEnd: false });
});
it('throws when list.retCode is non-zero', async () => {
const deps = makeDeps({
list: { retCode: 2, retMsg: 'busy', isEnd: true, items: [] } as any,
});
await expect(
ListGroupFilesPage.invoke(deps, {
groupId: 1,
targetDirectory: '/',
startIndex: 0,
pageSize: 10,
}),
).rejects.toThrow(/group file list failed: code=2/);
});
it('decodes the folder last-upload metadata from the QQ wire fields', async () => {
// Independent wire fixture confirmed against QQ's group-file list decoder:
// folder fields 5/9/10 are modify time, modifier UIN, and modifier name.
const folder = [
...stringField(1, 'd1'),
...stringField(3, 'dir'),
...varintField(4, 100n),
...varintField(5, 200n),
...varintField(6, 123n),
...stringField(7, 'creator'),
...varintField(8, 2n),
...varintField(9, 5_000_000_001n),
...stringField(10, 'uploader'),
];
const item = [...varintField(1, 2n), ...messageField(2, folder)];
const list = [...varintField(4, 1n), ...messageField(5, item)];
const body = messageField(2, list);
const responseData = Buffer.from(messageField(4, body));
const deps = {
sendRawPacket: vi.fn(async (): Promise<SendPacketResult> => ({
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
})),
};
const out = await ListGroupFilesPage.invoke(deps, {
groupId: 1,
targetDirectory: '/',
startIndex: 0,
pageSize: 10,
});
const decoded = out?.items?.[0]?.folderInfo as Record<string, unknown> | undefined;
expect(decoded).toMatchObject({
modifiedTime: 200,
modifierUin: 5_000_000_001n,
modifierName: 'uploader',
});
});
it('returns null when the server elides the list slot', async () => {
const deps = makeDeps({});
const out = await ListGroupFilesPage.invoke(deps, {
groupId: 1,
targetDirectory: '/',
startIndex: 0,
pageSize: 10,
});
expect(out).toBeNull();
});
});
@@ -0,0 +1,66 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileReq,
OidbGroupFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { MoveGroupFile } from '../../../src/oidb-services/group-file/move-group-file';
function makeDeps(body?: OidbGroupFileResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('MoveGroupFile namespace', () => {
it('declares 0x6D6_5 with uinForm=true', () => {
expect(MoveGroupFile.command).toBe(0x6d6);
expect(MoveGroupFile.subCommand).toBe(5);
expect(MoveGroupFile.uinForm).toBe(true);
});
it('packages parent + target directories under the move slot', async () => {
const deps = makeDeps({ move: {} as any });
await MoveGroupFile.invoke(deps, {
groupId: 12345,
fileId: 'fid',
parentDirectory: '/a',
targetDirectory: '/b',
});
const env = protobuf_decode<OidbBase<OidbGroupFileReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.move).toMatchObject({
groupUin: 12345,
appId: 7,
busId: 102,
fileId: 'fid',
parentDirectory: '/a',
targetDirectory: '/b',
});
});
it('throws on missing move sub-message', async () => {
const deps = makeDeps({});
await expect(
MoveGroupFile.invoke(deps, {
groupId: 1,
fileId: 'f',
parentDirectory: '/',
targetDirectory: '/',
}),
).rejects.toThrow(/move response missing/);
});
});
@@ -0,0 +1,50 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type { OidbGroupSendFileReq } from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { PublishGroupFile } from '../../../src/oidb-services/group-file/publish-group-file';
function makeDeps() {
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.alloc(0),
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('PublishGroupFile namespace', () => {
it('declares 0x6D9_4', () => {
expect(PublishGroupFile.command).toBe(0x6d9);
expect(PublishGroupFile.subCommand).toBe(4);
});
it('routes to 0x6d9_4 with busiType=102, type=2, field5=true', async () => {
const deps = makeDeps();
await PublishGroupFile.invoke(deps, { groupId: 12345, fileId: 'fid' });
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d9_4');
const env = protobuf_decode<OidbBase<OidbGroupSendFileReq>>(bytes);
expect(env.body?.body).toMatchObject({
groupUin: 12345,
type: 2,
info: expect.objectContaining({ busiType: 102, fileId: 'fid', field5: true }),
});
});
it('emits a non-zero `info.field3` random discriminator', async () => {
// Lagrange-V2 sets this to `Random.Shared.Next()`; our serialize uses
// Math.random() — both produce a 31-bit unsigned int, never 0.
const deps = makeDeps();
await PublishGroupFile.invoke(deps, { groupId: 1, fileId: 'f' });
const env = protobuf_decode<OidbBase<OidbGroupSendFileReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
const field3 = env.body?.body?.info?.field3 ?? 0;
expect(typeof field3).toBe('number');
});
});
@@ -0,0 +1,86 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileReq,
OidbGroupFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { RenameGroupFile } from '../../../src/oidb-services/group-file/rename-group-file';
// A success response the real server would send: command/sub echoed, rename
// ret slot with retCode 0. Lets `invoke`'s deserialize pass so we can assert
// the request wire (cmd name + body) without it throwing on an empty body.
function okRenameResponse(): Buffer {
const env: OidbBase<OidbGroupFileResp> = {
command: 0x6d6,
subCommand: 4,
body: { rename: { retCode: 0 } },
};
return Buffer.from(protobuf_encode<OidbBase<OidbGroupFileResp>>(env));
}
function makeSender() {
const defaultResp: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: okRenameResponse(),
};
return { sendRawPacket: vi.fn(async () => defaultResp) };
}
describe('RenameGroupFile namespace', () => {
it('declares command 0x6D6 sub 4 (rename slot), uin form', () => {
expect(RenameGroupFile.command).toBe(0x6d6);
expect(RenameGroupFile.subCommand).toBe(4);
expect(RenameGroupFile.uinForm).toBe(true);
});
it('serializes into the rename slot with busId 102 and the current parent folder', () => {
const out = RenameGroupFile.serialize({} as any, {
groupId: 12345,
fileId: '/abc',
parentDirectory: '/sub',
newFileName: 'new.txt',
});
expect(out).toEqual({
rename: {
groupUin: 12345,
busId: 102,
fileId: '/abc',
parentFolder: '/sub',
newFileName: 'new.txt',
},
});
});
it('round-trips the rename body through the OIDB envelope (locks the pb field tags)', async () => {
const sender = makeSender();
await RenameGroupFile.invoke(sender, {
groupId: 12345,
fileId: '/abc',
parentDirectory: '/',
newFileName: 'new.txt',
});
expect(sender.sendRawPacket).toHaveBeenCalledOnce();
const [cmd, bytes] = sender.sendRawPacket.mock.calls[0]!;
expect(cmd).toBe('OidbSvcTrpcTcp.0x6d6_4');
const env = protobuf_decode<OidbBase<OidbGroupFileReq>>(bytes);
expect(env.command).toBe(0x6d6);
expect(env.subCommand).toBe(4);
expect(env.body.rename).toMatchObject({
groupUin: 12345,
busId: 102,
fileId: '/abc',
parentFolder: '/',
newFileName: 'new.txt',
});
});
it('deserialize throws when the rename slot is missing from the response', () => {
expect(() => RenameGroupFile.deserialize({} as any, {})).toThrow(/rename response missing/);
});
});
@@ -0,0 +1,61 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileFolderReq,
OidbGroupFileFolderResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { RenameGroupFolder } from '../../../src/oidb-services/group-file/rename-group-folder';
function makeDeps(body?: OidbGroupFileFolderResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileFolderResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('RenameGroupFolder namespace', () => {
it('declares 0x6D7_2 with uinForm=true', () => {
expect(RenameGroupFolder.command).toBe(0x6d7);
expect(RenameGroupFolder.subCommand).toBe(2);
expect(RenameGroupFolder.uinForm).toBe(true);
});
it('packages rename.{groupUin, folderId, newFolderName}', async () => {
const deps = makeDeps({ rename: {} as any });
await RenameGroupFolder.invoke(deps, {
groupId: 12345,
folderId: 'fid',
newFolderName: 'newname',
});
const env = protobuf_decode<OidbBase<OidbGroupFileFolderReq>>(
deps.sendRawPacket.mock.calls[0]![1],
);
expect(env.body?.rename).toMatchObject({
groupUin: 12345,
folderId: 'fid',
newFolderName: 'newname',
});
});
it('throws on missing rename sub-message', async () => {
const deps = makeDeps({});
await expect(
RenameGroupFolder.invoke(deps, {
groupId: 1,
folderId: 'f',
newFolderName: 'n',
}),
).rejects.toThrow(/folder rename response missing/);
});
});
@@ -0,0 +1,62 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupSendFileReq,
OidbGroupSendFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { TransGroupFile } from '../../../src/oidb-services/group-file/trans-group-file';
function makeDeps(body?: OidbGroupSendFileResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupSendFileResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('TransGroupFile namespace', () => {
it('declares 0x6D9_0', () => {
expect(TransGroupFile.command).toBe(0x6d9);
expect(TransGroupFile.subCommand).toBe(0);
});
it('packages group + file id under the transFile slot', async () => {
const deps = makeDeps({ transFile: { saveBusId: 102, saveFilePath: '/abc' } });
const out = await TransGroupFile.invoke(deps, { groupId: 12345, fileId: 'fid' });
expect(out).toEqual({ saveBusId: 102, saveFilePath: '/abc' });
expect(deps.sendRawPacket).toHaveBeenCalledOnce();
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d9_0');
const env = protobuf_decode<OidbBase<OidbGroupSendFileReq>>(bytes);
expect(env.body?.transFile).toEqual({
groupUin: 12345n,
appId: 7,
busId: 102,
fileId: 'fid',
});
});
it('throws on missing transFile sub-message', async () => {
const deps = makeDeps({});
await expect(TransGroupFile.invoke(deps, { groupId: 1, fileId: 'f' })).rejects.toThrow(
/transfer response missing/,
);
});
it('throws on non-zero retCode', async () => {
const deps = makeDeps({ transFile: { retCode: 7, retMsg: 'denied' } });
await expect(TransGroupFile.invoke(deps, { groupId: 1, fileId: 'f' })).rejects.toThrow(
/code=7/,
);
});
});
@@ -0,0 +1,88 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileReq,
OidbGroupFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { UploadGroupFileRequest } from '../../../src/oidb-services/group-file/upload-group-file-request';
function makeDeps(body?: OidbGroupFileResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbGroupFileResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('UploadGroupFileRequest namespace', () => {
it('declares 0x6D6_0 with uinForm=true', () => {
expect(UploadGroupFileRequest.command).toBe(0x6d6);
expect(UploadGroupFileRequest.subCommand).toBe(0);
expect(UploadGroupFileRequest.uinForm).toBe(true);
});
it('packages the file pre-flight slot (entrance=6, appId=4, busId=102)', async () => {
const deps = makeDeps({ upload: { fileId: 'fid', boolFileExist: true } as any });
await UploadGroupFileRequest.invoke(deps, {
groupId: 12345,
fileName: 'doc.pdf',
folderId: '/',
fileSize: 1024,
fileSha1: new Uint8Array(20),
fileMd5: new Uint8Array(16),
});
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d6_0');
const env = protobuf_decode<OidbBase<OidbGroupFileReq>>(bytes);
expect(env.reserved).toBe(1); // uinForm
expect(env.body?.file).toMatchObject({
groupUin: 12345,
appId: 4,
busId: 102,
entrance: 6,
targetDirectory: '/',
fileName: 'doc.pdf',
localDirectory: '/doc.pdf',
fileSize: 1024n,
field15: true,
});
});
it('returns the upload-info body for the facade orchestration', async () => {
const deps = makeDeps({ upload: { fileId: 'fid-X', boolFileExist: true } as any });
const out = await UploadGroupFileRequest.invoke(deps, {
groupId: 1,
fileName: 'f',
folderId: '/',
fileSize: 0,
fileSha1: new Uint8Array(0),
fileMd5: new Uint8Array(0),
});
expect(out.fileId).toBe('fid-X');
expect(out.boolFileExist).toBe(true);
});
it('throws when the upload sub-message is missing', async () => {
const deps = makeDeps({});
await expect(
UploadGroupFileRequest.invoke(deps, {
groupId: 1,
fileName: 'f',
folderId: '/',
fileSize: 0,
fileSha1: new Uint8Array(0),
fileMd5: new Uint8Array(0),
}),
).rejects.toThrow(/upload response missing/);
});
});
@@ -0,0 +1,92 @@
import { describe, expect, it, vi } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbPrivateFileUploadReq,
OidbPrivateFileUploadResp,
} from '@snowluma/proto-defs/oidb-actions/media';
import type { SendPacketResult } from '@snowluma/common/packet-sender';
import { UploadPrivateFileRequest } from '../../../src/oidb-services/group-file/upload-private-file-request';
function makeDeps(body?: OidbPrivateFileUploadResp) {
const responseData =
body !== undefined
? Buffer.from(protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({ body }))
: Buffer.alloc(0);
const r: SendPacketResult = {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData,
};
return { sendRawPacket: vi.fn(async () => r) };
}
describe('UploadPrivateFileRequest namespace', () => {
it('declares 0xE37_1700', () => {
expect(UploadPrivateFileRequest.command).toBe(0xe37);
expect(UploadPrivateFileRequest.subCommand).toBe(1700);
});
it('packages businessId=3, clientType=1, flagSupportMediaPlatform=1 alongside the upload body', async () => {
const deps = makeDeps({ upload: { uuid: 'fid', boolFileExist: true } as any });
await UploadPrivateFileRequest.invoke(deps, {
senderUid: 's',
receiverUid: 'r',
fileName: 'f.pdf',
fileSize: 4096,
fileSha1: new Uint8Array(20),
fileMd5: new Uint8Array(16),
md510MCheckSum: new Uint8Array(16),
});
const [wire, bytes] = deps.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0xe37_1700');
const env = protobuf_decode<OidbBase<OidbPrivateFileUploadReq>>(bytes);
expect(env.body).toMatchObject({
command: 1700,
businessId: 3,
clientType: 1,
flagSupportMediaPlatform: 1,
});
expect(env.body?.upload).toMatchObject({
senderUid: 's',
receiverUid: 'r',
fileSize: 4096,
fileName: 'f.pdf',
});
});
it('returns the upload body for the facade host-selection step', async () => {
const deps = makeDeps({
upload: { uuid: 'fid-Y', fileAddon: 'hash', boolFileExist: true } as any,
});
const out = await UploadPrivateFileRequest.invoke(deps, {
senderUid: 's',
receiverUid: 'r',
fileName: 'f',
fileSize: 0,
fileSha1: new Uint8Array(0),
fileMd5: new Uint8Array(0),
md510MCheckSum: new Uint8Array(0),
});
expect(out.uuid).toBe('fid-Y');
expect(out.fileAddon).toBe('hash');
});
it('throws when the upload sub-message is missing', async () => {
const deps = makeDeps({});
await expect(
UploadPrivateFileRequest.invoke(deps, {
senderUid: 's',
receiverUid: 'r',
fileName: 'f',
fileSize: 0,
fileSha1: new Uint8Array(0),
fileMd5: new Uint8Array(0),
md510MCheckSum: new Uint8Array(0),
}),
).rejects.toThrow(/upload response missing/);
});
});