Skip to content
Merged
Show file tree
Hide file tree
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
11 changes: 6 additions & 5 deletions packages/functions_client/lib/src/functions_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,15 +119,16 @@ class FunctionsClient {
} else {
final bodyRequest = http.Request(method.name, uri);

final String? bodyStr;
if (body == null) {
bodyStr = null;
// No body to set
} else if (body is String) {
bodyStr = body;
bodyRequest.body = body;
} else if (body is Uint8List) {
bodyRequest.bodyBytes = body;
} else {
bodyStr = await _isolate.encode(body);
final bodyStr = await _isolate.encode(body);
bodyRequest.body = bodyStr;
}
if (bodyStr != null) bodyRequest.body = bodyStr;
request = bodyRequest;
}

Expand Down
49 changes: 45 additions & 4 deletions packages/functions_client/test/custom_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ class CustomHttpClient extends BaseClient {
headers: {
"Content-Type": "application/json",
},
reasonPhrase: "Enhance Your Calm",
);
} else if (request.url.path.endsWith('sse')) {
return StreamedResponse(
Expand All @@ -32,8 +33,37 @@ class CustomHttpClient extends BaseClient {
headers: {
"Content-Type": "text/event-stream",
});
} else if (request.url.path.endsWith('binary')) {
return StreamedResponse(
Stream.value([1, 2, 3, 4, 5]),
200,
request: request,
headers: {
"Content-Type": "application/octet-stream",
},
);
} else if (request.url.path.endsWith('text')) {
return StreamedResponse(
Stream.value(utf8.encode('Hello World')),
200,
request: request,
headers: {
"Content-Type": "text/plain",
},
);
} else if (request.url.path.endsWith('empty-json')) {
return StreamedResponse(
Stream.value([]),
200,
request: request,
headers: {
"Content-Type": "application/json",
},
);
} else {
final Stream<List<int>> stream;
final Map<String, String> headers;

if (request is MultipartRequest) {
stream = Stream.value(
utf8.encode(jsonEncode([
Expand All @@ -44,16 +74,27 @@ class CustomHttpClient extends BaseClient {
}
])),
);
headers = {"Content-Type": "application/json"};
} else {
stream = Stream.value(utf8.encode(jsonEncode({"key": "Hello World"})));
// Check if the request contains binary data (Uint8List)
final isOctetStream =
request.headers['Content-Type'] == 'application/octet-stream';
if (isOctetStream) {
// Return the original binary data
final bodyBytes = (request as Request).bodyBytes;
stream = Stream.value(bodyBytes);
headers = {"Content-Type": "application/octet-stream"};
} else {
stream =
Stream.value(utf8.encode(jsonEncode({"key": "Hello World"})));
headers = {"Content-Type": "application/json"};
}
}
return StreamedResponse(
stream,
200,
request: request,
headers: {
"Content-Type": "application/json",
},
headers: headers,
);
}
}
Expand Down
209 changes: 209 additions & 0 deletions packages/functions_client/test/functions_dart_test.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:typed_data';

import 'package:functions_client/src/functions_client.dart';
import 'package:functions_client/src/types.dart';
Expand Down Expand Up @@ -152,6 +153,214 @@ void main() {
expect(req.body, '{"thekey":"thevalue"}');
expect(req.headers["Content-Type"], contains("application/json"));
});

test('Uint8List is properly encoded as binary data', () async {
final binaryData = Uint8List.fromList([1, 2, 3, 4, 5]);
await functionsCustomHttpClient.invoke('function', body: binaryData);

final req = customHttpClient.receivedRequests.last;
expect(req, isA<Request>());

req as Request;
expect(req.bodyBytes, equals(binaryData));
expect(req.headers["Content-Type"], equals("application/octet-stream"));
});

test('null body sends no content-type', () async {
await functionsCustomHttpClient.invoke('function');

final req = customHttpClient.receivedRequests.last;
expect(req, isA<Request>());

req as Request;
expect(req.body, '');
expect(req.headers.containsKey("Content-Type"), isFalse);
});
});

