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
105 changes: 97 additions & 8 deletions lib/src/mock_supabase_http_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class MockSupabaseHttpClient extends BaseClient {
);

// Decode the request body if it's not a GET request
final body = (request.method != 'GET' && request.method != 'DELETE') &&
final body = (request.method != 'GET' &&
request.method != 'DELETE' &&
request.method != 'HEAD') &&
request is Request
? jsonDecode(await request.finalize().transform(utf8.decoder).join())
: null;
Expand All @@ -43,6 +45,8 @@ class MockSupabaseHttpClient extends BaseClient {
return _handleDelete(tableName, body, request);
case 'GET':
return _handleSelect(tableName, request.url.queryParameters, request);
case 'HEAD':
return _handleHead(tableName, request.url.queryParameters, request);
default:
return _createResponse({'error': 'Method not allowed'},
statusCode: 405, request: request);
Expand Down Expand Up @@ -219,7 +223,10 @@ class MockSupabaseHttpClient extends BaseClient {
}

StreamedResponse _handleSelect(
String tableName, Map<String, String> queryParams, BaseRequest request) {
String tableName,
Map<String, String> queryParams,
BaseRequest request,
) {
// Handle selecting data from the mock database
if (!_database.containsKey(tableName)) {
return _createResponse([], request: request);
Expand Down Expand Up @@ -283,6 +290,9 @@ class MockSupabaseHttpClient extends BaseClient {
}
});

// Get the count value before any limiting
final countValue = returningRows.length;

// Handle top level table ordering
if (queryParams.containsKey('order')) {
final orderParams = queryParams['order']!.split('.');
Expand Down Expand Up @@ -316,9 +326,12 @@ class MockSupabaseHttpClient extends BaseClient {
}).toList();
});

final offset = queryParams.containsKey('offset')
? int.parse(queryParams['offset']!)
: 0;

