diff --git a/packages/supabase_flutter/analysis_options.yaml b/packages/supabase_flutter/analysis_options.yaml index 8d81c200c..5138acc94 100644 --- a/packages/supabase_flutter/analysis_options.yaml +++ b/packages/supabase_flutter/analysis_options.yaml @@ -3,3 +3,4 @@ include: package:flutter_lints/flutter.yaml linter: rules: avoid_print: false + trailing_commas: preserve diff --git a/packages/supabase_flutter/test/auth_test.dart b/packages/supabase_flutter/test/auth_test.dart new file mode 100644 index 000000000..fdda33310 --- /dev/null +++ b/packages/supabase_flutter/test/auth_test.dart @@ -0,0 +1,101 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'widget_test_stubs.dart'; + +class _MockLocalStorage extends MockLocalStorage { + bool _initializeCalled = false; + + bool get initializeCalled => _initializeCalled; + + @override + Future initialize() async { + _initializeCalled = true; + return super.initialize(); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const supabaseUrl = ''; + const supabaseKey = ''; + + group('Authentication', () { + setUp(() async { + try { + await Supabase.instance.dispose(); + } catch (e) { + // Ignore dispose errors + } + + mockAppLink(); + }); + + tearDown(() async { + try { + await Supabase.instance.dispose(); + } catch (e) { + // Ignore dispose errors + } + }); + + group('Session management', () { + test('initializes local storage on initialize', () async { + final mockStorage = _MockLocalStorage(); + + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: false, + authOptions: FlutterAuthClientOptions( + localStorage: mockStorage, + pkceAsyncStorage: MockAsyncStorage(), + ), + ); + + // Give time for initialization to complete + await Future.delayed(const Duration(milliseconds: 100)); + + expect(mockStorage.initializeCalled, isTrue); + }); + }); + + group('Session recovery', () { + test('handles corrupted session data gracefully', () async { + final corruptedStorage = MockExpiredStorage(); + + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: false, + authOptions: FlutterAuthClientOptions( + localStorage: corruptedStorage, + pkceAsyncStorage: MockAsyncStorage(), + ), + ); + + // MockExpiredStorage returns an expired session, not null + expect(Supabase.instance.client.auth.currentSession, isNotNull); + expect(Supabase.instance.client.auth.currentSession?.isExpired, isTrue); + }); + + test('handles null session during initialization', () async { + final emptyStorage = MockEmptyLocalStorage(); + + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: false, + authOptions: FlutterAuthClientOptions( + localStorage: emptyStorage, + pkceAsyncStorage: MockAsyncStorage(), + ), + ); + + // Should handle empty storage gracefully + expect(Supabase.instance.client.auth.currentSession, isNull); + }); + }); + }); +} diff --git a/packages/supabase_flutter/test/initialization_test.dart b/packages/supabase_flutter/test/initialization_test.dart new file mode 100644 index 000000000..7fceeb8cb --- /dev/null +++ b/packages/supabase_flutter/test/initialization_test.dart @@ -0,0 +1,174 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +import 'widget_test_stubs.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const supabaseUrl = ''; + const supabaseKey = ''; + + group('Supabase initialization', () { + setUp(() { + SharedPreferences.setMockInitialValues({}); + mockAppLink(); + }); + + tearDown(() async { + try { + await Supabase.instance.dispose(); + } catch (e) { + // Ignore dispose errors + } + }); + + group('Basic initialization', () { + test('initialize successfully with default options', () async { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + ); + + expect(Supabase.instance, isNotNull); + expect(Supabase.instance.client, isNotNull); + }); + }); + + group('Custom storage initialization', () { + test('initialize successfully with custom localStorage', () async { + final localStorage = MockLocalStorage(); + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + authOptions: FlutterAuthClientOptions( + localStorage: localStorage, + ), + ); + + expect(Supabase.instance, isNotNull); + expect(Supabase.instance.client, isNotNull); + }); + + test('handles initialization with expired session in storage', () async { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: true, + authOptions: FlutterAuthClientOptions( + localStorage: MockExpiredStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), + ); + + // Should handle expired session gracefully + expect(Supabase.instance.client.auth.currentSession, isNotNull); + }); + }); + + group('Auth options initialization', () { + test('initialize successfully with PKCE auth flow', () async { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + authOptions: const FlutterAuthClientOptions( + authFlowType: AuthFlowType.pkce, + ), + ); + + expect(Supabase.instance, isNotNull); + expect(Supabase.instance.client, isNotNull); + }); + }); + + group('Custom client initialization', () { + test('initialize successfully with custom HTTP client', () async { + final httpClient = PkceHttpClient(); + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + httpClient: httpClient, + ); + + expect(Supabase.instance, isNotNull); + expect(Supabase.instance.client, isNotNull); + }); + + test('initialize successfully with custom access token', () async { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + accessToken: () async => 'custom-access-token', + ); + + expect(Supabase.instance, isNotNull); + expect(Supabase.instance.client, isNotNull); + + // Should throw AuthException when trying to access auth + expect( + () => Supabase.instance.client.auth, + throwsA(isA()), + ); + }); + }); + + group('Multiple initialization and disposal', () { + test('dispose and reinitialize works', () async { + // First initialization + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + ); + + expect(Supabase.instance, isNotNull); + + // Dispose + await Supabase.instance.dispose(); + + // Need to run the event loop to let the dispose complete + await Future.delayed(Duration.zero); + + // Re-initialize should work without errors + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + ); + + expect(Supabase.instance, isNotNull); + }); + + test('handles multiple initializations correctly', () async { + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: false, + authOptions: FlutterAuthClientOptions( + localStorage: MockLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), + ); + + // Store first instance to verify it's different after re-initialization + final firstInstance = Supabase.instance.client; + + // Dispose first instance before re-initializing + await Supabase.instance.dispose(); + + await Supabase.initialize( + url: supabaseUrl, + anonKey: supabaseKey, + debug: true, + authOptions: FlutterAuthClientOptions( + localStorage: MockEmptyLocalStorage(), + pkceAsyncStorage: MockAsyncStorage(), + ), + ); + + final secondInstance = Supabase.instance.client; + expect(secondInstance, isNotNull); + expect(identical(firstInstance, secondInstance), isFalse); + }); + }); + }); +} diff --git a/packages/supabase_flutter/test/storage_test.dart b/packages/supabase_flutter/test/storage_test.dart new file mode 100644 index 000000000..e41a4c8e0 --- /dev/null +++ b/packages/supabase_flutter/test/storage_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:supabase_flutter/supabase_flutter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Storage Tests', () { + // SharedPreferencesLocalStorage Tests + group('SharedPreferencesLocalStorage', () { + const testSessionValue = '{"key": "value"}'; + + Future createFreshLocalStorage() async { + // Use a unique key for each test to ensure complete isolation + final uniqueKey = + 'test_persist_key_${DateTime.now().microsecondsSinceEpoch}'; + + // Set up fresh shared preferences for each test + SharedPreferences.setMockInitialValues({}); + + final localStorage = SharedPreferencesLocalStorage( + persistSessionKey: uniqueKey, + ); + await localStorage.initialize(); + return localStorage; + } + + test('hasAccessToken returns false when no session exists', () async { + final localStorage = await createFreshLocalStorage(); + final result = await localStorage.hasAccessToken(); + expect(result, false); + }); + + test('hasAccessToken returns true when session exists', () async { + final localStorage = await createFreshLocalStorage(); + await localStorage.persistSession(testSessionValue); + final result = await localStorage.hasAccessToken(); + expect(result, true); + }); + + test('accessToken returns null when no session exists', () async { + final localStorage = await createFreshLocalStorage(); + final result = await localStorage.accessToken(); + expect(result, null); + }); + + test('accessToken returns session string when session exists', () async { + final localStorage = await createFreshLocalStorage(); + await localStorage.persistSession(testSessionValue); + final result = await localStorage.accessToken(); + expect(result, testSessionValue); + }); + + test('persistSession stores session string', () async { + final localStorage = await createFreshLocalStorage(); + await localStorage.persistSession(testSessionValue); + + // Verify the session was stored by checking through localStorage's own methods + final hasToken = await localStorage.hasAccessToken(); + expect(hasToken, true); + + final storedValue = await localStorage.accessToken(); + expect(storedValue, testSessionValue); + }); + + test('removePersistedSession removes session', () async { + final localStorage = await createFreshLocalStorage(); + // First store a session + await localStorage.persistSession(testSessionValue); + expect(await localStorage.hasAccessToken(), true); + + // Then remove it + await localStorage.removePersistedSession(); + expect(await localStorage.hasAccessToken(), false); + expect(await localStorage.accessToken(), null); + }); + }); + + // SharedPreferencesGotrueAsyncStorage Tests + group('SharedPreferencesGotrueAsyncStorage', () { + late SharedPreferencesGotrueAsyncStorage asyncStorage; + const testKey = 'test_key'; + const testValue = 'test_value'; + + setUp(() async { + // Set up fake shared preferences + SharedPreferences.setMockInitialValues({}); + asyncStorage = SharedPreferencesGotrueAsyncStorage(); + // Allow for initialization to complete + await Future.delayed(const Duration(milliseconds: 100)); + }); + + test('setItem stores value for key', () async { + await asyncStorage.setItem(key: testKey, value: testValue); + final prefs = await SharedPreferences.getInstance(); + final storedValue = prefs.getString(testKey); + expect(storedValue, testValue); + }); + + test('getItem returns null when no value exists', () async { + final result = await asyncStorage.getItem(key: 'non_existent_key'); + expect(result, null); + }); + + test('getItem returns value when value exists', () async { + await asyncStorage.setItem(key: testKey, value: testValue); + final result = await asyncStorage.getItem(key: testKey); + expect(result, testValue); + }); + + test('removeItem removes value', () async { + // First store a value + await asyncStorage.setItem(key: testKey, value: testValue); + expect(await asyncStorage.getItem(key: testKey), testValue); + + // Then remove it + await asyncStorage.removeItem(key: testKey); + expect(await asyncStorage.getItem(key: testKey), null); + }); + }); + }); +} diff --git a/packages/supabase_flutter/test/version_test.dart b/packages/supabase_flutter/test/version_test.dart new file mode 100644 index 000000000..6378af393 --- /dev/null +++ b/packages/supabase_flutter/test/version_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:supabase_flutter/src/constants.dart'; +import 'package:supabase_flutter/src/version.dart'; + +void main() { + group('Version', () { + test('version is a non-empty string', () { + expect(version, isNotEmpty); + expect(version, isA()); + }); + }); + + group('Constants', () { + test('defaultHeaders contains expected keys', () { + expect(Constants.defaultHeaders, isA>()); + expect(Constants.defaultHeaders.keys, contains('X-Client-Info')); + }); + }); +}