Skip to content

Commit fb58bcd

Browse files
authored
chore(supabase_flutter): Improve supabase_flutter coverage (#1181)
* chore: improve supabase_flutter coverage * preserve trailing commas * format documents * fix storage test issue * fix: ensure test isolation in storage tests - Use unique keys for each test to prevent SharedPreferences state leakage - Fix flaky tests that were failing due to shared state between test runs - Tests now pass consistently in CI/CD environments * run formatter
1 parent ea69a08 commit fb58bcd

File tree

5 files changed

+417
-0
lines changed

5 files changed

+417
-0
lines changed

packages/supabase_flutter/analysis_options.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ include: package:flutter_lints/flutter.yaml
33
linter:
44
rules:
55
avoid_print: false
6+
trailing_commas: preserve
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:supabase_flutter/supabase_flutter.dart';
3+
4+
import 'widget_test_stubs.dart';
5+
6+
class _MockLocalStorage extends MockLocalStorage {
7+
bool _initializeCalled = false;
8+
9+
bool get initializeCalled => _initializeCalled;
10+
11+
@override
12+
Future<void> initialize() async {
13+
_initializeCalled = true;
14+
return super.initialize();
15+
}
16+
}
17+
18+
void main() {
19+
TestWidgetsFlutterBinding.ensureInitialized();
20+
21+
const supabaseUrl = '';
22+
const supabaseKey = '';
23+
24+
group('Authentication', () {
25+
setUp(() async {
26+
try {
27+
await Supabase.instance.dispose();
28+
} catch (e) {
29+
// Ignore dispose errors
30+
}
31+
32+
mockAppLink();
33+
});
34+
35+
tearDown(() async {
36+
try {
37+
await Supabase.instance.dispose();
38+
} catch (e) {
39+
// Ignore dispose errors
40+
}
41+
});
42+
43+
group('Session management', () {
44+
test('initializes local storage on initialize', () async {
45+
final mockStorage = _MockLocalStorage();
46+
47+
await Supabase.initialize(
48+
url: supabaseUrl,
49+
anonKey: supabaseKey,
50+
debug: false,
51+
authOptions: FlutterAuthClientOptions(
52+
localStorage: mockStorage,
53+
pkceAsyncStorage: MockAsyncStorage(),
54+
),
55+
);
56+
57+
// Give time for initialization to complete
58+
await Future.delayed(const Duration(milliseconds: 100));
59+
60+
expect(mockStorage.initializeCalled, isTrue);
61+
});
62+
});
63+
64+
group('Session recovery', () {
65+
test('handles corrupted session data gracefully', () async {
66+
final corruptedStorage = MockExpiredStorage();
67+
68+
await Supabase.initialize(
69+
url: supabaseUrl,
70+
anonKey: supabaseKey,
71+
debug: false,
72+
authOptions: FlutterAuthClientOptions(
73+
localStorage: corruptedStorage,
74+
pkceAsyncStorage: MockAsyncStorage(),
75+
),
76+
);
77+
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);
81+
});
82+
83+
test('handles null session during initialization', () async {
84+
final emptyStorage = MockEmptyLocalStorage();
85+
86+
await Supabase.initialize(
87+
url: supabaseUrl,
88+
anonKey: supabaseKey,
89+
debug: false,
90+
authOptions: FlutterAuthClientOptions(
91+
localStorage: emptyStorage,
92+
pkceAsyncStorage: MockAsyncStorage(),
93+
),
94+
);
95+
96+
// Should handle empty storage gracefully
97+
expect(Supabase.instance.client.auth.currentSession, isNull);
98+
});
99+
});
100+
});
101+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import 'package:flutter_test/flutter_test.dart';
2+
import 'package:shared_preferences/shared_preferences.dart';
3+
import 'package:supabase_flutter/supabase_flutter.dart';
4+
5+
import 'widget_test_stubs.dart';
6+
7+
void main() {
8+
TestWidgetsFlutterBinding.ensureInitialized();
9+
10+
const supabaseUrl = '';
11+
const supabaseKey = '';
12+
13+
group('Supabase initialization', () {
14+
setUp(() {
15+
SharedPreferences.setMockInitialValues({});
16+
mockAppLink();
17+
});
18+
19+
tearDown(() async {
20+
try {
21+
await Supabase.instance.dispose();
22+
} catch (e) {
23+
// Ignore dispose errors
24+
}
25+
});
26+
27+
group('Basic initialization', () {
28+
test('initialize successfully with default options', () async {
29+
await Supabase.initialize(
30+
url: supabaseUrl,
31+
anonKey: supabaseKey,
32+
);
33+
34+
expect(Supabase.instance, isNotNull);
35+
expect(Supabase.instance.client, isNotNull);
36+
});
37+
});
38+
39+
group('Custom storage initialization', () {
40+
test('initialize successfully with custom localStorage', () async {
41+
final localStorage = MockLocalStorage();
42+
await Supabase.initialize(
43+
url: supabaseUrl,
44+
anonKey: supabaseKey,
45+
authOptions: FlutterAuthClientOptions(
46+
localStorage: localStorage,
47+
),
48+
);
49+
50+
expect(Supabase.instance, isNotNull);
51+
expect(Supabase.instance.client, isNotNull);
52+
});
53+
54+
test('handles initialization with expired session in storage', () async {
55+
await Supabase.initialize(
56+
url: supabaseUrl,
57+
anonKey: supabaseKey,
58+
debug: true,
59+
authOptions: FlutterAuthClientOptions(
60+
localStorage: MockExpiredStorage(),
61+
pkceAsyncStorage: MockAsyncStorage(),
62+
),
63+
);
64+
65+
// Should handle expired session gracefully
66+
expect(Supabase.instance.client.auth.currentSession, isNotNull);
67+
});
68+
});
69+
70+
group('Auth options initialization', () {
71+
test('initialize successfully with PKCE auth flow', () async {
72+
await Supabase.initialize(
73+
url: supabaseUrl,
74+
anonKey: supabaseKey,
75+
authOptions: const FlutterAuthClientOptions(
76+
authFlowType: AuthFlowType.pkce,
77+
),
78+
);
79+
80+
expect(Supabase.instance, isNotNull);
81+
expect(Supabase.instance.client, isNotNull);
82+
});
83+
});
84+
85+
group('Custom client initialization', () {
86+
test('initialize successfully with custom HTTP client', () async {
87+
final httpClient = PkceHttpClient();
88+
await Supabase.initialize(
89+
url: supabaseUrl,
90+
anonKey: supabaseKey,
91+
httpClient: httpClient,
92+
);
93+
94+
expect(Supabase.instance, isNotNull);
95+
expect(Supabase.instance.client, isNotNull);
96+
});
97+
98+
test('initialize successfully with custom access token', () async {
99+
await Supabase.initialize(
100+
url: supabaseUrl,
101+
anonKey: supabaseKey,
102+
accessToken: () async => 'custom-access-token',
103+
);
104+
105+
expect(Supabase.instance, isNotNull);
106+
expect(Supabase.instance.client, isNotNull);
107+
108+
// Should throw AuthException when trying to access auth
109+
expect(
110+
() => Supabase.instance.client.auth,
111+
throwsA(isA<AuthException>()),
112+
);
113+
});
114+
});
115+
116+
group('Multiple initialization and disposal', () {
117+
test('dispose and reinitialize works', () async {
118+
// First initialization
119+
await Supabase.initialize(
120+
url: supabaseUrl,
121+
anonKey: supabaseKey,
122+
);
123+
124+
expect(Supabase.instance, isNotNull);
125+
126+
// Dispose
127+
await Supabase.instance.dispose();
128+
129+
// Need to run the event loop to let the dispose complete
130+
await Future.delayed(Duration.zero);
131+
132+
// Re-initialize should work without errors
133+
await Supabase.initialize(
134+
url: supabaseUrl,
135+
anonKey: supabaseKey,
136+
);
137+
138+
expect(Supabase.instance, isNotNull);
139+
});
140+
141+
test('handles multiple initializations correctly', () async {
142+
await Supabase.initialize(
143+
url: supabaseUrl,
144+
anonKey: supabaseKey,
145+
debug: false,
146+
authOptions: FlutterAuthClientOptions(
147+
localStorage: MockLocalStorage(),
148+
pkceAsyncStorage: MockAsyncStorage(),
149+
),
150+
);
151+
152+
// Store first instance to verify it's different after re-initialization
153+
final firstInstance = Supabase.instance.client;
154+
155+
// Dispose first instance before re-initializing
156+
await Supabase.instance.dispose();
157+
158+
await Supabase.initialize(
159+
url: supabaseUrl,
160+
anonKey: supabaseKey,
161+
debug: true,
162+
authOptions: FlutterAuthClientOptions(
163+
localStorage: MockEmptyLocalStorage(),
164+
pkceAsyncStorage: MockAsyncStorage(),
165+
),
166+
);
167+
168+
final secondInstance = Supabase.instance.client;
169+
expect(secondInstance, isNotNull);
170+
expect(identical(firstInstance, secondInstance), isFalse);
171+
});
172+
});
173+
});
174+
}

0 commit comments

Comments
 (0)