Files
SnowLuma/packages/core/tests/actions/group-file.test.ts
2026-10-12 15:43:34 +08:00

774 lines
31 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, beforeAll, afterAll } from 'vitest';
import { protobuf_decode, protobuf_encode } from '@snowluma/proton';
import type { OidbBase } from '@snowluma/proto-defs/oidb';
import type {
OidbGroupFileCountViewResp,
OidbGroupFileReq,
OidbGroupFileResp,
OidbGroupFileViewResp,
OidbGroupFileFolderResp,
OidbGroupSendFileReq,
OidbGroupSendFileResp,
} from '@snowluma/proto-defs/oidb-actions/group-file';
import type {
OidbPrivateFileUploadResp,
NTV2RichMediaResp,
} from '@snowluma/proto-defs/oidb-actions/media';
// Post-namespace migration: GroupFileApi forwards single-OIDB methods
// through namespaces under @snowluma/protocol/oidb-services/group-file.
// The multi-stage methods (`upload` / `uploadPrivate`) keep their
// orchestration on the facade; we assert against bridge.sendRawPacket
// directly. Highway calls (fetchHighwaySession / uploadHighwayHttp)
// and the file-source loader are still module-mocked because the
// facade owns those calls directly.
vi.mock('@snowluma/protocol/highway', () => ({
fetchHighwaySession: vi.fn(async () => ({})),
uploadHighwayHttp: vi.fn(async () => undefined),
FileChunkSource: {
open: vi.fn(async () => ({ size: 3, read: vi.fn(), close: vi.fn(async () => undefined) })),
},
}));
vi.mock('@snowluma/protocol/highway/utils', () => ({
FILE_UPLOAD_MAX_BYTES: 4 * 1024 * 1024 * 1024,
}));
// The upload path now stages the source to disk + stream-hashes it; mock those
// two seams. stageSourceToDisk's filePath must be a REAL file so the mutation
// guard's fsp.stat succeeds (configured per-test in beforeEach → STAGED_FILE).
vi.mock('@snowluma/protocol/highway/stage', () => ({
stageSourceToDisk: vi.fn(),
}));
vi.mock('@snowluma/protocol/highway/hash-file', () => ({
hashFileStreaming: vi.fn(async () => ({
md5: new Uint8Array(16),
sha1: new Uint8Array(20),
md5Hex: '00'.repeat(16),
sha1Hex: '00'.repeat(20),
sha1Blocks: [],
headMd5: new Uint8Array(16),
})),
}));
import fs from 'fs';
import os from 'os';
import path from 'path';
import * as highwayClient from '@snowluma/protocol/highway';
import * as stageMod from '@snowluma/protocol/highway/stage';
import { GroupFileApi } from '../../src/bridge/apis/group-file';
import { mockBridge } from './_helpers';
function packResponse(body: Uint8Array) {
return {
success: true,
gotResponse: true,
errorCode: 0,
errorMessage: '',
responseData: Buffer.from(body),
};
}
let STAGED_FILE: string;
describe('apis/group-file', () => {
beforeAll(() => {
STAGED_FILE = path.join(os.tmpdir(), `sl-gf-staged-${process.pid}.bin`);
fs.writeFileSync(STAGED_FILE, Buffer.from([1, 2, 3])); // real 3-byte file for the guard stat
});
afterAll(() => {
try {
fs.unlinkSync(STAGED_FILE);
} catch {
/* ignore */
}
});
beforeEach(() => {
vi.mocked(highwayClient.fetchHighwaySession).mockClear();
vi.mocked(highwayClient.uploadHighwayHttp).mockClear();
vi.mocked(highwayClient.FileChunkSource.open).mockClear();
vi.mocked(stageMod.stageSourceToDisk).mockResolvedValue({
filePath: STAGED_FILE,
fileSize: 3,
fileName: 'file.bin',
cleanup: vi.fn(async () => undefined),
} as any);
});
it('getCount returns { fileCount, maxCount } from the OIDB response', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileCountViewResp>>({
body: { count: { fileCount: 42, maxCount: 1000 } },
}),
),
);
const out = await new GroupFileApi(bridge as any).getCount(12345);
expect(out).toEqual({ fileCount: 42, maxCount: 1000 });
});
it('getCount falls back to defaults on partial response', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileCountViewResp>>({ body: { count: {} } }),
),
);
const out = await new GroupFileApi(bridge as any).getCount(12345);
expect(out).toEqual({ fileCount: 0, maxCount: 10000 });
});
it('upload skips highway when boolFileExist is true', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: {
upload: {
fileId: 'fid-xyz',
boolFileExist: true,
},
} as any,
}),
),
);
const api = new GroupFileApi(bridge as any);
vi.spyOn(api, 'publish').mockResolvedValue();
const out = await api.upload(12345, '/path/file.bin');
expect(out).toEqual({ fileId: 'fid-xyz' });
expect(highwayClient.fetchHighwaySession).not.toHaveBeenCalled();
expect(highwayClient.uploadHighwayHttp).not.toHaveBeenCalled();
});
it('upload runs highway PUT when boolFileExist is false', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: {
upload: {
fileId: 'fid-xyz',
// boolFileExist is the proto3 default (false) — omit.
uploadIp: '1.2.3.4',
uploadPort: 8080,
fileKey: new Uint8Array([9]),
checkKey: new Uint8Array([8]),
},
} as any,
}),
),
);
const api = new GroupFileApi(bridge as any);
vi.spyOn(api, 'publish').mockResolvedValue();
await api.upload(12345, '/path/file.bin');
expect(highwayClient.fetchHighwaySession).toHaveBeenCalledOnce();
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
});
it('[stream] PUTs from the staged disk file via FileChunkSource and releases the temp', async () => {
const cleanup = vi.fn(async () => undefined);
vi.mocked(stageMod.stageSourceToDisk).mockResolvedValueOnce({
filePath: STAGED_FILE,
fileSize: 3,
fileName: 'file.bin',
cleanup,
} as any);
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: {
upload: {
fileId: 'fid-xyz',
uploadIp: '1.2.3.4',
uploadPort: 8080,
fileKey: new Uint8Array([9]),
checkKey: new Uint8Array([8]),
},
} as any,
}),
),
);
const api = new GroupFileApi(bridge as any);
vi.spyOn(api, 'publish').mockResolvedValue();
await api.upload(12345, '/path/file.bin');
// Main file streamed from the staged path (not buffered), and the temp is
// released afterward.
expect(highwayClient.FileChunkSource.open).toHaveBeenCalledWith(STAGED_FILE, 3);
expect(vi.mocked(highwayClient.uploadHighwayHttp).mock.calls[0]![3]).toBeDefined();
expect(cleanup).toHaveBeenCalled();
});
it('upload throws on missing upload response', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(protobuf_encode<OidbBase<OidbGroupFileResp>>({ body: {} })),
);
await expect(
new GroupFileApi(bridge as any).upload(12345, '/path/file.bin'),
).rejects.toThrow(/response missing/);
});
it('upload bubbles up OIDB retCode errors', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: { upload: { retCode: 999, retMsg: 'quota exceeded' } as any },
}),
),
);
await expect(
new GroupFileApi(bridge as any).upload(12345, '/path/file.bin'),
).rejects.toThrow(/code=999/);
});
it('upload publishes the file via OIDB 0x6d9_4 after upload (the "empty message" regression)', async () => {
// Reproduces the bug report: OIDB upload + highway PUT alone only
// stage the bytes on QQ's side; without the trailing 0x6d9_4 OIDB
// call the file never appears in the chat.
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: { upload: { fileId: 'fid-pub', boolFileExist: true } as any },
}),
),
);
const api = new GroupFileApi(bridge as any);
const publishSpy = vi.spyOn(api, 'publish').mockResolvedValue();
await api.upload(12345, '/path/some-file.bin', 'mynote.txt');
expect(publishSpy).toHaveBeenCalledOnce();
const [groupId, fileId] = publishSpy.mock.calls[0]!;
expect(groupId).toBe(12345);
expect(fileId).toBe('fid-pub');
expect(bridge.apis.message.sendGroup).not.toHaveBeenCalled();
});
it('upload skips the chat post when uploadFile=false', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: { upload: { fileId: 'fid-skip', boolFileExist: true } as any },
}),
),
);
const api = new GroupFileApi(bridge as any);
const publishSpy = vi.spyOn(api, 'publish').mockResolvedValue();
await api.upload(12345, '/path/file.bin', '', '/', false);
expect(publishSpy).not.toHaveBeenCalled();
});
it('upload can PUT bytes without publishing a separate group file message', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: {
upload: {
fileId: 'fid-forward',
uploadIp: '127.0.0.1',
uploadPort: 443,
fileKey: new Uint8Array([1]),
checkKey: new Uint8Array([2]),
} as any,
},
}),
),
);
const api = new GroupFileApi(bridge as any);
const publishSpy = vi.spyOn(api, 'publish').mockResolvedValue();
await api.upload(12345, 'base64://AQID', 'forward.txt', '/', true, false);
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
expect(publishSpy).not.toHaveBeenCalled();
});
it('upload returns success even when the chat post fails (file is still uploaded)', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: { upload: { fileId: 'fid-tolerant', boolFileExist: true } as any },
}),
),
);
const api = new GroupFileApi(bridge as any);
const publishSpy = vi
.spyOn(api, 'publish')
.mockRejectedValueOnce(new Error('message rejected'));
const out = await api.upload(12345, '/path/file.bin');
expect(out).toEqual({ fileId: 'fid-tolerant' });
expect(publishSpy).toHaveBeenCalledOnce();
});
it('upload caches the upload metadata for later resend by file_id', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: { upload: { fileId: 'fid-cached', boolFileExist: true } as any },
}),
),
);
const api = new GroupFileApi(bridge as any);
vi.spyOn(api, 'publish').mockResolvedValue();
await api.upload(12345, '/path/x.bin', 'x.bin');
expect(bridge.rememberUploadedFile).toHaveBeenCalledOnce();
const [meta] = bridge.rememberUploadedFile.mock.calls[0]!;
expect(meta).toMatchObject({
fileId: 'fid-cached',
scope: 'group',
groupId: 12345,
fileName: 'x.bin',
});
});
it('publish hits OIDB 0x6d9_4 with the right body', async () => {
const bridge = mockBridge();
await new GroupFileApi(bridge as any).publish(12345, 'fid-publish');
expect(bridge.sendRawPacket).toHaveBeenCalledOnce();
const [wire, bytes] = bridge.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d9_4');
const env = protobuf_decode<OidbBase<OidbGroupSendFileReq>>(bytes);
expect(env.command).toBe(0x6d9);
expect(env.subCommand).toBe(4);
expect(env.body?.body).toMatchObject({
groupUin: 12345,
type: 2,
info: expect.objectContaining({
busiType: 102,
fileId: 'fid-publish',
field5: true,
}),
});
});
it('trans hits OIDB 0x6d9_0 and returns the saved path metadata', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupSendFileResp>>({
body: { transFile: { saveBusId: 102, saveFilePath: '/saved/path' } },
}),
),
);
const out = await new GroupFileApi(bridge as any).trans(12345, 'fid-trans');
expect(out).toEqual({ saveBusId: 102, saveFilePath: '/saved/path' });
const [wire, bytes] = bridge.sendRawPacket.mock.calls[0]!;
expect(wire).toBe('OidbSvcTrpcTcp.0x6d9_0');
const env = protobuf_decode<OidbBase<OidbGroupSendFileReq>>(bytes);
expect(env.body?.transFile).toMatchObject({
groupUin: 12345n,
appId: 7,
busId: 102,
fileId: 'fid-trans',
});
});
it('uploadPrivate resolves both target + self UID before OIDB call', async () => {
const bridge = mockBridge({
identity: {
uin: '10001',
selfUid: '',
nickname: 'self-nick',
findUidByUin: vi.fn(() => 'cached-uid'),
findUinByUid: vi.fn(() => 0),
resolveUin: vi.fn(async () => null),
findGroupMember: vi.fn(() => null),
updateFriendRemark: vi.fn(() => true),
updateGroupRemark: vi.fn(() => true),
forgetGroup: vi.fn(),
},
});
vi.mocked(bridge.resolveUserUid)
.mockResolvedValueOnce('target-uid')
.mockResolvedValueOnce('self-uid-resolved');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: { uuid: 'fid', fileAddon: 'hash', boolFileExist: true } as any,
},
}),
),
);
const out = await new GroupFileApi(bridge as any).uploadPrivate(67890, '/path/file');
expect(out).toEqual({ fileId: 'fid', fileHash: 'hash' });
expect(bridge.resolveUserUid).toHaveBeenCalledTimes(2);
});
it('uploadPrivate publishes the file via apis.message.sendC2cFile after upload', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: { uuid: 'pfid', fileAddon: 'phash', boolFileExist: true } as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(
67890,
'/path/private-file.bin',
'doc.pdf',
);
expect(bridge.apis.message.sendC2cFile).toHaveBeenCalledOnce();
const [userUin, userUid, info] = bridge.apis.message.sendC2cFile.mock.calls[0]!;
expect(userUin).toBe(67890);
expect(userUid).toBe('target-uid');
expect(info).toMatchObject({ fileId: 'pfid', fileName: 'doc.pdf', fileHash: 'phash' });
expect(bridge.apis.message.sendPrivate).not.toHaveBeenCalled();
});
it('uploadPrivate caches the upload metadata for later resend by file_id', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: {
uuid: 'pfid-cache',
fileAddon: 'addon-hash',
boolFileExist: true,
} as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(
67890,
'/path/cache-me.txt',
'cache-me.txt',
);
expect(bridge.rememberUploadedFile).toHaveBeenCalledOnce();
const [meta] = bridge.rememberUploadedFile.mock.calls[0]!;
expect(meta).toMatchObject({
fileId: 'pfid-cache',
scope: 'private',
userId: 67890,
fileName: 'cache-me.txt',
fileHash: 'addon-hash',
});
});
it('uploadPrivate skips the chat post when uploadFile=false', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: { uuid: 'pfid', fileAddon: 'phash', boolFileExist: true } as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(67890, '/path/file', '', false);
expect(bridge.apis.message.sendC2cFile).not.toHaveBeenCalled();
});
it('uploadPrivate can PUT bytes without publishing a separate c2c file message', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: {
uuid: 'pfid-forward',
fileAddon: 'phash-forward',
rtpMediaPlatformUploadAddress: [
{ outIP: 0, outPort: 0, inIP: 16885952, inPort: 8080, iPType: 1 },
],
mediaPlatformUploadKey: new Uint8Array([1, 2, 3]),
} as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(
67890,
'base64://AQID',
'forward.txt',
true,
false,
);
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
expect(bridge.apis.message.sendC2cFile).not.toHaveBeenCalled();
});
it('[stream] uploadPrivate PUTs from the staged disk file via FileChunkSource and releases the temp', async () => {
const cleanup = vi.fn(async () => undefined);
vi.mocked(stageMod.stageSourceToDisk).mockResolvedValueOnce({
filePath: STAGED_FILE,
fileSize: 3,
fileName: 'file.bin',
cleanup,
} as any);
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: {
uuid: 'pfid-stream',
fileAddon: 'phash',
rtpMediaPlatformUploadAddress: [
{ outIP: 0, outPort: 0, inIP: 16885952, inPort: 8080, iPType: 1 },
],
mediaPlatformUploadKey: new Uint8Array([1, 2, 3]),
} as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(
67890,
'/path/file',
'doc.pdf',
true,
false,
);
expect(highwayClient.FileChunkSource.open).toHaveBeenCalledWith(STAGED_FILE, 3);
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
expect(cleanup).toHaveBeenCalled();
});
it('uploadPrivate reads host from rtpMediaPlatformUploadAddress[0].inIP when populated', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: {
uuid: 'pfid',
fileAddon: 'phash',
rtpMediaPlatformUploadAddress: [
{ outIP: 0, outPort: 0, inIP: 16885952, inPort: 8080, iPType: 1 },
],
mediaPlatformUploadKey: new Uint8Array([1, 2, 3]),
} as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(67890, '/path/file.bin');
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
});
it('uploadPrivate falls back to uploadDomain when uploadIp is empty', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: {
upload: {
uuid: 'pfid',
fileAddon: 'phash',
uploadDomain: 'upload.qpic.cn',
uploadPort: 8080,
mediaPlatformUploadKey: new Uint8Array([1, 2, 3]),
} as any,
},
}),
),
);
await new GroupFileApi(bridge as any).uploadPrivate(67890, '/path/file.bin');
expect(highwayClient.uploadHighwayHttp).toHaveBeenCalledOnce();
});
it('uploadPrivate throws "host is invalid" only when every host field is empty', async () => {
const bridge = mockBridge();
vi.mocked(bridge.resolveUserUid).mockResolvedValueOnce('target-uid');
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbPrivateFileUploadResp>>({
body: { upload: { uuid: 'pfid', fileAddon: 'phash', uploadPort: 8080 } as any },
}),
),
);
await expect(
new GroupFileApi(bridge as any).uploadPrivate(67890, '/path/file.bin'),
).rejects.toThrow(/upload host is invalid/);
});
it('list paginates files + folders out of OIDB items', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileViewResp>>({
body: {
list: {
isEnd: true,
items: [
{
type: 1,
fileInfo: {
fileId: 'f1',
fileName: 'a.txt',
uploaderUin: 1,
uploaderName: 'alice',
},
},
{
type: 2,
folderInfo: {
folderId: 'd1',
folderName: 'dir',
creatorUin: 2,
creatorName: 'bob',
modifiedTime: 200,
modifierUin: 5_000_000_001n,
modifierName: 'alice',
},
},
],
} as any,
},
}),
),
);
const out = await new GroupFileApi(bridge as any).list(12345);
expect(out.files).toHaveLength(1);
expect(out.files[0]).toMatchObject({
fileId: 'f1',
fileName: 'a.txt',
uploader: 1,
uploaderName: 'alice',
});
expect(out.folders).toHaveLength(1);
expect(out.folders[0]).toMatchObject({
folderId: 'd1',
folderName: 'dir',
creator: 2,
creatorName: 'bob',
lastUploadTime: 200,
lastUploader: 5_000_000_001,
lastUploaderName: 'alice',
});
});
it('getUrl builds the https URL from downloadDns + hex-encoded path', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: {
download: {
downloadDns: 'cdn.example.com',
downloadUrl: new Uint8Array([0x01, 0x02]),
} as any,
},
}),
),
);
const url = await new GroupFileApi(bridge as any).getUrl(12345, 'fid-xyz');
expect(url).toBe('https://cdn.example.com/ftn_handler/0102/?fname=fid-xyz');
});
it('getUrl throws when response is missing dns or url', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({
body: { download: {} as any },
}),
),
);
await expect(new GroupFileApi(bridge as any).getUrl(12345, 'fid-xyz')).rejects.toThrow(
/invalid/,
);
});
it('delete / move dispatch the right sub-commands', async () => {
const bridge = mockBridge();
bridge.sendRawPacket
.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({ body: { delete: {} as any } }),
),
)
.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileResp>>({ body: { move: {} as any } }),
),
);
const api = new GroupFileApi(bridge as any);
await api.delete(12345, 'fid');
await api.move(12345, 'fid', '/a', '/b');
const env1 = protobuf_decode<OidbBase<OidbGroupFileReq>>(
bridge.sendRawPacket.mock.calls[0]![1],
);
const env2 = protobuf_decode<OidbBase<OidbGroupFileReq>>(
bridge.sendRawPacket.mock.calls[1]![1],
);
expect(env1.subCommand).toBe(3);
expect(env2.subCommand).toBe(5);
});
it('createFolder / deleteFolder / renameFolder dispatch 0x6d7 family', async () => {
const bridge = mockBridge();
bridge.sendRawPacket
.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileFolderResp>>({
body: { create: {} as any },
}),
),
)
.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileFolderResp>>({
body: { delete: {} as any },
}),
),
)
.mockResolvedValueOnce(
packResponse(
protobuf_encode<OidbBase<OidbGroupFileFolderResp>>({
body: { rename: {} as any },
}),
),
);
const api = new GroupFileApi(bridge as any);
await api.createFolder(1, 'folder');
await api.deleteFolder(1, 'fid');
await api.renameFolder(1, 'fid', 'newname');
const cmds = bridge.sendRawPacket.mock.calls.map((c) => c[0]);
expect(cmds).toEqual([
'OidbSvcTrpcTcp.0x6d7_0',
'OidbSvcTrpcTcp.0x6d7_1',
'OidbSvcTrpcTcp.0x6d7_2',
]);
});
it('get*Url builds https://domain/path?rkey from the NTV2 response', async () => {
const bridge = mockBridge();
bridge.sendRawPacket.mockResolvedValue(
packResponse(
protobuf_encode<OidbBase<NTV2RichMediaResp>>({
body: {
respHead: {},
download: {
info: { domain: 'media.example.com', urlPath: '/path/x' },
rKeyParam: '?rkey=abc',
} as any,
} as any,
}),
),
);
const url = await new GroupFileApi(bridge as any).getVideoUrl(12345, { fileUuid: 'uuid' });
expect(url).toBe('https://media.example.com/path/x?rkey=abc');
});
});