// Handle top level table offset
if (queryParams.containsKey('offset')) {
final offset = int.parse(queryParams['offset']!);
if (offset > 0) {
returningRows = returningRows.skip(offset).toList();
}

Expand Down Expand Up @@ -395,13 +408,28 @@ class MockSupabaseHttpClient extends BaseClient {
// Handle top level column selection
if (!selectedColumns.contains('*')) {
returningRows = returningRows.map((row) {
print(row);
return Map<String, dynamic>.fromEntries(row.entries
.where((entry) => selectedColumns.contains(entry.key)));
}).toList();
}
}

// Handle count
final preferHeader = request.headers['Prefer'];
final isCountRequest =
preferHeader != null && preferHeader.contains('count=');

if (isCountRequest) {
final countType =
preferHeader.contains('count=exact') ? 'exact' : 'planned';

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

// Handle single
if (request.headers['Accept'] == 'application/vnd.pgrst.object+json') {
if (returningRows.length == 1) {
Expand Down Expand Up @@ -430,6 +458,61 @@ class MockSupabaseHttpClient extends BaseClient {
return _createResponse(returningRows, request: request);
}

StreamedResponse _handleHead(
String tableName, Map<String, String> queryParams, BaseRequest request) {
// Perform the same filtering as in _handleSelect
var returningRows =
List<Map<String, dynamic>>.from(_database[tableName] ?? []);

// Apply filters (you may want to extract this to a separate method)
queryParams.forEach((key, value) {
if (key != 'select' &&
key != 'order' &&
key != 'limit' &&
key != 'range') {
final filter = _parseFilter(
columnName: key,
postrestFilter: value,
targetRow: returningRows.isNotEmpty ? returningRows.first : {},
);
returningRows = returningRows.where((item) => filter(item)).toList();
}
});

// Handle count
final preferHeader = request.headers['Prefer'];
final isCountRequest =
preferHeader != null && preferHeader.contains('count=');

if (isCountRequest) {
final count = returningRows.length;
final countType =
preferHeader.contains('count=exact') ? 'exact' : 'planned';

// Return only headers for HEAD request
return StreamedResponse(
Stream.value([]), // Empty body for HEAD request
200,
headers: {
'content-range': '0-$count/$count',
'content-profile': tableName,
'preference-applied': 'count=$countType'
},
request: request,
);
}

// If it's not a count request, return basic headers
return StreamedResponse(
Stream.value([]), // Empty body for HEAD request
200,
headers: {
'content-profile': tableName,
},
request: request,
);
}

bool Function(Map<String, dynamic> row) _parseFilter({
required String columnName,
required String postrestFilter,
Expand Down Expand Up @@ -530,12 +613,18 @@ class MockSupabaseHttpClient extends BaseClient {
}

StreamedResponse _createResponse(dynamic data,
{int statusCode = 200, required BaseRequest request}) {
// Create a response for the mock client
{int statusCode = 200,
required BaseRequest request,
Map<String, String>? headers}) {
final responseHeaders = {
'content-type': 'application/json; charset=utf-8',
...?headers,
};

return StreamedResponse(
Stream.value(utf8.encode(jsonEncode(data))),
statusCode,
headers: {'content-type': 'application/json; charset=utf-8'},
headers: responseHeaders,
request: request,
);
}
Expand Down
74 changes: 74 additions & 0 deletions test/mock_supabase_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,17 @@ void main() {
expect(posts.length, 1);
});

test('limit with a filter', () async {
await mockSupabase.from('posts').insert([
{'id': 1, 'title': 'First post', 'author_id': 1},
{'id': 2, 'title': 'Second post', 'author_id': 2},
{'id': 3, 'title': 'Third post', 'author_id': 1}
]);
final posts =
await mockSupabase.from('posts').select().eq('author_id', 1).limit(1);
expect(posts.length, 1);
});

test('Order', () async {
await mockSupabase.from('posts').insert([
{'id': 1, 'title': 'First post'},
Expand Down Expand Up @@ -648,6 +659,69 @@ void main() {
});
});

group('count', () {
test('count', () async {
await mockSupabase.from('posts').insert([
{'title': 'First post'},
{'title': 'Second post'}
]);

final count = await mockSupabase.from('posts').count();
expect(count, 2);
});

test('count with data', () async {
await mockSupabase.from('posts').insert([
{'title': 'First post'},
{'title': 'Second post'}
]);
final response = await mockSupabase.from('posts').select().count();
expect(response.data.length, 2);
expect(response.data.first['title'], 'First post');
expect(response.count, 2);
});

test('count with filter', () async {
await mockSupabase.from('posts').insert([
{'title': 'First post', 'author_id': 1},
{'title': 'Second post', 'author_id': 2},
{'title': 'Third post', 'author_id': 1}
]);
final count = await mockSupabase.from('posts').count().eq('author_id', 1);
expect(count, 2);
});

test('count with data and filter', () async {
await mockSupabase.from('posts').insert([
{'title': 'First post', 'author_id': 1},
{'title': 'Second post', 'author_id': 2},
{'title': 'Third post', 'author_id': 1}
]);
final response =
await mockSupabase.from('posts').select().eq('author_id', 1).count();
expect(response.data.length, 2);
expect(response.data.first['title'], 'First post');
expect(response.count, 2);
});

test('count with filter and modifier', () async {
await mockSupabase.from('posts').insert([
{'title': 'First post', 'author_id': 1},
{'title': 'Second post', 'author_id': 2},
{'title': 'Third post', 'author_id': 1}
]);
final response = await mockSupabase
.from('posts')
.select()
.eq('author_id', 1)
.limit(1)
.count();
expect(response.data.length, 1);
expect(response.data.first['title'], 'First post');
expect(response.count, 2);
});
});

group('non-ASCII characters tests', () {
test('Insert Japanese text', () async {
await mockSupabase.from('posts').insert({'title': 'こんにちは'});
Expand Down