Files
rustdesk/flutter/test/file_model_test.dart
T
RustDeskandClaude Opus 5.5 9997b7a3b8 test: remove tests that do not pin user-visible behavior (#16539)
* test: remove tests that do not pin user-visible behavior

Apply the AGENTS.md test rules to the existing Rust and Flutter suites.
A test is kept when it is the only coverage of user- or API-visible
behavior, the regression test of a reported or fixed bug, a security
boundary (one test per rejection class), or a check against an external
reference. Tests that assert private state or the order of internal
steps, pin review-round corner cases, or repeat what a kept test already
covers are removed.

Rust: 679 -> 298 tests. Dart: 118 -> 45 tests.

Test-only helpers left without callers go with them
(applyLastOnlineStateForTesting, terminalKeyboardRowWidth,
debugBufferedInputCount, tokio's test-util dev feature), and cfg gates
and visibility that existed only for the deleted tests are narrowed.
Runtime behavior is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* test: keep tests that pin security and user-visible behavior

Restore six tests that review showed are the only coverage of their
behavior:

- update metadata rejects an artifact file name with path components;
- ICE candidates from another session are dropped;
- KCP delivers every multi-segment frame queued before a close;
- set_files rejects an empty name in a multi-file transfer;
- a lopsided elongated cursor keeps its centre hotspot (#16242);
- toggling Show hidden during a pending same-path read issues a new read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* test: keep offline update and key-exchange downgrade rejections

Restore three tests that are the only coverage of their rejection class:

- offline update metadata rejects a signed artifact with an http or
  credential-bearing URL;
- offline verification rejects a renamed local file and a version that
  does not match the release;
- a key-exchange pick lowered in transit leaves both sides unable to
  decrypt, so a v0 fallback after a failed v1 decrypt cannot pass
  unnoticed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

* test: keep the decomposed-grapheme and reconnect-cancel tests

Each catches a plausible wrong change that passes every kept test:
mapping the first scalar under a locked Ctrl turns é into \x05,
and a reconnect that does not fail pending reads leaves them hanging
until the 30 s timeout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WS1HTjp6UQuG43KEcQPq1R

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:45:22 +08:00

210 lines
7.2 KiB
Dart

import 'dart:convert';
import 'package:flutter_hbb/models/file_model.dart';
import 'package:flutter_hbb/models/model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:uuid/uuid.dart';
final _sessionId = UuidValue('00000000-0000-0000-0000-000000000000');
class _FakeFFI implements FFI {
@override
String id = 'test-peer';
@override
UuidValue get sessionId => _sessionId;
@override
late final FfiModel ffiModel = FfiModel(WeakReference(this));
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
FileController _createController(FileFetcher fileFetcher) {
final ffi = _FakeFFI();
return FileController(
isLocal: false,
getSessionID: () => _sessionId,
rootState: WeakReference(ffi),
jobController: JobController(() => _sessionId, () => null),
fileFetcher: fileFetcher,
getOtherSideDirectoryData: () =>
DirectoryData(FileDirectory(), DirectoryOptions()),
);
}
FileDirectory _directory(String path) => FileDirectory()..path = path;
String _directoryJson(String path) => jsonEncode({
'id': 0,
'path': path,
'entries': <Object>[],
});
class _SentRead {
final String path;
final bool includeHidden;
const _SentRead(this.path, this.includeHidden);
}
void main() {
test('a fast remote response is matched after registration', () async {
late final FileFetcher fileFetcher;
fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, __) {
fileFetcher.tryCompleteTask(_directoryJson(path), 'false');
return Future<void>.value();
},
);
final directory = await fileFetcher.fetchDirectory('/fast', false, false);
expect(directory.path, '/fast');
});
test('a resolved Home path completes the sole empty-path request', () async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final controller = _createController(fileFetcher);
controller.directory.value = _directory('/initial');
final home = controller.openDirectory('');
await Future<void>.delayed(Duration.zero);
final response = _directoryJson('/home/user');
controller.initDirAndHome({'value': response});
expect(controller.homePath, '/home/user');
expect(controller.directory.value.path, '/initial');
fileFetcher.tryCompleteTask(response, 'false');
expect(await home, isTrue);
expect(controller.directory.value.path, '/home/user');
expect(sent.single.path, isEmpty);
});
test('an automatic response initializes Home without a pending request', () {
final controller = _createController(FileFetcher(() => _sessionId));
controller.initDirAndHome({'value': _directoryJson('/home/user')});
expect(controller.homePath, '/home/user');
expect(controller.directory.value.path, '/home/user');
});
test('a read error completes the sole pending request', () async {
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, __, ___) async {},
);
final request = fileFetcher.fetchDirectory('/denied', false, false);
await Future<void>.delayed(Duration.zero);
final expectation = expectLater(request, throwsA('permission denied'));
fileFetcher.tryCompleteRemoteTaskWithError('permission denied');
await expectation;
expect(fileFetcher.hasPendingRemoteRead('/denied'), isFalse);
});
test('same-path requests share the pending read', () async {
final sent = <_SentRead>[];
late final FileFetcher fileFetcher;
fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final controller = _createController(fileFetcher);
controller.directory.value = _directory('/initial');
final first = controller.openDirectory('/same');
final waiting = controller.openDirectory('/same');
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.path), ['/same']);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect(await first, isTrue);
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.path), ['/same']);
expect(await waiting, isTrue);
expect(controller.directory.value.path, '/same');
});
test('same-path requests with different hidden options are serialized',
() async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final first = fileFetcher.fetchDirectory('/same', false, false);
final second = fileFetcher.fetchDirectory('/same', false, true);
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false]);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await first).path, '/same');
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false, true]);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await second).path, '/same');
});
test('session invalidation cancels active and waiting reads', () async {
final sent = <_SentRead>[];
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, path, includeHidden) async {
sent.add(_SentRead(path, includeHidden));
},
);
final first = fileFetcher.fetchDirectory('/same', false, false);
final waiting = fileFetcher.fetchDirectory('/same', false, true);
await Future<void>.delayed(Duration.zero);
final firstError = expectLater(first, throwsA(isA<StateError>()));
final waitingError = expectLater(waiting, throwsA(isA<StateError>()));
fileFetcher.beginRemoteSession();
await firstError;
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false]);
await waitingError;
expect(fileFetcher.hasPendingRemoteRead('/same'), isFalse);
final replacement = fileFetcher.fetchDirectory('/same', false, true);
await Future<void>.delayed(Duration.zero);
expect(sent.map((request) => request.includeHidden), [false, true]);
fileFetcher.tryCompleteTask(_directoryJson('/same'), 'false');
expect((await replacement).path, '/same');
});
test('navigation ignores stale directory responses', () async {
final fileFetcher = FileFetcher(
() => _sessionId,
readRemoteDirectory: (_, __, ___) async {},
);
final controller = _createController(fileFetcher);
controller.directory.value = _directory('/initial');
final stale = controller.openDirectory('/stale');
final latest = controller.openDirectory('/latest');
await Future<void>.delayed(Duration.zero);
fileFetcher.tryCompleteTask(_directoryJson('/latest'), 'false');
expect(await latest, isTrue);
fileFetcher.tryCompleteTask(_directoryJson('/stale'), 'false');
expect(await stale, isTrue);
expect(controller.directory.value.path, '/latest');
});
}