Skip to content

Commit ea271e5

Browse files
committed
chore: More test coverage on supabase_flutter
1 parent fb58bcd commit ea271e5

File tree

8 files changed

+633
-211
lines changed

8 files changed

+633
-211
lines changed

packages/supabase_flutter/test/auth_test.dart

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,39 +62,61 @@ void main() {
6262
});
6363

6464
group('Session recovery', () {
65-
test('handles corrupted session data gracefully', () async {
66-
final corruptedStorage = MockExpiredStorage();
67-
65+
test('handles expired session with auto-refresh disabled', () async {
6866
await Supabase.initialize(
6967
url: supabaseUrl,
7068
anonKey: supabaseKey,
7169
debug: false,
7270
authOptions: FlutterAuthClientOptions(
73-
localStorage: corruptedStorage,
71+
localStorage: MockExpiredStorage(),
7472
pkceAsyncStorage: MockAsyncStorage(),
73+
autoRefreshToken: false,
7574
),
7675
);
7776

78-
// MockExpiredStorage returns an expired session, not null
79-
expect(Supabase.instance.client.auth.currentSession, isNotNull);
80-
expect(Supabase.instance.client.auth.currentSession?.isExpired, isTrue);
77+
// Give it a delay to wait for recoverSession to throw
78+
await Future.delayed(const Duration(milliseconds: 100));
79+
80+
await expectLater(Supabase.instance.client.auth.onAuthStateChange,
81+
emitsError(isA<AuthException>()));
8182
});
8283

8384
test('handles null session during initialization', () async {
84-
final emptyStorage = MockEmptyLocalStorage();
85-
8685
await Supabase.initialize(
8786
url: supabaseUrl,
8887
anonKey: supabaseKey,
8988
debug: false,
9089
authOptions: FlutterAuthClientOptions(
91-
localStorage: emptyStorage,
90+
localStorage: MockEmptyLocalStorage(),
9291
pkceAsyncStorage: MockAsyncStorage(),
9392
),
9493
);
9594

9695
// Should handle empty storage gracefully
9796
expect(Supabase.instance.client.auth.currentSession, isNull);
97+
98+
// Verify initial session event
99+
final event =
100+
await Supabase.instance.client.auth.onAuthStateChange.first;
101+
expect(event.event, AuthChangeEvent.initialSession);
102+
expect(event.session, isNull);
103+
});
104+
105+
test('handles expired session with auto-refresh enabled', () async {
106+
await Supabase.initialize(
107+
url: supabaseUrl,
108+
anonKey: supabaseKey,
109+
debug: false,
110+
authOptions: FlutterAuthClientOptions(
111+
localStorage: MockExpiredStorage(),
112+
pkceAsyncStorage: MockAsyncStorage(),
113+
autoRefreshToken: true,
114+
),
115+
);
116+
117+
// With auto-refresh enabled, expired session should be handled
118+
expect(Supabase.instance.client.auth.currentSession, isNotNull);
119+
expect(Supabase.instance.client.auth.currentSession?.isExpired, isTrue);
98120
});
99121
});
100122
});

