Skip to content

Commit 6ff2e83

Browse files
committed
rename errorTigger to postgrestExceptionTrigger
1 parent 93765ae commit 6ff2e83

File tree

3 files changed

+220
-16
lines changed

3 files changed

+220
-16
lines changed

README.md

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,142 @@ void main() {
139139
}
140140
```
141141

142+
### Mocking Errors
143+
144+
You can simulate error scenarios by configuring an error trigger callback. This is useful for testing how your application handles various error conditions:
145+
146+
```dart
147+
void main() {
148+
late final SupabaseClient mockSupabase;
149+
late final MockSupabaseHttpClient mockHttpClient;
150+
151+
setUp(() {
152+
// Configure error trigger
153+
mockHttpClient = MockSupabaseHttpClient(
154+
postgrestExceptionTrigger: (schema, table, data, type) {
155+
// Simulate unique constraint violation on email
156+
if (table == 'users' && type == RequestType.insert) {
157+
throw PostgrestException(
158+
message: 'duplicate key value violates unique constraint "users_email_key"',
159+
code: '23505', // Postgres unique violation code
160+
);
161+
}
162+
163+
// Simulate permission error for certain operations
164+
if (table == 'private_data' && type == RequestType.select) {
165+
throw PostgrestException(
166+
message: 'permission denied for table private_data',
167+
code: '42501', // Postgres permission denied code
168+
);
169+
}
170+
},
171+
);
172+
173+
mockSupabase = SupabaseClient(
174+
'https://mock.supabase.co',
175+
'fakeAnonKey',
176+
httpClient: mockHttpClient,
177+
);
178+
});
179+
180+
test('handles duplicate email error', () async {
181+
expect(
182+
() => mockSupabase.from('users').insert({
183+
'email': '[email protected]',
184+
'name': 'Test User'
185+
}),
186+
throwsA(isA<PostgrestException>()),
187+
);
188+
});
189+
}
190+
```
191+
192+
### RPC Functions
193+
194+
You can mock Remote Procedure Call (RPC) functions by registering them with the mock client:
195+
196+
```dart
197+
void main() {
198+
late final SupabaseClient mockSupabase;
199+
late final MockSupabaseHttpClient mockHttpClient;
200+
201+
setUp(() {
202+
mockHttpClient = MockSupabaseHttpClient();
203+
204+
// Register mock RPC functions
205+
mockHttpClient.registerRpcFunction(
206+
'get_user_status',
207+
(params, tables) => {'status': 'active', 'last_seen': '2024-03-20'},
208+
);
209+
210+
mockHttpClient.registerRpcFunction(
211+
'calculate_total',
212+
(params, tables) {
213+
final amount = params['amount'] as num;
214+
final tax = params['tax_rate'] as num;
215+
return {
216+
'total': amount + (amount * tax),
217+
'tax_amount': amount * tax,
218+
};
219+
},
220+
);
221+
222+
mockSupabase = SupabaseClient(
223+
'https://mock.supabase.co',
224+
'fakeAnonKey',
225+
httpClient: mockHttpClient,
226+
);
227+
});
228+
229+
test('calls RPC function with parameters', () async {
230+
final result = await mockSupabase.rpc(
231+
'calculate_total',
232+
params: {'amount': 100, 'tax_rate': 0.1},
233+
);
234+
235+
expect(result, {
236+
'total': 110.0,
237+
'tax_amount': 10.0,
238+
});
239+
});
240+
241+
test('mocks complex RPC function using database state', () async {
242+
// Insert some test data
243+
await mockSupabase.from('orders').insert([
244+
{'id': 1, 'user_id': 123, 'amount': 100},
245+
{'id': 2, 'user_id': 123, 'amount': 200},
246+
]);
247+
248+
// Register RPC that uses the mock database state
249+
mockHttpClient.registerRpcFunction(
250+
'get_user_total_orders',
251+
(params, tables) {
252+
final userId = params['user_id'];
253+
final orders = tables['public.orders'] as List<Map<String, dynamic>>;
254+
255+
final userOrders = orders.where((order) => order['user_id'] == userId);
256+
final total = userOrders.fold<num>(
257+
0,
258+
(sum, order) => sum + (order['amount'] as num),
259+
);
260+
261+
return {'total_orders': userOrders.length, 'total_amount': total};
262+
},
263+
);
264+
265+
final result = await mockSupabase.rpc(
266+
'get_user_total_orders',
267+
params: {'user_id': 123},
268+
);
269+
270+
expect(result, {
271+
'total_orders': 2,
272+
'total_amount': 300,
273+
});
274+
});
275+
}
276+
```
277+
142278
## Current Limitations
143279

144280
- The mock Supabase client does not know the table schema. This means that it does not know if the inserted mock data is a referenced table data, or just a array/JSON object. This could potentially return more data than you construct a mock data with more than one referenced table.
@@ -154,7 +290,6 @@ void main() {
154290
- count and head requests are not supported.
155291
- aggregate functions are not supported.
156292
- Respect nullsFirst on ordering is not supported.
157-
- rpc support is not supported.
158293
- The errors thrown by the mock Supabase client is not the same as the actual Supabase client.
159294
- The mock Supabase client does not support auth, realtime, storage, or calling edge functions.
160295
- You can either mock those using libraries like [mockito](https://pub.dev/packages/mockito) or use the Supabase CLI to do a full integration testing. You could use our [GitHub actions](https://github.com/supabase/setup-cli) to do that.

lib/src/mock_supabase_http_client.dart

Lines changed: 74 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,74 @@ import 'package:supabase/supabase.dart';
66
import 'handlers/rpc_handler.dart';
77
import 'utils/filter_parser.dart';
88

9+
/// {@template mock_supabase_http_client}
10+
/// A mock HTTP client for testing Supabase applications that simulates
11+
/// a Supabase API requests and responses by storing data in memory.
12+
///
13+
/// This client implements all the core Supabase operations including:
14+
/// * CRUD operations with filters and transformers
15+
/// * RPC functions
16+
/// * Custom error simulation
17+
///
18+
/// Example usage:
19+
/// ```dart
20+
/// final client = MockSupabaseHttpClient();
21+
///
22+
/// // Create a Supabase client with the mock HTTP client
23+
/// final supabase = SupabaseClient(
24+
/// 'https://mock.supabase.co',
25+
/// 'mock-key',
26+
/// httpClient: client,
27+
/// );
28+
///
29+
/// // Insert data
30+
/// await supabase.from('users').insert({
31+
/// 'id': 1,
32+
/// 'name': 'Alice',
33+
/// 'email': '[email protected]'
34+
/// });
35+
///
36+
/// // Query data
37+
/// final users = await supabase
38+
/// .from('users')
39+
/// .select()
40+
/// .eq('name', 'Alice');
41+
/// ```
42+
///
43+
/// The mock client maintains an in-memory database represented as:
44+
/// ```dart
45+
/// {
46+
/// 'public.users': [
47+
/// {'id': 1, ...},
48+
/// ...
49+
/// ]
50+
/// }
51+
/// ```
52+
///
53+
/// You can simulate errors using the [postgrestExceptionTrigger] callback:
54+
/// ```dart
55+
/// final client = MockSupabaseHttpClient(
56+
/// postgrestExceptionTrigger: (schema, table, data, type) {
57+
/// if (table == 'users' && type == RequestType.insert) {
58+
/// throw PostgrestException(
59+
/// message: 'Email already exists',
60+
/// code: '400',
61+
/// );
62+
/// }
63+
/// },
64+
/// );
65+
/// ```
66+
///
67+
/// The client supports custom RPC functions through [registerRpcFunction]:
68+
/// ```dart
69+
/// client.registerRpcFunction(
70+
/// 'get_user_status',
71+
/// (params, tables) => {'status': 'active'},
72+
/// );
73+
/// ```
74+
///
75+
/// A mock HTTP client that simulates Supabase backend operations for testing.
76+
/// {@endtemplate}
977
class MockSupabaseHttpClient extends BaseClient {
1078
final Map<String, List<Map<String, dynamic>>> _database = {};
1179
final Map<
@@ -15,12 +83,12 @@ class MockSupabaseHttpClient extends BaseClient {
1583

1684
/// A function that can be used to trigger errors.
1785
///
18-
/// Throw a PostgrestException within the `errorTrigger` to mock an error.
86+
/// Throw a PostgrestException within the `postgrestExceptionTrigger` to mock an error.
1987
///
2088
/// Example:
2189
/// ```dart
2290
/// final client = MockSupabaseHttpClient(
23-
/// errorTrigger: (schema, table, data, type) {
91+
/// postgrestExceptionTrigger: (schema, table, data, type) {
2492
/// if (table == 'users' && type == RequestType.insert) {
2593
/// throw PostgrestException(
2694
/// message: 'Email already exists', // Provide a message
@@ -38,12 +106,13 @@ class MockSupabaseHttpClient extends BaseClient {
38106
String? table,
39107
dynamic data,
40108
RequestType type,
41-
)? errorTrigger;
109+
)? postgrestExceptionTrigger;
42110

43111
late final RpcHandler _rpcHandler;
44112

113+
/// {@macro mock_supabase_http_client}
45114
MockSupabaseHttpClient({
46-
this.errorTrigger,
115+
this.postgrestExceptionTrigger,
47116
}) {
48117
_rpcHandler = RpcHandler(_rpcFunctions, _database);
49118
}
@@ -185,7 +254,7 @@ class MockSupabaseHttpClient extends BaseClient {
185254
);
186255

187256
try {
188-
errorTrigger?.call(
257+
postgrestExceptionTrigger?.call(
189258
schema,
190259
table,
191260
body,

test/mock_supabase_http_client_test.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,7 +1161,7 @@ void main() {
11611161
group('basic operation exceptions', () {
11621162
test('select throws PostgrestException', () async {
11631163
mockHttpClient = MockSupabaseHttpClient(
1164-
errorTrigger: (schema, table, data, type) {
1164+
postgrestExceptionTrigger: (schema, table, data, type) {
11651165
if (type == RequestType.select) {
11661166
throw PostgrestException(
11671167
code: '400',
@@ -1188,7 +1188,7 @@ void main() {
11881188

11891189
test('insert throws PostgrestException', () async {
11901190
mockHttpClient = MockSupabaseHttpClient(
1191-
errorTrigger: (schema, table, data, type) {
1191+
postgrestExceptionTrigger: (schema, table, data, type) {
11921192
if (type == RequestType.insert) {
11931193
throw PostgrestException(
11941194
code: '409',
@@ -1215,7 +1215,7 @@ void main() {
12151215

12161216
test('update throws PostgrestException', () async {
12171217
mockHttpClient = MockSupabaseHttpClient(
1218-
errorTrigger: (schema, table, data, type) {
1218+
postgrestExceptionTrigger: (schema, table, data, type) {
12191219
if (type == RequestType.update) {
12201220
throw PostgrestException(
12211221
code: '404',
@@ -1244,7 +1244,7 @@ void main() {
12441244

12451245
test('delete throws PostgrestException', () async {
12461246
mockHttpClient = MockSupabaseHttpClient(
1247-
errorTrigger: (schema, table, data, type) {
1247+
postgrestExceptionTrigger: (schema, table, data, type) {
12481248
if (type == RequestType.delete) {
12491249
throw PostgrestException(
12501250
code: '403',
@@ -1271,7 +1271,7 @@ void main() {
12711271

12721272
test('upsert throws PostgrestException', () async {
12731273
mockHttpClient = MockSupabaseHttpClient(
1274-
errorTrigger: (schema, table, data, type) {
1274+
postgrestExceptionTrigger: (schema, table, data, type) {
12751275
if (type == RequestType.upsert) {
12761276
throw PostgrestException(
12771277
code: '422',
@@ -1298,7 +1298,7 @@ void main() {
12981298

12991299
test('rpc throws PostgrestException', () async {
13001300
mockHttpClient = MockSupabaseHttpClient(
1301-
errorTrigger: (schema, table, data, type) {
1301+
postgrestExceptionTrigger: (schema, table, data, type) {
13021302
if (type == RequestType.rpc) {
13031303
throw PostgrestException(
13041304
code: '500',
@@ -1327,7 +1327,7 @@ void main() {
13271327
group('conditional exceptions', () {
13281328
test('throws when age is negative', () async {
13291329
mockHttpClient = MockSupabaseHttpClient(
1330-
errorTrigger: (schema, table, data, type) {
1330+
postgrestExceptionTrigger: (schema, table, data, type) {
13311331
if (type == RequestType.insert && data is Map) {
13321332
if (data['age'] != null && data['age'] < 0) {
13331333
throw PostgrestException(
@@ -1356,7 +1356,7 @@ void main() {
13561356

13571357
test('throws when email is invalid', () async {
13581358
mockHttpClient = MockSupabaseHttpClient(
1359-
errorTrigger: (schema, table, data, type) {
1359+
postgrestExceptionTrigger: (schema, table, data, type) {
13601360
if ((type == RequestType.insert || type == RequestType.update) &&
13611361
data is Map) {
13621362
if (data['email'] != null && !data['email'].contains('@')) {
@@ -1388,7 +1388,7 @@ void main() {
13881388

13891389
test('throws when table does not exist', () async {
13901390
mockHttpClient = MockSupabaseHttpClient(
1391-
errorTrigger: (schema, table, data, type) {
1391+
postgrestExceptionTrigger: (schema, table, data, type) {
13921392
if (table == 'non_existent_table') {
13931393
throw PostgrestException(
13941394
code: '404',
@@ -1415,7 +1415,7 @@ void main() {
14151415

14161416
test('throws when schema does not exist', () async {
14171417
mockHttpClient = MockSupabaseHttpClient(
1418-
errorTrigger: (schema, table, data, type) {
1418+
postgrestExceptionTrigger: (schema, table, data, type) {
14191419
if (schema == 'non_existent_schema') {
14201420
throw PostgrestException(
14211421
code: '404',

0 commit comments

Comments
 (0)