diff --git a/packages/supabase/test/client_test.dart b/packages/supabase/test/client_test.dart index 7db3f47cb..1aeef4a1f 100644 --- a/packages/supabase/test/client_test.dart +++ b/packages/supabase/test/client_test.dart @@ -282,4 +282,208 @@ void main() { expect(xClientInfoHeader, 'supabase-flutter/0.0.0'); }); }); + + group('Client Advanced Features', () { + late SupabaseClient supabase; + const supabaseUrl = 'https://example.supabase.co'; + const supabaseKey = 'test-key'; + + setUp(() { + supabase = SupabaseClient(supabaseUrl, supabaseKey); + }); + + tearDown(() async { + await supabase.dispose(); + }); + + group('Headers Management', () { + test('should update headers and propagate to all clients', () { + final newHeaders = {'Custom-Header': 'custom-value'}; + supabase.headers = newHeaders; + + expect(supabase.headers['Custom-Header'], 'custom-value'); + expect(supabase.rest.headers['Custom-Header'], 'custom-value'); + expect(supabase.functions.headers['Custom-Header'], 'custom-value'); + expect(supabase.storage.headers['Custom-Header'], 'custom-value'); + expect(supabase.realtime.headers['Custom-Header'], 'custom-value'); + }); + + test('should preserve default headers when setting custom headers', () { + final newHeaders = {'Custom-Header': 'custom-value'}; + supabase.headers = newHeaders; + + expect(supabase.headers['X-Client-Info'], startsWith('supabase-dart/')); + }); + + test('should not update auth headers when using custom access token', () { + final customTokenClient = SupabaseClient( + supabaseUrl, + supabaseKey, + accessToken: () async => 'custom-token', + ); + + final newHeaders = {'Custom-Header': 'custom-value'}; + customTokenClient.headers = newHeaders; + + expect(customTokenClient.headers['Custom-Header'], 'custom-value'); + }); + }); + + group('Error Handling', () { + test( + 'should throw AuthException when accessing auth with custom access token', + () { + final customTokenClient = SupabaseClient( + supabaseUrl, + supabaseKey, + accessToken: () async => 'custom-token', + ); + + expect( + () => customTokenClient.auth, + throwsA(isA()), + ); + }); + }); + + group('Schema Support', () { + test('should create query builder with custom schema', () { + final customSchema = supabase.schema('custom'); + expect(customSchema, isA()); + + final queryBuilder = customSchema.from('table'); + expect(queryBuilder, isA()); + }); + + test('should handle nested schema calls', () { + final schema1 = supabase.schema('schema1'); + final schema2 = schema1.schema('schema2'); + + expect(schema2, isA()); + }); + }); + + group('RPC Support', () { + test('should create RPC call', () { + final rpcCall = supabase.rpc('test_function'); + expect(rpcCall, isA()); + }); + + test('should create RPC call with parameters', () { + final rpcCall = + supabase.rpc('test_function', params: {'param': 'value'}); + expect(rpcCall, isA()); + }); + + test('should create RPC call with get flag', () { + final rpcCall = supabase.rpc('test_function', params: {}, get: true); + expect(rpcCall, isA()); + }); + }); + + group('Client Options', () { + test('should accept custom Postgrest options', () { + final client = SupabaseClient( + supabaseUrl, + supabaseKey, + postgrestOptions: PostgrestClientOptions(schema: 'custom_schema'), + ); + + expect(client, isA()); + }); + + test('should accept custom Auth options', () { + final client = SupabaseClient( + supabaseUrl, + supabaseKey, + authOptions: AuthClientOptions(autoRefreshToken: false), + ); + + expect(client, isA()); + }); + + test('should accept custom Storage options', () { + final client = SupabaseClient( + supabaseUrl, + supabaseKey, + storageOptions: StorageClientOptions(retryAttempts: 5), + ); + + expect(client, isA()); + }); + + test('should accept custom Realtime options', () { + final client = SupabaseClient( + supabaseUrl, + supabaseKey, + realtimeClientOptions: + RealtimeClientOptions(logLevel: RealtimeLogLevel.debug), + ); + + expect(client, isA()); + }); + }); + + group('Dispose', () { + test('should properly dispose all resources', () async { + final client = SupabaseClient(supabaseUrl, supabaseKey); + + // Should not throw + await client.dispose(); + }); + }); + }); + + group('Query Schema', () { + late SupabaseClient supabase; + const supabaseUrl = 'https://example.supabase.co'; + const supabaseKey = 'test-key'; + + setUp(() { + supabase = SupabaseClient(supabaseUrl, supabaseKey); + }); + + tearDown(() async { + await supabase.dispose(); + }); + + test('should create SupabaseQueryBuilder from schema', () { + final schema = supabase.schema('custom_schema'); + final queryBuilder = schema.from('test_table'); + + expect(queryBuilder, isA()); + }); + + test('should create nested schemas', () { + final schema1 = supabase.schema('schema1'); + final schema2 = schema1.schema('schema2'); + + expect(schema2, isA()); + }); + }); + + group('Query Builder', () { + late SupabaseClient supabase; + const supabaseUrl = 'https://example.supabase.co'; + const supabaseKey = 'test-key'; + + setUp(() { + supabase = SupabaseClient(supabaseUrl, supabaseKey); + }); + + tearDown(() async { + await supabase.dispose(); + }); + + group('Stream Creation', () { + test('should throw assertion error for empty primary key', () { + final queryBuilder = supabase.from('test_table'); + + expect( + () => queryBuilder.stream(primaryKey: []), + throwsA(isA()), + ); + }); + }); + }); } diff --git a/packages/supabase/test/mock_test.dart b/packages/supabase/test/mock_test.dart index f3f4f53ef..dc5bf1a01 100644 --- a/packages/supabase/test/mock_test.dart +++ b/packages/supabase/test/mock_test.dart @@ -1,3 +1,5 @@ +// ignore_for_file: deprecated_member_use_from_same_package + import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -679,4 +681,118 @@ void main() { expect(stream, emits(isList)); }); }); + + group('Deprecated execute method', () { + test('should work with deprecated execute method', () { + handleRequests(mockServer); + final streamBuilder = supabase.from('todos').stream(primaryKey: ['id']); + final stream = streamBuilder.execute(); + expect(stream, emits(isList)); + }); + }); + + group('Error Handling', () { + group('RealtimeSubscribeException', () { + test('should create exception with status only', () { + final exception = + RealtimeSubscribeException(RealtimeSubscribeStatus.timedOut); + + expect(exception.status, RealtimeSubscribeStatus.timedOut); + expect(exception.details, isNull); + expect(exception.toString(), contains('timedOut')); + }); + + test('should create exception with status and details', () { + final exception = RealtimeSubscribeException( + RealtimeSubscribeStatus.channelError, 'Connection failed'); + + expect(exception.status, RealtimeSubscribeStatus.channelError); + expect(exception.details, 'Connection failed'); + expect(exception.toString(), contains('channelError')); + expect(exception.toString(), contains('Connection failed')); + }); + }); + + group('Stream Error Handling', () { + test('should handle postgrest errors gracefully', () async { + final errorServer = await HttpServer.bind('localhost', 0); + + // Setup server to return error for rest requests + errorServer.listen((request) { + if (request.uri.path.contains('/rest/')) { + request.response + ..statusCode = HttpStatus.unauthorized + ..headers.contentType = ContentType.json + ..write('{"error": "Unauthorized"}') + ..close(); + } else { + request.response + ..statusCode = HttpStatus.ok + ..close(); + } + }); + + final errorClient = SupabaseClient( + 'http://${errorServer.address.host}:${errorServer.port}', + 'test-key', + headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + ); + + final stream = errorClient.from('todos').stream(primaryKey: ['id']); + + bool errorReceived = false; + final completer = Completer(); + + final subscription = stream.listen( + (_) {}, + onError: (error) { + errorReceived = true; + completer.complete(); + }, + ); + + await completer.future.timeout(Duration(seconds: 5)); + expect(errorReceived, isTrue); + + await subscription.cancel(); + await errorClient.dispose(); + await errorServer.close(); + }); + + test('should handle access token retrieval errors', () async { + final clientWithFailingToken = SupabaseClient( + 'http://${mockServer.address.host}:${mockServer.port}', + 'test-key', + accessToken: () async { + throw Exception('Token retrieval failed'); + }, + headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + ); + + // Should handle token errors gracefully + expect( + () async => await clientWithFailingToken.from('test').select(), + throwsA(isA()), + ); + + await clientWithFailingToken.dispose(); + }); + }); + + group('Dispose Error Handling', () { + test('should handle dispose errors gracefully', () async { + final client = SupabaseClient( + 'http://${mockServer.address.host}:${mockServer.port}', + 'test-key', + headers: {'X-Client-Info': 'supabase-flutter/0.0.0'}, + ); + + // First dispose should succeed + await client.dispose(); + + // Operations after dispose should not throw + expect(() => client.from('test'), returnsNormally); + }); + }); + }); } diff --git a/packages/supabase/test/utilities_test.dart b/packages/supabase/test/utilities_test.dart new file mode 100644 index 000000000..3a709b4d5 --- /dev/null +++ b/packages/supabase/test/utilities_test.dart @@ -0,0 +1,166 @@ +// ignore_for_file: deprecated_member_use_from_same_package + +import 'dart:io'; + +import 'package:http/http.dart' as http; +import 'package:supabase/src/auth_http_client.dart'; +import 'package:supabase/src/constants.dart'; +import 'package:supabase/src/counter.dart'; +import 'package:supabase/src/supabase_event_types.dart'; +import 'package:test/test.dart'; + +void main() { + group('Counter', () { + late Counter counter; + + setUp(() { + counter = Counter(); + }); + + test('should start with value 0', () { + expect(counter.value, 0); + }); + + test('should increment and return previous value', () { + expect(counter.increment(), 0); + expect(counter.value, 1); + }); + + test('should increment multiple times correctly', () { + expect(counter.increment(), 0); + expect(counter.increment(), 1); + expect(counter.increment(), 2); + expect(counter.value, 3); + }); + }); + + group('Constants', () { + test('should have default headers with X-Client-Info', () { + expect(Constants.defaultHeaders, + containsPair('X-Client-Info', startsWith('supabase-dart/'))); + }); + + test('should include platform headers when not on web', () { + if (!kIsWeb) { + expect( + Constants.defaultHeaders, contains('X-Supabase-Client-Platform')); + expect(Constants.defaultHeaders, + contains('X-Supabase-Client-Platform-Version')); + } + }); + + test('should have platform getter', () { + if (kIsWeb) { + expect(Constants.platform, isNull); + } else { + expect(Constants.platform, isNotNull); + expect(Constants.platform, isA()); + } + }); + + test('should have platformVersion getter', () { + if (kIsWeb) { + expect(Constants.platformVersion, isNull); + } else { + expect(Constants.platformVersion, isNotNull); + expect(Constants.platformVersion, isA()); + } + }); + }); + + group('AuthHttpClient', () { + late HttpServer mockServer; + late AuthHttpClient authClient; + const supabaseKey = 'test-supabase-key'; + + setUp(() async { + mockServer = await HttpServer.bind('localhost', 0); + mockServer.listen((request) { + request.response + ..statusCode = HttpStatus.ok + ..headers.contentType = ContentType.json + ..write('{"success": true}') + ..close(); + }); + + authClient = AuthHttpClient( + supabaseKey, + http.Client(), + () async => 'test-access-token', + ); + }); + + tearDown(() async { + await mockServer.close(); + }); + + test('should add Authorization header with access token', () async { + final uri = Uri.parse( + 'http://${mockServer.address.host}:${mockServer.port}/test'); + final request = http.Request('GET', uri); + + await authClient.send(request); + + expect(request.headers['Authorization'], 'Bearer test-access-token'); + }); + + test('should add apikey header', () async { + final uri = Uri.parse( + 'http://${mockServer.address.host}:${mockServer.port}/test'); + final request = http.Request('GET', uri); + + await authClient.send(request); + + expect(request.headers['apikey'], supabaseKey); + }); + + test('should use supabase key when access token is null', () async { + final authClientNoToken = AuthHttpClient( + supabaseKey, + http.Client(), + () async => null, + ); + + final uri = Uri.parse( + 'http://${mockServer.address.host}:${mockServer.port}/test'); + final request = http.Request('GET', uri); + + await authClientNoToken.send(request); + + expect(request.headers['Authorization'], 'Bearer $supabaseKey'); + }); + + test('should not override existing Authorization header', () async { + final uri = Uri.parse( + 'http://${mockServer.address.host}:${mockServer.port}/test'); + final request = http.Request('GET', uri); + request.headers['Authorization'] = 'Bearer existing-token'; + + await authClient.send(request); + + expect(request.headers['Authorization'], 'Bearer existing-token'); + }); + + test('should not override existing apikey header', () async { + final uri = Uri.parse( + 'http://${mockServer.address.host}:${mockServer.port}/test'); + final request = http.Request('GET', uri); + request.headers['apikey'] = 'existing-key'; + + await authClient.send(request); + + expect(request.headers['apikey'], 'existing-key'); + }); + }); + + group('SupabaseEventTypes', () { + test('should return correct name for each event type', () { + expect(SupabaseEventTypes.insert.name(), 'INSERT'); + expect(SupabaseEventTypes.update.name(), 'UPDATE'); + expect(SupabaseEventTypes.delete.name(), 'DELETE'); + expect(SupabaseEventTypes.all.name(), '*'); + expect(SupabaseEventTypes.broadcast.name(), 'BROADCAST'); + expect(SupabaseEventTypes.presence.name(), 'PRESENCE'); + }); + }); +}