packages/supabase_flutter/test/deep_link_test.dart

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,93 @@ void main() {
6868
expect(pkceHttpClient.requestCount, 1);
6969
expect(pkceHttpClient.lastRequestBody['auth_code'], 'my-code-verifier');
7070
});
71+
72+
tearDown(() async {
73+
try {
74+
await Supabase.instance.dispose();
75+
} catch (e) {
76+
// Ignore dispose errors in tests
77+
}
78+
});
79+
});
80+
81+
group('Deep Link Error Handling', () {
82+
setUp(() {
83+
mockAppLink();
84+
});
85+
86+
tearDown(() async {
87+
try {
88+
await Supabase.instance.dispose();
89+
} catch (e) {
90+
// Ignore dispose errors in tests
91+
}
92+
});
93+
94+
test('handles malformed deep link URL gracefully', () async {
95+
// This test simulates error handling in deep link processing
96+
await Supabase.initialize(
97+
url: supabaseUrl,
98+
anonKey: supabaseKey,
99+
authOptions: FlutterAuthClientOptions(
100+
localStorage: MockEmptyLocalStorage(),
101+
),
102+
);
103+
104+
// The initialization should complete successfully even if there are
105+
// potential deep link errors to handle
106+
expect(Supabase.instance.client, isNotNull);
107+
});
108+
109+
test('handles non-auth deep links correctly', () async {
110+
// Mock a deep link that is not auth-related
111+
mockAppLink(
112+
initialLink: 'com.supabase://other-action/?param=value',
113+
);
114+
115+
await Supabase.initialize(
116+
url: supabaseUrl,
117+
anonKey: supabaseKey,
118+
authOptions: FlutterAuthClientOptions(
119+
localStorage: MockEmptyLocalStorage(),
120+
),
121+
);
122+
123+
// Should initialize normally without attempting auth
124+
expect(Supabase.instance.client, isNotNull);
125+
});
126+
127+
test('handles auth deep link without proper parameters', () async {
128+
// Mock a deep link that looks like auth but missing required params
129+
mockAppLink(
130+
initialLink: 'com.supabase://callback/?error=access_denied',
131+
);
132+
133+
await Supabase.initialize(
134+
url: supabaseUrl,
135+
anonKey: supabaseKey,
136+
authOptions: FlutterAuthClientOptions(
137+
localStorage: MockEmptyLocalStorage(),
138+
),
139+
);
140+
141+
// Should initialize normally and handle the error case
142+
expect(Supabase.instance.client, isNotNull);
143+
});
144+
145+
test('handles empty deep link', () async {
146+
// Mock empty initial link
147+
mockAppLink(initialLink: '');
148+
149+
await Supabase.initialize(
150+
url: supabaseUrl,
151+
anonKey: supabaseKey,
152+
authOptions: FlutterAuthClientOptions(
153+
localStorage: MockEmptyLocalStorage(),
154+
),
155+
);
156+
157+
expect(Supabase.instance.client, isNotNull);
158+
});
71159
});
72160
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import 'package:flutter/widgets.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:shared_preferences/shared_preferences.dart';
4+
import 'package:supabase_flutter/supabase_flutter.dart';
5+
6+
import 'widget_test_stubs.dart';
7+
8+
void main() {
9+
TestWidgetsFlutterBinding.ensureInitialized();
10+
11+
const supabaseUrl = 'https://test.supabase.co';
12+
const supabaseKey = 'test-anon-key';
13+
14+
group('App Lifecycle Management', () {
15+
setUp(() {
16+
SharedPreferences.setMockInitialValues({});
17+
mockAppLink();
18+
});
19+
20+
tearDown(() async {
21+
try {
22+
await Supabase.instance.dispose();
23+
} catch (e) {
24+
// Ignore dispose errors in tests
25+
}
26+
});
27+
28+
test('onResumed handles realtime reconnection when channels exist', () async {
29+
await Supabase.initialize(
30+
url: supabaseUrl,
31+
anonKey: supabaseKey,
32+
);
33+
34+
final supabase = Supabase.instance;
35+
36+
// Create a mock channel to simulate having active channels
37+
final channel = supabase.client.realtime.channel('test-channel');
38+
39+
// Simulate app lifecycle state changes
40+
supabase.didChangeAppLifecycleState(AppLifecycleState.paused);
41+
42+
// Verify realtime was disconnected
43+
expect(supabase.client.realtime.connState, isNot(SocketStates.open));
44+
45+
// Simulate app resuming
46+
supabase.didChangeAppLifecycleState(AppLifecycleState.resumed);
47+
48+
// The onResumed method should be called
49+
expect(supabase.client.realtime, isNotNull);
50+
51+
// Clean up
52+
await channel.unsubscribe();
53+
});
54+
55+
test('didChangeAppLifecycleState handles different lifecycle states', () async {
56+
await Supabase.initialize(
57+
url: supabaseUrl,
58+
anonKey: supabaseKey,
59+
);
60+
61+
final supabase = Supabase.instance;
62+
63+
// Test paused state
64+
expect(() => supabase.didChangeAppLifecycleState(AppLifecycleState.paused),
65+
returnsNormally);
66+
67+
// Test detached state
68+
expect(() => supabase.didChangeAppLifecycleState(AppLifecycleState.detached),
69+
returnsNormally);
70+
71+
// Test resumed state
72+
expect(() => supabase.didChangeAppLifecycleState(AppLifecycleState.resumed),
73+
returnsNormally);
74+
75+
// Test inactive state (should be handled by default case)
76+
expect(() => supabase.didChangeAppLifecycleState(AppLifecycleState.inactive),
77+
returnsNormally);
78+
});
79+
80+
test('onResumed handles disconnecting state properly', () async {
81+
await Supabase.initialize(
82+
url: supabaseUrl,
83+
anonKey: supabaseKey,
84+
);
85+
86+
final supabase = Supabase.instance;
87+
88+
// Create a channel to ensure channels exist
89+
final channel = supabase.client.realtime.channel('test-channel');
90+
91+
// Simulate disconnecting state by pausing first
92+
supabase.didChangeAppLifecycleState(AppLifecycleState.paused);
93+
94+
// Now test resuming while in disconnecting state
95+
await supabase.onResumed();
96+
97+
expect(supabase.client.realtime, isNotNull);
98+
99+
// Clean up
100+
await channel.unsubscribe();
101+
});
102+
103+
test('app lifecycle observer is properly added and removed', () async {
104+
await Supabase.initialize(
105+
url: supabaseUrl,
106+
anonKey: supabaseKey,
107+
);
108+
109+
final supabase = Supabase.instance;
110+
111+
// The observer should be added during initialization
112+
expect(supabase, isNotNull);
113+
114+
// Dispose should remove the observer
115+
await supabase.dispose();
116+
117+
// After disposal, the instance should be reset
118+
expect(() => Supabase.instance, throwsA(isA<AssertionError>()));
119+
});
120+
});
121+
}

0 commit comments

Comments
 (0)