Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions packages/devtools_app/test/http/curl_command_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import 'dart:typed_data';

import 'package:devtools_app/devtools_app.dart';
import 'package:devtools_app/src/shared/http/curl_command.dart';
import 'package:devtools_app_shared/utils.dart';
import 'package:devtools_test/devtools_test.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:vm_service/vm_service.dart';

Expand Down Expand Up @@ -275,8 +277,193 @@ void main() {
expect(CurlCommand.from(data).toString(), contains("--data-raw '{}'"));
});
});

// Regression coverage for the request body across the lifecycle of a
// request in the Network tab. The body is known once the request has been
// sent, so it must be available for Copy as cURL whether the request is
// still awaiting its response, completed, or failed, and it must survive the
// profile refreshes that replace the underlying request while it is pending.
// See https://github.com/flutter/devtools/pull/9963.
group('NetworkCurlCommand request body lifecycle', () {
const body = '{"email":"user@example.com"}';
const curlWithHeaderAndBody =
"curl --location --request POST 'https://example.com/api/login' "
"\\\n--header 'content-type: application/json' "
"\\\n--data-raw '$body'";

test('includes body for a pending request awaiting its response', () {
final data = DartIOHttpRequestData(
_parseProfileRequest(
requestSent: true,
response: null,
requestBody: utf8.encode(body),
),
requestFullDataFromVmService: false,
);

expect(data.inProgress, isTrue);
expect(data.requestBody, body);
expect(CurlCommand.from(data).toString(), curlWithHeaderAndBody);
});

test('includes body for a completed request', () {
final data = DartIOHttpRequestData(
_parseProfileRequest(
requestSent: true,
response: _completedResponseJson,
requestBody: utf8.encode(body),
),
requestFullDataFromVmService: false,
);

expect(data.inProgress, isFalse);
expect(data.requestBody, body);
expect(CurlCommand.from(data).toString(), curlWithHeaderAndBody);
});

test('includes body for a request that failed without a response', () {
final data = DartIOHttpRequestData(
_parseProfileRequest(
requestSent: true,
requestError: 'Connection timed out',
response: null,
requestBody: utf8.encode(body),
),
requestFullDataFromVmService: false,
);

expect(data.didFail, isTrue);
expect(data.requestBody, body);
expect(CurlCommand.from(data).toString(), contains("--data-raw '$body'"));
});

// These tests exercise `getFullRequestData`, which fetches the body from
// the VM service when a request is selected, followed by `merge` calls
// that simulate `getHttpProfile` polling.
group('with VM service', () {
tearDown(() => removeGlobal(ServiceConnectionManager));

test(
'retains body fetched while pending across profile refreshes',
() async {
_serveFullRequestFromVmService(
_parseProfileRequest(
requestSent: true,
response: null,
requestBody: utf8.encode(body),
),
);

// Entries from `getHttpProfile` polling never carry bodies.
final data = DartIOHttpRequestData(
_parseProfileRequest(requestSent: true, response: null),
requestFullDataFromVmService: false,
);

// Selecting the request in the Network tab fetches its full data.
await data.getFullRequestData();
expect(data.inProgress, isTrue);
expect(data.requestBody, body);
expect(CurlCommand.from(data).toString(), curlWithHeaderAndBody);

// The next poll replaces the profile entry while still pending.
data.merge(
DartIOHttpRequestData(
_parseProfileRequest(requestSent: true, response: null),
requestFullDataFromVmService: false,
),
);
expect(data.inProgress, isTrue);
expect(data.requestBody, body);
expect(CurlCommand.from(data).toString(), curlWithHeaderAndBody);

// The response eventually completes.
data.merge(
DartIOHttpRequestData(
_parseProfileRequest(
requestSent: true,
response: _completedResponseJson,
),
requestFullDataFromVmService: false,
),
);
expect(data.inProgress, isFalse);
expect(data.requestBody, body);
expect(CurlCommand.from(data).toString(), curlWithHeaderAndBody);
},
);
});
});
}

/// Sets up a fake VM service whose `getHttpProfileRequest` returns [request].
void _serveFullRequestFromVmService(HttpProfileRequest request) {
setGlobal(
ServiceConnectionManager,
FakeServiceConnectionManager(
service: FakeServiceManager.createFakeService(
httpProfile: HttpProfile(
requests: [request],
timestamp: DateTime.fromMicrosecondsSinceEpoch(0),
),
),
),
);
}

/// Parses an [HttpProfileRequest] shaped like the dart:io HTTP profiler JSON.
///
/// dart:io only reports `endTime` and `request` once the request has been
/// fully sent ([requestSent]), and `response` once a response starts.
HttpProfileRequest _parseProfileRequest({
required bool requestSent,
required Map<String, Object?>? response,
String? requestError,
List<int>? requestBody,
}) {
return HttpProfileRequest.parse({
'id': '1',
'isolateId': 'isolates/0',
'method': 'POST',
'uri': 'https://example.com/api/login',
'events': <Object>[],
'startTime': 0,
if (requestSent) ...{
'endTime': 1000,
'request': requestError != null
? {'error': requestError}
: {
'headers': {
'content-type': ['application/json'],
},
'connectionInfo': <String, Object?>{},
'contentLength': requestBody?.length ?? 0,
'cookies': <Object>[],
'followRedirects': true,
'maxRedirects': 5,
'persistentConnection': true,
},
},
'response': ?response,
'requestBody': ?requestBody,
})!;
}

const _completedResponseJson = <String, Object?>{
'startTime': 2000,
'endTime': 3000,
'headers': <String, Object?>{},
'compressionState': 'notCompressed',
'connectionInfo': <String, Object?>{},
'contentLength': 0,
'cookies': <Object>[],
'isRedirect': false,
'persistentConnection': true,
'reasonPhrase': 'OK',
'redirects': <Object>[],
'statusCode': 200,
};

class _TestDartIOHttpRequestData extends DartIOHttpRequestData {
_TestDartIOHttpRequestData(this._request) : super(_request);

Expand Down
Loading