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
78 changes: 43 additions & 35 deletions lib/src/mock_supabase_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -378,12 +378,12 @@ class MockSupabaseHttpClient extends BaseClient {

if (data is Map<String, dynamic>) {
_database[tableKey]!.add(data);
return _createResponse([data], request: request);
return _createRowsResponse([data], request: request);
} else if (data is List) {
final List<Map<String, dynamic>> items =
List<Map<String, dynamic>>.from(data);
_database[tableKey]!.addAll(items);
return _createResponse(items, request: request);
return _createRowsResponse(items, request: request);
} else {
return _createResponse({'error': 'Invalid data format'},
statusCode: 400, request: request);
Expand Down Expand Up @@ -412,7 +412,7 @@ class MockSupabaseHttpClient extends BaseClient {
var updated = false;

// Track updated rows
final updatedRows = [];
final updatedRows = <Map<String, dynamic>>[];

// Update items that match the filters
if (_database.containsKey(tableKey)) {
Expand All @@ -426,7 +426,7 @@ class MockSupabaseHttpClient extends BaseClient {
}

if (updated) {
return _createResponse(updatedRows, request: request);
return _createRowsResponse(updatedRows, request: request);
} else {
return _createResponse({'error': 'Not found'},
statusCode: 404, request: request);
Expand Down Expand Up @@ -503,7 +503,7 @@ class MockSupabaseHttpClient extends BaseClient {
return item;
}).toList();

return _createResponse(results, request: request);
return _createRowsResponse(results, request: request);
}

StreamedResponse _handleDelete(
Expand All @@ -520,7 +520,7 @@ class MockSupabaseHttpClient extends BaseClient {
statusCode: 400, request: request);
}

List removedItems = [];
final removedItems = <Map<String, dynamic>>[];
if (_database.containsKey(tableKey)) {
_database[tableKey]!.removeWhere((row) {
final matched = _matchesFilters(row: row, filters: queryParams);
Expand All @@ -531,7 +531,7 @@ class MockSupabaseHttpClient extends BaseClient {
});
}

return _createResponse(removedItems, request: request);
return _createRowsResponse(removedItems, request: request);
}

StreamedResponse _handleSelect(
Expand All @@ -543,7 +543,7 @@ class MockSupabaseHttpClient extends BaseClient {
final tableKey = '$schema.$table';
// Handle selecting data from the mock database
if (!_database.containsKey(tableKey)) {
return _createResponse([], request: request);
return _createRowsResponse([], request: request);
}

var returningRows = List<Map<String, dynamic>>.from(_database[tableKey]!);
Expand Down Expand Up @@ -740,39 +740,14 @@ class MockSupabaseHttpClient extends BaseClient {
final countType =
preferHeader.contains('count=exact') ? 'exact' : 'planned';

return _createResponse(returningRows, request: request, headers: {
return _createRowsResponse(returningRows, request: request, headers: {
'content-range': '$offset-${offset + returningRows.length}/$countValue',
'content-profile': tableKey,
'preference-applied': 'count=$countType'
});
}

// Handle single
if (request.headers['Accept'] == 'application/vnd.pgrst.object+json') {
if (returningRows.length == 1) {
return _createResponse(returningRows.first, request: request);
} else {
return _createResponse({
'error': '${returningRows.length} rows were found for single query'
}, request: request);
}
}

// Handle maybeSingle
if (request.headers['Accept'] == 'application/json') {
if (returningRows.isEmpty) {
return _createResponse(null, request: request);
} else if (returningRows.length == 1) {
return _createResponse(returningRows.first, request: request);
} else {
return _createResponse({
'error':
'${returningRows.length} rows were found for maybeSingle query'
}, statusCode: 405, request: request);
}
}

return _createResponse(returningRows, request: request);
return _createRowsResponse(returningRows, request: request);
}

StreamedResponse _handleHead(
Expand Down Expand Up @@ -835,6 +810,39 @@ class MockSupabaseHttpClient extends BaseClient {
);
}

/// Creates a response body shaped the way PostgREST would shape it for the
/// request's `Accept` header.
///
/// A request made through `.single()` asks for
/// `application/vnd.pgrst.object+json`, which returns a single object rather
/// than a list, and fails with a 406 when the result does not contain
/// exactly one row.
StreamedResponse _createRowsResponse(
List<Map<String, dynamic>> rows, {
required BaseRequest request,
Map<String, String>? headers,
}) {
final accept = request.headers['Accept'] ?? '';
if (!accept.startsWith('application/vnd.pgrst.object+json')) {
return _createResponse(rows, request: request, headers: headers);
}
if (rows.length == 1) {
return _createResponse(rows.first, request: request, headers: headers);
}
return _createResponse(
{
'code': 'PGRST116',
'details': 'Results contain ${rows.length} rows, '
'application/vnd.pgrst.object+json requires 1 row',
'hint': null,
'message': 'JSON object requested, multiple (or no) rows returned',
},
statusCode: 406,
request: request,
headers: headers,
);
}

StreamedResponse _createResponse(
dynamic data, {
int statusCode = 200,
Expand Down
107 changes: 107 additions & 0 deletions test/mock_supabase_http_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -729,6 +729,113 @@ void main() {
expect(() => mockSupabase.from('posts').select().maybeSingle(),
throwsException);
});

test('Insert then single', () async {
final post = await mockSupabase
.from('posts')
.insert({'id': 1, 'title': 'First post'})
.select()
.single();
expect(post, {'id': 1, 'title': 'First post'});
});

test('Insert then maybeSingle', () async {
final post = await mockSupabase
.from('posts')
.insert({'id': 1, 'title': 'First post'})
.select()
.maybeSingle();
expect(post, {'id': 1, 'title': 'First post'});
});

test('Insert multiple rows then single throws', () async {
expect(
() => mockSupabase
.from('posts')
.insert([
{'id': 1, 'title': 'First post'},
{'id': 2, 'title': 'Second post'},
])
.select()
.single(),
throwsA(isA<PostgrestException>()
.having((error) => error.code, 'code', 'PGRST116')),
);
});

test('Upsert then single', () async {
await mockSupabase.from('posts').insert({'id': 1, 'title': 'First post'});
final post = await mockSupabase
.from('posts')
.upsert({'id': 1, 'title': 'Updated post'})
.select()
.single();
expect(post, {'id': 1, 'title': 'Updated post'});
});

test('Update then single', () async {
await mockSupabase.from('posts').insert({'id': 1, 'title': 'First post'});
final post = await mockSupabase
.from('posts')
.update({'title': 'Updated post'})
.eq('id', 1)
.select()
.single();
expect(post, {'id': 1, 'title': 'Updated post'});
});

test('Delete then single', () async {
await mockSupabase.from('posts').insert({'id': 1, 'title': 'First post'});
final post = await mockSupabase
.from('posts')
.delete()
.eq('id', 1)
.select()
.single();
expect(post, {'id': 1, 'title': 'First post'});
});

test('Select single with no rows throws', () async {
expect(
() => mockSupabase.from('posts').select().single(),
throwsA(isA<PostgrestException>()
.having((error) => error.code, 'code', 'PGRST116')),
);
});

test('Delete with no match then maybeSingle returns null', () async {
await mockSupabase.from('posts').insert({'id': 1, 'title': 'First post'});
final post = await mockSupabase
.from('posts')
.delete()
.eq('id', 2)
.select()
.maybeSingle();
expect(post, null);
});

test('Single with count', () async {
await mockSupabase.from('posts').insert({'id': 1, 'title': 'First post'});
final response = await mockSupabase
.from('posts')
.select()
.single()
.count(CountOption.exact);
expect(response.data, {'id': 1, 'title': 'First post'});
expect(response.count, 1);
});

test('Select single with multiple rows throws', () async {
await mockSupabase.from('posts').insert([
{'id': 1, 'title': 'First post'},
{'id': 2, 'title': 'Second post'},
]);
expect(
() => mockSupabase.from('posts').select().single(),
throwsA(isA<PostgrestException>()
.having((error) => error.code, 'code', 'PGRST116')),
);
});
});

group('Referenced table queries', () {
Expand Down
Loading