Add tests for sending "cookie" and receiving "set-cookie" headers (#1113)
diff --git a/pkgs/cronet_http/example/integration_test/client_test.dart b/pkgs/cronet_http/example/integration_test/client_test.dart
index cc6b80e..f126f84 100644
--- a/pkgs/cronet_http/example/integration_test/client_test.dart
+++ b/pkgs/cronet_http/example/integration_test/client_test.dart
@@ -13,6 +13,8 @@
() => testAll(
CronetClient.defaultCronetEngine,
canStreamRequestBody: false,
+ canReceiveSetCookieHeaders: true,
+ canSendCookieHeaders: true,
));
group('from cronet engine', () {
diff --git a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
index a0823bf..3007123 100644
--- a/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
+++ b/pkgs/cupertino_http/example/integration_test/client_conformance_test.dart
@@ -11,11 +11,19 @@
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
group('defaultSessionConfiguration', () {
- testAll(CupertinoClient.defaultSessionConfiguration);
+ testAll(
+ CupertinoClient.defaultSessionConfiguration,
+ canReceiveSetCookieHeaders: true,
+ canSendCookieHeaders: true,
+ );
});
group('fromSessionConfiguration', () {
final config = URLSessionConfiguration.ephemeralSessionConfiguration();
- testAll(() => CupertinoClient.fromSessionConfiguration(config),
- canWorkInIsolates: false);
+ testAll(
+ () => CupertinoClient.fromSessionConfiguration(config),
+ canWorkInIsolates: false,
+ canReceiveSetCookieHeaders: true,
+ canSendCookieHeaders: true,
+ );
});
}
diff --git a/pkgs/http/test/io/client_conformance_test.dart b/pkgs/http/test/io/client_conformance_test.dart
index 20bf39f..65368e5 100644
--- a/pkgs/http/test/io/client_conformance_test.dart
+++ b/pkgs/http/test/io/client_conformance_test.dart
@@ -10,6 +10,9 @@
import 'package:test/test.dart';
void main() {
- testAll(IOClient.new, preservesMethodCase: false // https://dartbug.com/54187
- );
+ testAll(
+ IOClient.new, preservesMethodCase: false, // https://dartbug.com/54187
+ canReceiveSetCookieHeaders: true,
+ canSendCookieHeaders: true,
+ );
}
diff --git a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
index bd83c02..07903b5 100644
--- a/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
+++ b/pkgs/http_client_conformance_tests/lib/http_client_conformance_tests.dart
@@ -11,10 +11,12 @@
import 'src/redirect_tests.dart';
import 'src/request_body_streamed_tests.dart';
import 'src/request_body_tests.dart';
+import 'src/request_cookies_test.dart';
import 'src/request_headers_tests.dart';
import 'src/request_methods_tests.dart';
import 'src/response_body_streamed_test.dart';
import 'src/response_body_tests.dart';
+import 'src/response_cookies_test.dart';
import 'src/response_headers_tests.dart';
import 'src/response_status_line_tests.dart';
import 'src/server_errors_test.dart';
@@ -27,10 +29,12 @@
export 'src/redirect_tests.dart' show testRedirect;
export 'src/request_body_streamed_tests.dart' show testRequestBodyStreamed;
export 'src/request_body_tests.dart' show testRequestBody;
+export 'src/request_cookies_test.dart' show testRequestCookies;
export 'src/request_headers_tests.dart' show testRequestHeaders;
export 'src/request_methods_tests.dart' show testRequestMethods;
export 'src/response_body_streamed_test.dart' show testResponseBodyStreamed;
export 'src/response_body_tests.dart' show testResponseBody;
+export 'src/response_cookies_test.dart' show testResponseCookies;
export 'src/response_headers_tests.dart' show testResponseHeaders;
export 'src/response_status_line_tests.dart' show testResponseStatusLine;
export 'src/server_errors_test.dart' show testServerErrors;
@@ -54,6 +58,12 @@
/// If [preservesMethodCase] is `false` then tests that assume that the
/// [Client] preserves custom request method casing will be skipped.
///
+/// If [canSendCookieHeaders] is `false` then tests that require that "cookie"
+/// headers be sent by the client will not be run.
+///
+/// If [canReceiveSetCookieHeaders] is `false` then tests that require that
+/// "set-cookie" headers be received by the client will not be run.
+///
/// The tests are run against a series of HTTP servers that are started by the
/// tests. If the tests are run in the browser, then the test servers are
/// started in another process. Otherwise, the test servers are run in-process.
@@ -64,6 +74,8 @@
bool redirectAlwaysAllowed = false,
bool canWorkInIsolates = true,
bool preservesMethodCase = false,
+ bool canSendCookieHeaders = false,
+ bool canReceiveSetCookieHeaders = false,
}) {
testRequestBody(clientFactory());
testRequestBodyStreamed(clientFactory(),
@@ -82,4 +94,8 @@
testMultipleClients(clientFactory);
testClose(clientFactory);
testIsolate(clientFactory, canWorkInIsolates: canWorkInIsolates);
+ testRequestCookies(clientFactory(),
+ canSendCookieHeaders: canSendCookieHeaders);
+ testResponseCookies(clientFactory(),
+ canReceiveSetCookieHeaders: canReceiveSetCookieHeaders);
}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart
new file mode 100644
index 0000000..44653a7
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server.dart
@@ -0,0 +1,55 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:stream_channel/stream_channel.dart';
+
+/// Starts an HTTP server that captures "cookie" headers.
+///
+/// Channel protocol:
+/// On Startup:
+/// - send port
+/// On Request Received:
+/// - send a list of header lines starting with "cookie:"
+/// When Receive Anything:
+/// - exit
+void hybridMain(StreamChannel<Object?> channel) async {
+ late ServerSocket server;
+
+ server = (await ServerSocket.bind('localhost', 0))
+ ..listen((Socket socket) async {
+ final request = utf8.decoder.bind(socket).transform(const LineSplitter());
+
+ final cookies = <String>[];
+ request.listen((line) {
+ if (line.toLowerCase().startsWith('cookie:')) {
+ cookies.add(line);
+ }
+
+ if (line.isEmpty) {
+ // A blank line indicates the end of the headers.
+ channel.sink.add(cookies);
+ }
+ });
+
+ socket.writeAll(
+ [
+ 'HTTP/1.1 200 OK',
+ 'Access-Control-Allow-Origin: *',
+ 'Content-Length: 0',
+ '\r\n', // Add \r\n at the end of this header section.
+ ],
+ '\r\n', // Separate each field by \r\n.
+ );
+ await socket.close();
+ });
+
+ channel.sink.add(server.port);
+ await channel
+ .stream.first; // Any writes indicates that the server should exit.
+ unawaited(server.close());
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart
new file mode 100644
index 0000000..1f30e5f
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_vm.dart
@@ -0,0 +1,14 @@
+// Generated by generate_server_wrappers.dart. Do not edit.
+
+import 'package:stream_channel/stream_channel.dart';
+
+import 'request_cookies_server.dart';
+
+export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension;
+
+/// Starts the redirect test HTTP server in the same process.
+Future<StreamChannel<Object?>> startServer() async {
+ final controller = StreamChannelController<Object?>(sync: true);
+ hybridMain(controller.foreign);
+ return controller.local;
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart
new file mode 100644
index 0000000..31d961b
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_server_web.dart
@@ -0,0 +1,11 @@
+// Generated by generate_server_wrappers.dart. Do not edit.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension;
+
+/// Starts the redirect test HTTP server out-of-process.
+Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri(
+ scheme: 'package',
+ path: 'http_client_conformance_tests/src/request_cookies_server.dart'));
diff --git a/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart b/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart
new file mode 100644
index 0000000..a4eb78c
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/request_cookies_test.dart
@@ -0,0 +1,56 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. 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:async/async.dart';
+import 'package:http/http.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+import 'request_cookies_server_vm.dart'
+ if (dart.library.js_interop) 'request_cookies_server_web.dart';
+
+// The an HTTP header into [name, value].
+final headerSplitter = RegExp(':[ \t]+');
+
+/// Tests that the [Client] correctly sends "cookie" headers in the request.
+///
+/// If [canSendCookieHeaders] is `false` then tests that require that "cookie"
+/// headers be sent by the client will not be run.
+void testRequestCookies(Client client,
+ {bool canSendCookieHeaders = false}) async {
+ group('request cookies', () {
+ late final String host;
+ late final StreamChannel<Object?> httpServerChannel;
+ late final StreamQueue<Object?> httpServerQueue;
+
+ setUpAll(() async {
+ httpServerChannel = await startServer();
+ httpServerQueue = StreamQueue(httpServerChannel.stream);
+ host = 'localhost:${await httpServerQueue.nextAsInt}';
+ });
+ tearDownAll(() => httpServerChannel.sink.add(null));
+
+ test('one cookie', () async {
+ await client
+ .get(Uri.http(host, ''), headers: {'cookie': 'SID=298zf09hf012fh2'});
+
+ final cookies = (await httpServerQueue.next as List).cast<String>();
+ expect(cookies, hasLength(1));
+ final [header, value] = cookies[0].split(headerSplitter);
+ expect(header.toLowerCase(), 'cookie');
+ expect(value, 'SID=298zf09hf012fh2');
+ }, skip: canSendCookieHeaders ? false : 'cannot send cookie headers');
+
+ test('multiple cookies semicolon separated', () async {
+ await client.get(Uri.http(host, ''),
+ headers: {'cookie': 'SID=298zf09hf012fh2; lang=en-US'});
+
+ final cookies = (await httpServerQueue.next as List).cast<String>();
+ expect(cookies, hasLength(1));
+ final [header, value] = cookies[0].split(headerSplitter);
+ expect(header.toLowerCase(), 'cookie');
+ expect(value, 'SID=298zf09hf012fh2; lang=en-US');
+ }, skip: canSendCookieHeaders ? false : 'cannot send cookie headers');
+ });
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart
new file mode 100644
index 0000000..392e228
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server.dart
@@ -0,0 +1,44 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+
+import 'package:async/async.dart';
+import 'package:stream_channel/stream_channel.dart';
+
+/// Starts an HTTP server that returns a custom status line.
+///
+/// Channel protocol:
+/// On Startup:
+/// - send port
+/// On Request Received:
+/// - load response status line from channel
+/// - exit
+void hybridMain(StreamChannel<Object?> channel) async {
+ late HttpServer server;
+ final clientQueue = StreamQueue(channel.stream);
+
+ server = (await HttpServer.bind('localhost', 0))
+ ..listen((request) async {
+ await request.drain<void>();
+ final socket = await request.response.detachSocket(writeHeaders: false);
+
+ final headers = (await clientQueue.next) as List;
+ socket.writeAll(
+ [
+ 'HTTP/1.1 200 OK',
+ 'Access-Control-Allow-Origin: *',
+ 'Content-Length: 0',
+ ...headers,
+ '\r\n', // Add \r\n at the end of this header section.
+ ],
+ '\r\n', // Separate each field by \r\n.
+ );
+ await socket.close();
+ unawaited(server.close());
+ });
+
+ channel.sink.add(server.port);
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart
new file mode 100644
index 0000000..2edbb45
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_vm.dart
@@ -0,0 +1,14 @@
+// Generated by generate_server_wrappers.dart. Do not edit.
+
+import 'package:stream_channel/stream_channel.dart';
+
+import 'response_cookies_server.dart';
+
+export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension;
+
+/// Starts the redirect test HTTP server in the same process.
+Future<StreamChannel<Object?>> startServer() async {
+ final controller = StreamChannelController<Object?>(sync: true);
+ hybridMain(controller.foreign);
+ return controller.local;
+}
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart
new file mode 100644
index 0000000..cb8e384
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_server_web.dart
@@ -0,0 +1,11 @@
+// Generated by generate_server_wrappers.dart. Do not edit.
+
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+export 'server_queue_helpers.dart' show StreamQueueOfNullableObjectExtension;
+
+/// Starts the redirect test HTTP server out-of-process.
+Future<StreamChannel<Object?>> startServer() async => spawnHybridUri(Uri(
+ scheme: 'package',
+ path: 'http_client_conformance_tests/src/response_cookies_server.dart'));
diff --git a/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart b/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart
new file mode 100644
index 0000000..f8e154d
--- /dev/null
+++ b/pkgs/http_client_conformance_tests/lib/src/response_cookies_test.dart
@@ -0,0 +1,92 @@
+// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file
+// for details. 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:async/async.dart';
+import 'package:http/http.dart';
+import 'package:stream_channel/stream_channel.dart';
+import 'package:test/test.dart';
+
+import 'response_cookies_server_vm.dart'
+ if (dart.library.js_interop) 'response_cookies_server_web.dart';
+
+/// Tests that the [Client] correctly receives "set-cookie-headers"
+///
+/// If [canReceiveSetCookieHeaders] is `false` then tests that require that
+/// "set-cookie" headers be received by the client will not be run.
+void testResponseCookies(Client client,
+ {required bool canReceiveSetCookieHeaders}) async {
+ group('response cookies', () {
+ late String host;
+ late StreamChannel<Object?> httpServerChannel;
+ late StreamQueue<Object?> httpServerQueue;
+
+ setUp(() async {
+ httpServerChannel = await startServer();
+ httpServerQueue = StreamQueue(httpServerChannel.stream);
+ host = 'localhost:${await httpServerQueue.nextAsInt}';
+ });
+
+ test('single session cookie', () async {
+ httpServerChannel.sink.add(['Set-Cookie: SID=1231AB3']);
+ final response = await client.get(Uri.http(host, ''));
+
+ expect(response.headers['set-cookie'], 'SID=1231AB3');
+ },
+ skip: canReceiveSetCookieHeaders
+ ? false
+ : 'cannot receive set-cookie headers');
+
+ test('multiple session cookies', () async {
+ // RFC-2616 4.2 says:
+ // "The field value MAY be preceded by any amount of LWS, though a single
+ // SP is preferred." and
+ // "The field-content does not include any leading or trailing LWS ..."
+ httpServerChannel.sink
+ .add(['Set-Cookie: SID=1231AB3', 'Set-Cookie: lang=en_US']);
+ final response = await client.get(Uri.http(host, ''));
+
+ expect(
+ response.headers['set-cookie'],
+ matches(r'SID=1231AB3'
+ r'[ \t]*,[ \t]*'
+ r'lang=en_US'));
+ },
+ skip: canReceiveSetCookieHeaders
+ ? false
+ : 'cannot receive set-cookie headers');
+
+ test('permanent cookie with expires', () async {
+ httpServerChannel.sink
+ .add(['Set-Cookie: id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT']);
+ final response = await client.get(Uri.http(host, ''));
+
+ expect(response.headers['set-cookie'],
+ 'id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT');
+ },
+ skip: canReceiveSetCookieHeaders
+ ? false
+ : 'cannot receive set-cookie headers');
+
+ test('multiple permanent cookies with expires', () async {
+ // RFC-2616 4.2 says:
+ // "The field value MAY be preceded by any amount of LWS, though a single
+ // SP is preferred." and
+ // "The field-content does not include any leading or trailing LWS ..."
+ httpServerChannel.sink.add([
+ 'Set-Cookie: id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT',
+ 'Set-Cookie: id=2fasd; Expires=Wed, 21 Oct 2025 07:28:00 GMT'
+ ]);
+ final response = await client.get(Uri.http(host, ''));
+
+ expect(
+ response.headers['set-cookie'],
+ matches(r'id=a3fWa; Expires=Wed, 10 Jan 2024 07:28:00 GMT'
+ r'[ \t]*,[ \t]*'
+ r'id=2fasd; Expires=Wed, 21 Oct 2025 07:28:00 GMT'));
+ },
+ skip: canReceiveSetCookieHeaders
+ ? false
+ : 'cannot receive set-cookie headers');
+ });
+}