[web] validate WebDriver responses (#96884)
Validate WebDriver responses
diff --git a/packages/flutter_driver/lib/src/driver/web_driver.dart b/packages/flutter_driver/lib/src/driver/web_driver.dart
index 4e7852b..7852bfe 100644
--- a/packages/flutter_driver/lib/src/driver/web_driver.dart
+++ b/packages/flutter_driver/lib/src/driver/web_driver.dart
@@ -105,25 +105,60 @@
);
}
+ static DriverError _createMalformedExtensionResponseError(Object? data) {
+ throw DriverError(
+ 'Received malformed response from the FlutterDriver extension.\n'
+ 'Expected a JSON map containing a "response" field and, optionally, an '
+ '"isError" field, but got ${data.runtimeType}: $data'
+ );
+ }
+
@override
Future<Map<String, dynamic>> sendCommand(Command command) async {
- Map<String, dynamic> response;
+ final Map<String, dynamic> response;
+ final Object? data;
final Map<String, String> serialized = command.serialize();
_logCommunication('>>> $serialized');
try {
- final dynamic data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout);
- response = data != null ? (json.decode(data as String) as Map<String, dynamic>?)! : <String, dynamic>{};
+ data = await _connection.sendCommand("window.\$flutterDriver('${jsonEncode(serialized)}')", command.timeout);
+
+ // The returned data is expected to be a string. If it's null or anything
+ // other than a string, something's wrong.
+ if (data is! String) {
+ throw _createMalformedExtensionResponseError(data);
+ }
+
+ final Object? decoded = json.decode(data);
+ if (decoded is! Map<String, dynamic>) {
+ throw _createMalformedExtensionResponseError(data);
+ } else {
+ response = decoded;
+ }
+
_logCommunication('<<< $response');
+ } on DriverError catch(_) {
+ rethrow;
} catch (error, stackTrace) {
throw DriverError(
- "Failed to respond to $command due to remote error\n : \$flutterDriver('${jsonEncode(serialized)}')",
+ 'FlutterDriver command ${command.runtimeType} failed due to a remote error.\n'
+ 'Command sent: ${jsonEncode(serialized)}',
error,
stackTrace
);
}
- if (response['isError'] == true)
- throw DriverError('Error in Flutter application: ${response['response']}');
- return response['response'] as Map<String, dynamic>;
+
+ final Object? isError = response['isError'];
+ final Object? responseData = response['response'];
+ if (isError is! bool?) {
+ throw _createMalformedExtensionResponseError(data);
+ } else if (isError == true) {
+ throw DriverError('Error in Flutter application: $responseData');
+ }
+
+ if (responseData is! Map<String, dynamic>) {
+ throw _createMalformedExtensionResponseError(data);
+ }
+ return responseData;
}
@override
diff --git a/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart b/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart
index ff7661e..8679f66 100644
--- a/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart
+++ b/packages/flutter_driver/test/src/real_tests/flutter_driver_test.dart
@@ -745,8 +745,8 @@
const String waitForCommandLog = '>>> {command: waitFor, timeout: 1234, finderType: ByTooltipMessage, text: logCommunicationToFile test}';
const String responseLog = '<<< {isError: false, response: {status: ok}, type: Response}';
- expect(commandLog.contains(waitForCommandLog), true, reason: '$commandLog not contains $waitForCommandLog');
- expect(commandLog.contains(responseLog), true, reason: '$commandLog not contains $responseLog');
+ expect(commandLog, contains(waitForCommandLog), reason: '$commandLog not contains $waitForCommandLog');
+ expect(commandLog, contains(responseLog), reason: '$commandLog not contains $responseLog');
});
test('logCommunicationToFile = false', () async {
diff --git a/packages/flutter_driver/test/src/web_tests/web_driver_test.dart b/packages/flutter_driver/test/src/web_tests/web_driver_test.dart
new file mode 100644
index 0000000..9370e0e
--- /dev/null
+++ b/packages/flutter_driver/test/src/web_tests/web_driver_test.dart
@@ -0,0 +1,139 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'package:flutter_driver/src/common/error.dart';
+import 'package:flutter_driver/src/common/health.dart';
+import 'package:flutter_driver/src/driver/web_driver.dart';
+import 'package:webdriver/src/common/log.dart';
+
+import '../../common.dart';
+
+void main() {
+ group('WebDriver', () {
+ late FakeFlutterWebConnection fakeConnection;
+ late WebFlutterDriver driver;
+
+ setUp(() {
+ fakeConnection = FakeFlutterWebConnection();
+ driver = WebFlutterDriver.connectedTo(fakeConnection);
+ });
+
+ test('sendCommand succeeds', () async {
+ fakeConnection.fakeResponse = '''
+{
+ "isError": false,
+ "response": {
+ "test": "hello"
+ }
+}
+''';
+
+ final Map<String, Object?> response = await driver.sendCommand(const GetHealth());
+ expect(response['test'], 'hello');
+ });
+
+ test('sendCommand fails on communication error', () async {
+ fakeConnection.communicationError = Error();
+ expect(
+ () => driver.sendCommand(const GetHealth()),
+ _throwsDriverErrorWithMessage(
+ 'FlutterDriver command GetHealth failed due to a remote error.\n'
+ 'Command sent: {"command":"get_health"}'
+ ),
+ );
+ });
+
+ test('sendCommand fails on null', () async {
+ fakeConnection.fakeResponse = null;
+ expect(
+ () => driver.sendCommand(const GetHealth()),
+ _throwsDriverErrorWithDataString('Null', 'null'),
+ );
+ });
+
+ test('sendCommand fails when response data is not a string', () async {
+ fakeConnection.fakeResponse = 1234;
+ expect(
+ () => driver.sendCommand(const GetHealth()),
+ _throwsDriverErrorWithDataString('int', '1234'),
+ );
+ });
+
+ test('sendCommand fails when isError is true', () async {
+ fakeConnection.fakeResponse = '''
+{
+ "isError": true,
+ "response": "test error message"
+}
+''';
+ expect(
+ () => driver.sendCommand(const GetHealth()),
+ _throwsDriverErrorWithMessage(
+ 'Error in Flutter application: test error message'
+ ),
+ );
+ });
+
+ test('sendCommand fails when isError is not bool', () async {
+ fakeConnection.fakeResponse = '{ "isError": 5 }';
+ expect(
+ () => driver.sendCommand(const GetHealth()),
+ _throwsDriverErrorWithDataString('String', '{ "isError": 5 }'),
+ );
+ });
+
+ test('sendCommand fails when "response" field is not a JSON map', () async {
+ fakeConnection.fakeResponse = '{ "response": 5 }';
+ expect(
+ () => driver.sendCommand(const GetHealth()),
+ _throwsDriverErrorWithDataString('String', '{ "response": 5 }'),
+ );
+ });
+ });
+}
+
+Matcher _throwsDriverErrorWithMessage(String expectedMessage) {
+ return throwsA(allOf(
+ isA<DriverError>(),
+ predicate<DriverError>((DriverError error) {
+ final String actualMessage = error.message;
+ return actualMessage == expectedMessage;
+ }, 'contains message: $expectedMessage'),
+ ));
+}
+
+Matcher _throwsDriverErrorWithDataString(String dataType, String dataString) {
+ return _throwsDriverErrorWithMessage(
+ 'Received malformed response from the FlutterDriver extension.\n'
+ 'Expected a JSON map containing a "response" field and, optionally, an '
+ '"isError" field, but got $dataType: $dataString'
+ );
+}
+
+class FakeFlutterWebConnection implements FlutterWebConnection {
+ @override
+ bool supportsTimelineAction = false;
+
+ @override
+ Future<void> close() async {}
+
+ @override
+ Stream<LogEntry> get logs => throw UnimplementedError();
+
+ @override
+ Future<List<int>> screenshot() {
+ throw UnimplementedError();
+ }
+
+ Object? fakeResponse;
+ Error? communicationError;
+
+ @override
+ Future<Object?> sendCommand(String script, Duration? duration) async {
+ if (communicationError != null) {
+ throw communicationError!;
+ }
+ return fakeResponse;
+ }
+}