Skip to content

Commit fc38cff

Browse files
authored
Merge pull request #13 from leo-gall/function-mocking
feat: add functions support
2 parents a90fac9 + 6f40497 commit fc38cff

File tree

3 files changed

+425
-1
lines changed

3 files changed

+425
-1
lines changed

README.md

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,140 @@ void main() {
139139
}
140140
```
141141

142+
### Mocking Edge Functions
143+
144+
You can easily mock edge functions using the `registerEdgeFunction` method of `MockSupabaseHttpClient`. This method allows you to specify a handler function, giving you fine-grained control over the response based on the request body, HTTP method, and query parameters. You even have access to the mock database.
145+
146+
```dart
147+
group('Edge Functions Client', () {
148+
test('invoke registered edge function with POST', () async {
149+
mockHttpClient.registerEdgeFunction('greet',
150+
(body, queryParams, method, tables) {
151+
return FunctionResponse(
152+
data: {'message': 'Hello, ${body['name']}!'},
153+
status: 200,
154+
);
155+
});
156+
157+
final response = await mockSupabase.functions.invoke(
158+
'greet',
159+
body: {'name': 'Alice'},
160+
);
161+
162+
expect(response.status, 200);
163+
expect(response.data, {'message': 'Hello, Alice!'});
164+
});
165+
166+
test('invoke edge function with different HTTP methods', () async {
167+
mockHttpClient.registerEdgeFunction('say-hello',
168+
(body, queryParams, method, tables) {
169+
final name = switch (method) {
170+
HttpMethod.patch => 'Linda',
171+
HttpMethod.post => 'Bob',
172+
_ => 'Unknown'
173+
};
174+
return FunctionResponse(
175+
data: {'hello': name},
176+
status: 200,
177+
);
178+
});
179+
180+
var patchResponse = await mockSupabase.functions
181+
.invoke('say-hello', method: HttpMethod.patch);
182+
expect(patchResponse.data, {'hello': 'Linda'});
183+
184+
var postResponse = await mockSupabase.functions
185+
.invoke('say-hello', method: HttpMethod.post);
186+
expect(postResponse.data, {'hello': 'Bob'});
187+
});
188+
189+
test('edge function receives query params and body', () async {
190+
mockHttpClient.registerEdgeFunction('params-test',
191+
(body, queryParams, method, tables) {
192+
final city = queryParams['city'];
193+
final street = body['street'] as String;
194+
return FunctionResponse(
195+
data: {'address': '$street, $city'},
196+
status: 200,
197+
);
198+
});
199+
200+
final response = await mockSupabase.functions.invoke(
201+
'params-test',
202+
body: {'street': '123 Main St'},
203+
queryParameters: {'city': 'Springfield'},
204+
);
205+
206+
expect(response.data, {'address': '123 Main St, Springfield'});
207+
});
208+
209+
test('edge function returns different content types', () async {
210+
mockHttpClient.registerEdgeFunction('binary',
211+
(body, queryParams, method, tables) {
212+
return FunctionResponse(
213+
data: Uint8List.fromList([1, 2, 3]),
214+
status: 200,
215+
);
216+
});
217+
218+
var response = await mockSupabase.functions.invoke('binary');
219+
expect(response.data is Uint8List, true);
220+
expect((response.data as Uint8List).length, 3);
221+
222+
mockHttpClient.registerEdgeFunction('text',
223+
(body, queryParams, method, tables) {
224+
return FunctionResponse(
225+
data: 'Hello, world!',
226+
status: 200,
227+
);
228+
});
229+
230+
response = await mockSupabase.functions.invoke('text');
231+
expect(response.data, 'Hello, world!');
232+
233+
mockHttpClient.registerEdgeFunction('json',
234+
(body, queryParams, method, tables) {
235+
return FunctionResponse(
236+
data: {'key': 'value'},
237+
status: 200,
238+
);
239+
});
240+
241+
response = await mockSupabase.functions.invoke('json');
242+
expect(response.data, {'key': 'value'});
243+
});
244+
245+
test('edge function modifies mock database', () async {
246+
mockHttpClient.registerEdgeFunction('add-user',
247+
(body, queryParams, method, tables) {
248+
final users = tables['public.users'] ?? [];
249+
final newUser = {
250+
'id': users.length + 1,
251+
'name': body['name'],
252+
};
253+
users.add(newUser);
254+
tables['public.users'] = users;
255+
return FunctionResponse(data: newUser, status: 201);
256+
});
257+
258+
var users = await mockSupabase.from('users').select();
259+
expect(users, isEmpty);
260+
261+
final response = await mockSupabase.functions.invoke(
262+
'add-user',
263+
body: {'name': 'Alice'},
264+
);
265+
expect(response.status, 201);
266+
expect(response.data, {'id': 1, 'name': 'Alice'});
267+
268+
users = await mockSupabase.from('users').select();
269+
expect(users, [
270+
{'id': 1, 'name': 'Alice'}
271+
]);
272+
});
273+
});
274+
```
275+
142276
### Mocking Errors
143277

144278
You can simulate error scenarios by configuring an error trigger callback. This is useful for testing how your application handles various error conditions:

lib/src/mock_supabase_http_client.dart

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'dart:convert';
2+
import 'dart:typed_data';
23

34
import 'package:http/http.dart';
45
import 'package:supabase/supabase.dart';
@@ -50,6 +51,20 @@ import 'utils/filter_parser.dart';
5051
/// }
5152
/// ```
5253
///
54+
/// You can mock edge functions using the [registerEdgeFunction] callback:
55+
/// ```dart
56+
/// final client = MockSupabaseHttpClient();
57+
/// client.registerEdgeFunction(
58+
/// 'get_user_status',
59+
/// (body, queryParameters, method, tables) {
60+
/// return FunctionResponse(
61+
/// data: {'status': 'active'},
62+
/// status: 200,
63+
/// );
64+
///
65+
/// final response = await supabaseClient.functions.invoke('get_user_status');
66+
/// ```
67+
///
5368
/// You can simulate errors using the [postgrestExceptionTrigger] callback:
5469
/// ```dart
5570
/// final client = MockSupabaseHttpClient(
@@ -76,6 +91,14 @@ import 'utils/filter_parser.dart';
7691
/// {@endtemplate}
7792
class MockSupabaseHttpClient extends BaseClient {
7893
final Map<String, List<Map<String, dynamic>>> _database = {};
94+
final Map<
95+
String,
96+
FunctionResponse Function(
97+
Map<String, dynamic> body,
98+
Map<String, String> queryParameters,
99+
HttpMethod method,
100+
Map<String, List<Map<String, dynamic>>> tables,
101+
)> _edgeFunctions = {};
79102
final Map<
80103
String,
81104
dynamic Function(Map<String, dynamic>? params,
@@ -122,6 +145,7 @@ class MockSupabaseHttpClient extends BaseClient {
122145
// Clear the mock database and RPC functions
123146
_database.clear();
124147
_rpcHandler.reset();
148+
_edgeFunctions.clear();
125149
}
126150

127151
/// Registers a RPC function that can be called using the `rpc` method on a `Postgrest` client.
@@ -206,6 +230,19 @@ class MockSupabaseHttpClient extends BaseClient {
206230

207231
@override
208232
Future<StreamedResponse> send(BaseRequest request) async {
233+
final functionName = _extractFunctionName(request.url);
234+
if (functionName != null) {
235+
if (_edgeFunctions.containsKey(functionName)) {
236+
return _handleFunctionInvocation(functionName, request);
237+
} else {
238+
return _createResponse(
239+
{'error': 'Function $functionName not found'},
240+
statusCode: 404,
241+
request: request,
242+
);
243+
}
244+
}
245+
209246
// Decode the request body if it's not a GET, DELETE, or HEAD request
210247
dynamic body;
211248
if (request.method != 'GET' &&
@@ -791,13 +828,125 @@ class MockSupabaseHttpClient extends BaseClient {
791828
'content-type': 'application/json; charset=utf-8',
792829
...?headers,
793830
};
831+
Stream<List<int>> stream;
832+
if (data is Uint8List) {
833+
stream = Stream.value(data);
834+
responseHeaders['content-type'] = _getContentType(data);
835+
} else if (data is String) {
836+
stream = Stream.value(utf8.encode(data));
837+
responseHeaders['content-type'] = _getContentType(data);
838+
} else {
839+
final jsonData = jsonEncode(data);
840+
stream = Stream.value(utf8.encode(jsonData));
841+
}
842+
794843
return StreamedResponse(
795-
Stream.value(utf8.encode(data is String ? data : jsonEncode(data))),
844+
stream,
796845
statusCode,
797846
headers: responseHeaders,
798847
request: request,
799848
);
800849
}
850+
851+
/// Registers an edge function with the given name and handler.
852+
///
853+
/// The [name] parameter specifies the name of the edge function.
854+
///
855+
/// The [handler] parameter is a function that takes the following parameters:
856+
/// - [body]: A map containing the body of the request.
857+
/// - [queryParameters]: A map containing the query parameters of the request.
858+
/// - [method]: The HTTP method of the request.
859+
/// - [tables]: A map containing lists of maps representing the tables involved in the request.
860+
///
861+
/// The [handler] function should return a [FunctionResponse].
862+
void registerEdgeFunction(
863+
String name,
864+
FunctionResponse Function(
865+
Map<String, dynamic> body,
866+
Map<String, String> queryParameters,
867+
HttpMethod method,
868+
Map<String, List<Map<String, dynamic>>> tables,
869+
) handler,
870+
) {
871+
_edgeFunctions[name] = handler;
872+
}
873+
874+
String? _extractFunctionName(Uri url) {
875+
final pathSegments = url.pathSegments;
876+
// Handle functions endpoint: /functions/v1/{function_name}
877+
if (pathSegments.length >= 3 &&
878+
pathSegments[0] == 'functions' &&
879+
pathSegments[1] == 'v1') {
880+
return pathSegments[2];
881+
}
882+
return null;
883+
}
884+
885+
Future<StreamedResponse> _handleFunctionInvocation(
886+
String functionName,
887+
BaseRequest request,
888+
) async {
889+
if (!_edgeFunctions.containsKey(functionName)) {
890+
return _createResponse(
891+
{'error': 'Edge function $functionName not found'},
892+
statusCode: 404,
893+
request: request,
894+
);
895+
}
896+
897+
// Parse request data
898+
final tables = _database;
899+
final body = await _parseRequestBody(request);
900+
final queryParams = request.url.queryParameters;
901+
final method = _parseMethod(request.method);
902+
903+
// Call handler
904+
final response = _edgeFunctions[functionName]!(
905+
body ?? {},
906+
queryParams,
907+
method,
908+
tables,
909+
);
910+
911+
return _createResponse(
912+
response.data,
913+
statusCode: response.status,
914+
request: request,
915+
headers: {
916+
'content-type': _getContentType(response.data),
917+
},
918+
);
919+
}
920+
921+
Future<dynamic> _parseRequestBody(BaseRequest request) async {
922+
if (request is! Request) return null;
923+
final content = await request.finalize().transform(utf8.decoder).join();
924+
return content.isEmpty ? null : jsonDecode(content);
925+
}
926+
927+
HttpMethod _parseMethod(String method) {
928+
switch (method.toUpperCase()) {
929+
case 'GET':
930+
return HttpMethod.get;
931+
case 'POST':
932+
return HttpMethod.post;
933+
case 'PATCH':
934+
return HttpMethod.patch;
935+
case 'DELETE':
936+
return HttpMethod.delete;
937+
case 'PUT':
938+
return HttpMethod.put;
939+
default:
940+
return HttpMethod.get;
941+
}
942+
}
943+
944+
String _getContentType(dynamic data) {
945+
if (data is Uint8List) return 'application/octet-stream';
946+
if (data is String) return 'text/plain';
947+
if (data is Stream<List<int>>) return 'text/event-stream';
948+
return 'application/json';
949+
}
801950
}
802951

803952
/// Represents the different types of HTTP requests that can be made to the Supabase API

0 commit comments

Comments
 (0)