Files
BetterDesk/web-nodejs/tests/rdclient.protocol.test.js
T
UNITRONIX 088cada11e feat(remote): enhance file transfer functionality and UI improvements
- Added a drag-and-drop upload feature in the Web Remote interface, allowing users to easily upload files to the currently open folder on the remote side.
- Implemented file transfer features including zstd compression, overwrite confirmation dialogs, and a context menu for file management.
- Updated various language files to reflect new UI elements and messages related to file transfer operations.
- Enhanced CSS for improved layout and user experience in the file transfer interface.
2026-07-06 20:16:06 +02:00

287 lines
10 KiB
JavaScript

/**
* Regression: Auth2FA must serialize under auth_2fa (not auth2Fa).
* Wrong field names produce empty Message payloads — peer never receives the code.
*
* FILE_TRANSFER: dedicated relay session uses ConnType.FILE_TRANSFER and
* LoginRequest.file_transfer union — required for RustDesk file manager (#217).
*/
const protobuf = require('protobufjs');
const path = require('path');
describe('RDClient Auth2FA protobuf encoding', () => {
let Message;
beforeAll(async () => {
const root = await protobuf.load([
path.join(__dirname, '../protos/message.proto')
]);
Message = root.lookupType('hbb.Message');
});
it('encodes auth_2fa with a non-empty payload', () => {
const msg = Message.fromObject({ auth_2fa: { code: '741626' } });
const buf = Message.encode(msg).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = Message.decode(buf).toJSON();
expect(decoded.auth_2fa).toEqual({ code: '741626' });
});
it('does not encode auth2Fa camelCase alias (protobuf.js quirk)', () => {
const msg = Message.fromObject({ auth2Fa: { code: '741626' } });
const buf = Message.encode(msg).finish();
expect(buf.length).toBe(0);
});
});
describe('RDClient FILE_TRANSFER protobuf encoding', () => {
let RendezvousMessage;
let Message;
let ConnType;
let LoginRequest;
beforeAll(async () => {
const rendezvousRoot = await protobuf.load([
path.join(__dirname, '../protos/rendezvous.proto')
]);
RendezvousMessage = rendezvousRoot.lookupType('hbb.RendezvousMessage');
ConnType = rendezvousRoot.lookupEnum('hbb.ConnType');
const messageRoot = await protobuf.load([
path.join(__dirname, '../protos/message.proto')
]);
Message = messageRoot.lookupType('hbb.Message');
LoginRequest = messageRoot.lookupType('hbb.LoginRequest');
});
it('encodes PunchHoleRequest with FILE_TRANSFER conn_type', () => {
const msg = RendezvousMessage.create({
punchHoleRequest: {
id: '1234567',
natType: 0,
licenceKey: 'test-key',
connType: ConnType.values.FILE_TRANSFER,
forceRelay: true
}
});
const buf = RendezvousMessage.encode(msg).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = RendezvousMessage.decode(buf).toJSON();
expect(decoded.punchHoleRequest.connType).toBe('FILE_TRANSFER');
});
it('encodes LoginRequest with fileTransfer union (not empty payload)', () => {
const login = LoginRequest.create({
username: '',
password: Buffer.from([1, 2, 3, 4]),
myId: 'web-client-ft',
myName: 'BetterDesk Web',
fileTransfer: { dir: '', showHidden: false }
});
const buf = LoginRequest.encode(login).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = LoginRequest.decode(buf).toJSON();
expect(decoded.fileTransfer).toEqual({ dir: '', showHidden: false });
});
it('regression: empty LoginRequest without fileTransfer union', () => {
const login = LoginRequest.create({
password: Buffer.from([1, 2, 3, 4])
});
const decoded = LoginRequest.decode(LoginRequest.encode(login).finish()).toJSON();
expect(decoded.fileTransfer).toBeUndefined();
});
});
describe('RDClient file transfer message encoding', () => {
let Message;
let FileEntry;
let FileAction;
beforeAll(async () => {
const root = await protobuf.load([
path.join(__dirname, '../protos/message.proto')
]);
Message = root.lookupType('hbb.Message');
FileEntry = root.lookupType('hbb.FileEntry');
FileAction = root.lookupType('hbb.FileAction');
});
function buildRemoteUploadPath(remoteDir, fileName) {
const dir = remoteDir || '';
const sep = dir.includes('\\') ? '\\' : '/';
if (!dir) return fileName || '';
return dir + (dir.endsWith(sep) ? '' : sep) + (fileName || '');
}
it('buildRemoteUploadPath joins directory and file name', () => {
expect(buildRemoteUploadPath('C:\\Users\\admin', 'test.txt')).toBe('C:\\Users\\admin\\test.txt');
expect(buildRemoteUploadPath('/home/user/', 'a.bin')).toBe('/home/user/a.bin');
expect(buildRemoteUploadPath('', 'only.txt')).toBe('only.txt');
});
it('encodes FileTransferReceiveRequest for upload to remote (dir + files)', () => {
const entry = FileEntry.create({
entryType: 4,
name: 'readme.txt',
size: 42,
modifiedTime: 1700000000
});
const action = FileAction.create({
receive: {
id: 7,
path: 'C:\\Users\\admin',
files: [entry],
fileNum: 0,
totalSize: 42
}
});
const buf = FileAction.encode(action).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = FileAction.decode(buf).toJSON();
expect(decoded.receive.path).toBe('C:\\Users\\admin');
expect(decoded.receive.files).toHaveLength(1);
expect(decoded.receive.files[0].name).toBe('readme.txt');
expect(decoded.receive.files[0].entryType).toBe('File');
});
it('encodes FileTransferSendRequest for download from remote (full file path)', () => {
const fullPath = buildRemoteUploadPath('C:\\Users\\admin', 'readme.txt');
const action = FileAction.create({
send: {
id: 3,
path: fullPath,
includeHidden: false,
fileNum: 0
}
});
const buf = FileAction.encode(action).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = FileAction.decode(buf).toJSON();
expect(decoded.send.path).toBe('C:\\Users\\admin\\readme.txt');
});
it('wraps file_action in Message for peer relay', () => {
const msg = Message.create({
fileAction: {
send: { id: 1, path: '/tmp/x.dat', includeHidden: false, fileNum: 0 }
}
});
const decoded = Message.decode(Message.encode(msg).finish()).toJSON();
expect(decoded.fileAction.send.path).toBe('/tmp/x.dat');
});
it('encodes FileResponse.digest for upload (operator sends metadata first)', () => {
const msg = Message.create({
fileResponse: {
digest: {
id: 5,
fileNum: 0,
fileSize: 12345,
lastModified: 1700000000
}
}
});
const decoded = Message.decode(Message.encode(msg).finish()).toJSON();
expect(decoded.fileResponse.digest.fileSize).toBe('12345');
expect(decoded.fileResponse.digest.lastModified).toBe('1700000000');
});
it('encodes FileResponse.digest with resume fields', () => {
const msg = Message.create({
fileResponse: {
digest: {
id: 9,
fileNum: 0,
fileSize: 999999,
lastModified: 1700000001,
isResume: true,
transferredSize: 131072
}
}
});
const decoded = Message.decode(Message.encode(msg).finish()).toJSON();
expect(decoded.fileResponse.digest.isResume).toBe(true);
expect(decoded.fileResponse.digest.transferredSize).toBe('131072');
});
it('encodes FileAction.send_confirm with offset_blk for resume', () => {
const action = FileAction.create({
sendConfirm: { id: 2, fileNum: 0, offsetBlk: 4 }
});
const decoded = FileAction.decode(FileAction.encode(action).finish()).toJSON();
expect(decoded.sendConfirm.offsetBlk).toBe(4);
});
});
describe('RDClient KeyEvent protobuf encoding', () => {
let Message;
beforeAll(async () => {
const root = await protobuf.load([
path.join(__dirname, '../protos/message.proto')
]);
Message = root.lookupType('hbb.Message');
});
it('encodes Map scancode keyEvent with mode Map', () => {
const buf = Message.encode(Message.fromObject({
keyEvent: { chr: 0x1E, down: true, press: false, modifiers: [], mode: 'Map' },
})).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = Message.decode(buf).toJSON();
expect(decoded.keyEvent.chr).toBe(30);
expect(decoded.keyEvent.mode).toBe('Map');
});
it('encodes Legacy controlKey with string enum name', () => {
const buf = Message.encode(Message.fromObject({
keyEvent: {
controlKey: 'Shift',
down: true,
press: false,
modifiers: [],
mode: 'Legacy',
},
})).finish();
expect(buf.length).toBeGreaterThan(0);
const decoded = Message.decode(buf).toJSON();
expect(decoded.keyEvent.controlKey).toBe('Shift');
expect(decoded.keyEvent.mode).toBe('Legacy');
});
it('encodes Legacy chr with CapsLock modifier', () => {
const buf = Message.encode(Message.fromObject({
keyEvent: {
chr: 97,
down: true,
press: false,
modifiers: ['CapsLock'],
mode: 'Legacy',
},
})).finish();
const decoded = Message.decode(buf).toJSON();
expect(decoded.keyEvent.modifiers).toEqual(['CapsLock']);
});
it('does not encode control_key snake_case alias on KeyEvent', () => {
const buf = Message.encode(Message.fromObject({
keyEvent: { control_key: 'Shift', down: true, mode: 'Legacy' },
})).finish();
const decoded = Message.decode(buf).toJSON();
expect(decoded.keyEvent?.controlKey).toBeUndefined();
});
it('create() leaves controlKey as Unknown (regression guard)', () => {
const msg = Message.create({
keyEvent: { controlKey: 'Shift', down: true, mode: 'Legacy' },
});
const decoded = Message.decode(Message.encode(msg).finish()).toJSON();
expect(decoded.keyEvent.controlKey).toBe('Unknown');
});
});