group('HTTP methods', () {
test('GET method', () async {
await functionsCustomHttpClient.invoke(
'function',
method: HttpMethod.get,
);

final req = customHttpClient.receivedRequests.last;
expect(req.method, 'get');
});

test('PUT method', () async {
await functionsCustomHttpClient.invoke(
'function',
method: HttpMethod.put,
);

final req = customHttpClient.receivedRequests.last;
expect(req.method, 'put');
});

test('DELETE method', () async {
await functionsCustomHttpClient.invoke(
'function',
method: HttpMethod.delete,
);

final req = customHttpClient.receivedRequests.last;
expect(req.method, 'delete');
});

test('PATCH method', () async {
await functionsCustomHttpClient.invoke(
'function',
method: HttpMethod.patch,
);

final req = customHttpClient.receivedRequests.last;
expect(req.method, 'patch');
});
});

group('Headers', () {
test('setAuth updates authorization header', () async {
functionsCustomHttpClient.setAuth('new-token');

await functionsCustomHttpClient.invoke('function');

final req = customHttpClient.receivedRequests.last;
expect(req.headers['Authorization'], 'Bearer new-token');
});

test('headers getter returns current headers', () {
functionsCustomHttpClient.setAuth('test-token');

final headers = functionsCustomHttpClient.headers;
expect(headers['Authorization'], 'Bearer test-token');
expect(headers, contains('X-Client-Info'));
});

test('custom headers override defaults', () async {
await functionsCustomHttpClient.invoke(
'function',
headers: {'Content-Type': 'custom/type'},
);

final req = customHttpClient.receivedRequests.last;
expect(req.headers['Content-Type'], 'custom/type');
});

test('custom headers merge with defaults', () async {
await functionsCustomHttpClient.invoke(
'function',
headers: {'X-Custom': 'value'},
);

final req = customHttpClient.receivedRequests.last;
expect(req.headers['X-Custom'], 'value');
expect(req.headers, contains('X-Client-Info'));
});
});

group('Constructor variations', () {
test('constructor with all parameters', () {
final isolate = YAJsonIsolate();
final httpClient = CustomHttpClient();
final client = FunctionsClient(
'https://example.com',
{'X-Test': 'value'},
httpClient: httpClient,
isolate: isolate,
);

expect(client.headers['X-Test'], 'value');
expect(client.headers, contains('X-Client-Info'));
});

test('constructor with minimal parameters', () {
final client = FunctionsClient('https://example.com', {});

expect(client.headers, contains('X-Client-Info'));
});
});

group('Multipart requests', () {
test('multipart with both files and fields', () async {
await functionsCustomHttpClient.invoke(
'function',
body: {'field1': 'value1', 'field2': 'value2'},
files: [
MultipartFile.fromString('file1', 'content1'),
MultipartFile.fromString('file2', 'content2'),
],
);

final req = customHttpClient.receivedRequests.last;
expect(req.headers['Content-Type'], contains('multipart/form-data'));
expect(req, isA<MultipartRequest>());
});

test('multipart with only files', () async {
await functionsCustomHttpClient.invoke(
'function',
files: [MultipartFile.fromString('file', 'content')],
);

final req = customHttpClient.receivedRequests.last;
expect(req.headers['Content-Type'], contains('multipart/form-data'));
expect(req, isA<MultipartRequest>());
});
});

group('Response content types', () {
test('handles application/octet-stream response', () async {
final res = await functionsCustomHttpClient.invoke('binary');

expect(res.data, isA<Uint8List>());
expect(res.data, equals(Uint8List.fromList([1, 2, 3, 4, 5])));
expect(res.status, 200);
});

test('handles text/plain response', () async {
final res = await functionsCustomHttpClient.invoke('text');

expect(res.data, isA<String>());
expect(res.data, 'Hello World');
expect(res.status, 200);
});

test('handles empty JSON response', () async {
final res = await functionsCustomHttpClient.invoke('empty-json');

expect(res.data, '');
expect(res.status, 200);
});
});

group('Error handling', () {
test('FunctionException contains all error details', () async {
try {
await functionsCustomHttpClient.invoke('error-function');
fail('should throw');
} on FunctionException catch (e) {
expect(e.status, 420);
expect(e.details, isNotNull);
expect(e.reasonPhrase, isNotNull);
expect(e.toString(), contains('420'));
}
});
});

group('Edge cases', () {
test('multipart request with invalid body type throws assertion',
() async {
expect(
() => functionsCustomHttpClient.invoke(
'function',
body: 42, // Invalid: should be Map<String, String> for multipart
files: [MultipartFile.fromString('file', 'content')],
),
throwsA(isA<AssertionError>()),
);
});
});
});
}