Skip to content

chore: More test coverage on supabase_flutter #1193

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 8 commits into
base: main
Choose a base branch
from
Draft
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
42 changes: 32 additions & 10 deletions packages/supabase_flutter/test/auth_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -62,39 +62,61 @@ void main() {
});

group('Session recovery', () {
test('handles corrupted session data gracefully', () async {
final corruptedStorage = MockExpiredStorage();

test('handles expired session with auto-refresh disabled', () async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
debug: false,
authOptions: FlutterAuthClientOptions(
localStorage: corruptedStorage,
localStorage: MockExpiredStorage(),
pkceAsyncStorage: MockAsyncStorage(),
autoRefreshToken: false,
),
);

// MockExpiredStorage returns an expired session, not null
expect(Supabase.instance.client.auth.currentSession, isNotNull);
expect(Supabase.instance.client.auth.currentSession?.isExpired, isTrue);
// Give it a delay to wait for recoverSession to throw
await Future.delayed(const Duration(milliseconds: 100));

await expectLater(Supabase.instance.client.auth.onAuthStateChange,
emitsError(isA<AuthException>()));
});

test('handles null session during initialization', () async {
final emptyStorage = MockEmptyLocalStorage();

await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
debug: false,
authOptions: FlutterAuthClientOptions(
localStorage: emptyStorage,
localStorage: MockEmptyLocalStorage(),
pkceAsyncStorage: MockAsyncStorage(),
),
);

// Should handle empty storage gracefully
expect(Supabase.instance.client.auth.currentSession, isNull);

// Verify initial session event
final event =
await Supabase.instance.client.auth.onAuthStateChange.first;
expect(event.event, AuthChangeEvent.initialSession);
expect(event.session, isNull);
});

test('handles expired session with auto-refresh enabled', () async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
debug: false,
authOptions: FlutterAuthClientOptions(
localStorage: MockExpiredStorage(),
pkceAsyncStorage: MockAsyncStorage(),
autoRefreshToken: true,
),
);

// With auto-refresh enabled, expired session should be handled
expect(Supabase.instance.client.auth.currentSession, isNotNull);
expect(Supabase.instance.client.auth.currentSession?.isExpired, isTrue);
});
});
});
Expand Down
90 changes: 90 additions & 0 deletions packages/supabase_flutter/test/deep_link_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import 'package:app_links/app_links.dart';
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';
Expand Down Expand Up @@ -68,5 +69,94 @@ void main() {
expect(pkceHttpClient.requestCount, 1);
expect(pkceHttpClient.lastRequestBody['auth_code'], 'my-code-verifier');
});

tearDown(() async {
try {
await Supabase.instance.dispose();
} catch (e) {
// Ignore dispose errors in tests
}
});
});

group('Deep Link Error Handling', () {
setUp(() {
SharedPreferences.setMockInitialValues({});
mockAppLink();
});

tearDown(() async {
try {
await Supabase.instance.dispose();
} catch (e) {
// Ignore dispose errors in tests
}
});

test('handles malformed deep link URL gracefully', () async {
// This test simulates error handling in deep link processing
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
authOptions: FlutterAuthClientOptions(
localStorage: MockEmptyLocalStorage(),
),
);

// The initialization should complete successfully even if there are
// potential deep link errors to handle
expect(Supabase.instance.client, isNotNull);
});

test('handles non-auth deep links correctly', () async {
// Mock a deep link that is not auth-related
mockAppLink(
initialLink: 'com.supabase://other-action/?param=value',
);

await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
authOptions: FlutterAuthClientOptions(
localStorage: MockEmptyLocalStorage(),
),
);

// Should initialize normally without attempting auth
expect(Supabase.instance.client, isNotNull);
});

test('handles auth deep link without proper parameters', () async {
// Mock a deep link that looks like auth but missing required params
mockAppLink(
initialLink: 'com.supabase://callback/?error=access_denied',
);

await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
authOptions: FlutterAuthClientOptions(
localStorage: MockEmptyLocalStorage(),
),
);

// Should initialize normally and handle the error case
expect(Supabase.instance.client, isNotNull);
});

test('handles empty deep link', () async {
// Mock empty initial link
mockAppLink(initialLink: '');

await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
authOptions: FlutterAuthClientOptions(
localStorage: MockEmptyLocalStorage(),
),
);

expect(Supabase.instance.client, isNotNull);
});
});
}
127 changes: 127 additions & 0 deletions packages/supabase_flutter/test/lifecycle_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import 'package:flutter/widgets.dart';
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 = 'https://test.supabase.co';
const supabaseKey = 'test-anon-key';

group('App Lifecycle Management', () {
setUp(() {
SharedPreferences.setMockInitialValues({});
mockAppLink();
});

tearDown(() async {
try {
await Supabase.instance.dispose();
} catch (e) {
// Ignore dispose errors in tests
}
});

test('onResumed handles realtime reconnection when channels exist',
() async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
);

final supabase = Supabase.instance;

// Create a mock channel to simulate having active channels
final channel = supabase.client.realtime.channel('test-channel');

// Simulate app lifecycle state changes
supabase.didChangeAppLifecycleState(AppLifecycleState.paused);

// Verify realtime was disconnected
expect(supabase.client.realtime.connState, isNot(SocketStates.open));

// Simulate app resuming
supabase.didChangeAppLifecycleState(AppLifecycleState.resumed);

// The onResumed method should be called
expect(supabase.client.realtime, isNotNull);

// Clean up
await channel.unsubscribe();
});

test('didChangeAppLifecycleState handles different lifecycle states',
() async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
);

final supabase = Supabase.instance;

// Test paused state
expect(
() => supabase.didChangeAppLifecycleState(AppLifecycleState.paused),
returnsNormally);

// Test detached state
expect(
() => supabase.didChangeAppLifecycleState(AppLifecycleState.detached),
returnsNormally);

// Test resumed state
expect(
() => supabase.didChangeAppLifecycleState(AppLifecycleState.resumed),
returnsNormally);

// Test inactive state (should be handled by default case)
expect(
() => supabase.didChangeAppLifecycleState(AppLifecycleState.inactive),
returnsNormally);
});

test('onResumed handles disconnecting state properly', () async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
);

final supabase = Supabase.instance;

// Create a channel to ensure channels exist
final channel = supabase.client.realtime.channel('test-channel');

// Simulate disconnecting state by pausing first
supabase.didChangeAppLifecycleState(AppLifecycleState.paused);

// Now test resuming while in disconnecting state
await supabase.onResumed();

expect(supabase.client.realtime, isNotNull);

// Clean up
await channel.unsubscribe();
});

test('app lifecycle observer is properly added and removed', () async {
await Supabase.initialize(
url: supabaseUrl,
anonKey: supabaseKey,
);

final supabase = Supabase.instance;

// The observer should be added during initialization
expect(supabase, isNotNull);

// Dispose should remove the observer
await supabase.dispose();

// After disposal, the instance should be reset
expect(() => Supabase.instance, throwsA(isA<AssertionError>()));
});
});
}
Loading
Loading