Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
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
198 changes: 136 additions & 62 deletions packages/gotrue/lib/src/gotrue_client.dart

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions packages/gotrue/lib/src/types/gotrue_async_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
abstract class GotrueAsyncStorage {
const GotrueAsyncStorage();

/// May be implemented to allow for initialization of the storage before use.
Future<void> initialize() async {}

/// Retrieves an item asynchronously from the storage with the key.
Future<String?> getItem({required String key});
Future<String?> getItem(String key);

/// Stores the value asynchronously to the storage with the key.
Future<void> setItem({
required String key,
required String value,
});
Future<void> setItem(String key, String value);

/// Removes an item asynchronously from the storage for the given key.
Future<void> removeItem({required String key});
Future<void> removeItem(String key);
}
2 changes: 2 additions & 0 deletions packages/gotrue/lib/src/types/types.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
/// An interface to use [html.BroadcastChannel] on web to broadcast sessions to
/// other tabs.
typedef BroadcastChannel = ({
Stream<Map<String, dynamic>> onMessage,
void Function(Map) postMessage,
Expand Down
222 changes: 222 additions & 0 deletions packages/gotrue/test/initialize_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import 'dart:async';

import 'package:gotrue/gotrue.dart';
import 'package:gotrue/src/constants.dart';
import 'package:test/test.dart';

import 'utils.dart';

void main() {
group('GoTrueClient._initialize()', () {
const defaultGotrueUrl = 'http://localhost:9998';
const defaultHeaders = {
'Authorization': 'Bearer test_token',
'apikey': 'test_token',
};

group('with persistSession = false', () {
test('does not attempt to load session from storage', () async {
final storage = TestAsyncStorage();
final sessionData = getSessionData(
DateTime.now().add(const Duration(hours: 1)),
);
// Store with default key
await storage.setItem(
Constants.defaultStorageKey,
sessionData.sessionString,
);

final client = GoTrueClient(
url: defaultGotrueUrl,
headers: defaultHeaders,
asyncStorage: storage,
persistSession: false,
);

// Wait for initialization to complete
await Future.delayed(const Duration(milliseconds: 300));
expect(client.currentSession, isNull);
});

test('does not require asyncStorage', () async {
// Should not throw even without asyncStorage
final client = GoTrueClient(
url: defaultGotrueUrl,
headers: defaultHeaders,
persistSession: false,
);

expect(client.currentSession, isNull);
});
});

group('with persistSession = true', () {
test('load session and emit initial session event', () async {
final authStateChanges = <AuthState>[];
final storage = TestAsyncStorage();
final sessionData = getSessionData(
DateTime.now().add(const Duration(hours: 1)),
);
await storage.setItem(
Constants.defaultStorageKey,
sessionData.sessionString,
);

final client = GoTrueClient(
url: defaultGotrueUrl,
headers: defaultHeaders,
asyncStorage: storage,
persistSession: true,
);

expect(client.currentSession, isNull);

final subscription = client.onAuthStateChange.listen(
(state) => authStateChanges.add(state),
);

// Wait for initialization
await Future.delayed(const Duration(milliseconds: 300));

expect(client.currentSession, isNotNull);

subscription.cancel();

expect(
authStateChanges.length,
greaterThanOrEqualTo(1),
);
expect(
authStateChanges.first.event,
AuthChangeEvent.initialSession,
);
});

test('emits initialSession event when no session is stored', () async {
final authStateChanges = <AuthState>[];
final storage = TestAsyncStorage();

final client = GoTrueClient(
url: defaultGotrueUrl,
headers: defaultHeaders,
asyncStorage: storage,
persistSession: true,
);

final subscription = client.onAuthStateChange.listen(
(state) => authStateChanges.add(state),
);

// Wait for initialization
await Future.delayed(const Duration(milliseconds: 300));

subscription.cancel();

expect(authStateChanges.length, greaterThanOrEqualTo(1));
expect(
authStateChanges.first.event,
AuthChangeEvent.initialSession,
);
expect(authStateChanges.first.session, isNull);
});

test('handles corrupted session data gracefully', () async {
final authExceptions = <Object>[];
final authEvents = <AuthState>[];
final storage = TestAsyncStorage();
await storage.setItem(
Constants.defaultStorageKey, '{"invalid": "json"}');

final client = GoTrueClient(
url: defaultGotrueUrl,
headers: defaultHeaders,
asyncStorage: storage,
persistSession: true,
);

final subscription = client.onAuthStateChange.listen(
(state) => authEvents.add(state),
onError: (error) => authExceptions.add(error as Object),
);

// Wait for initialization
await Future.delayed(const Duration(milliseconds: 300));

subscription.cancel();

expect(client.currentSession, isNull);
expect(authExceptions.length, equals(1));
expect(
authExceptions.first,
isA<AuthException>(),
);

expect(authEvents.length, equals(1));
expect(
authEvents.first.event,
AuthChangeEvent.signedOut,
);
});

test('calls recoverSession after setting initial session', () async {
final storage = TestAsyncStorage();
final sessionData = getSessionData(
DateTime.now().subtract(const Duration(hours: 2)),
);
await storage.setItem(
Constants.defaultStorageKey,
sessionData.sessionString,
);

final client = GoTrueClient(
url: defaultGotrueUrl,
headers: defaultHeaders,
asyncStorage: storage,
persistSession: true,
);
final authEvents = <AuthState>[];
final authExceptions = <Object>[];

client.onAuthStateChange.listen(
(state) => authEvents.add(state),
onError: (error) => authExceptions.add(error as Object),
);

// Wait for initialization
await Future.delayed(const Duration(milliseconds: 300));

expect(
authEvents.length,
equals(2),
);

// We first get the initial and expired session loaded from storage.
expect(
authEvents.first.event,
AuthChangeEvent.initialSession,
);
expect(authEvents.first.session, isNotNull);

expect(client.currentSession, isNull);
expect(authExceptions.length, equals(1));
expect(
authExceptions.first,
isA<AuthApiException>(),
);

// The client tried to recover the session, but the stub session has
// no valid refresh token, so we expect a failure with that message
expect(authExceptions.first.toString(),
contains('Refresh token is not valid'));

// The invalid refresh token causes a sign out.
expect(
authEvents[1].event,
AuthChangeEvent.signedOut,
);
expect(authEvents[1].session, isNull);
expect(client.currentSession, isNull);
});
});
});
}
6 changes: 3 additions & 3 deletions packages/gotrue/test/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,17 @@ String getServiceRoleToken(DotEnv env) {
class TestAsyncStorage extends GotrueAsyncStorage {
final Map<String, String> _map = {};
@override
Future<String?> getItem({required String key}) async {
Future<String?> getItem(String key) async {
return _map[key];
}

@override
Future<void> removeItem({required String key}) async {
Future<void> removeItem(String key) async {
_map.remove(key);
}

@override
Future<void> setItem({required String key, required String value}) async {
Future<void> setItem(String key, String value) async {
_map[key] = value;
}
}
34 changes: 13 additions & 21 deletions packages/supabase/lib/src/supabase_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,21 @@ import 'auth_http_client.dart';
import 'counter.dart';

/// {@template supabase_client}
///
/// Creates a Supabase client to interact with your Supabase instance.
///
/// [supabaseUrl] and [supabaseKey] can be found on your Supabase dashboard.
/// Pass the `publishable` (anon) key for client-side usage or the `secret`
/// key for trusted server-side environments.
///
/// You can access none public schema by passing different [schema].
///
/// Default headers can be overridden by specifying [headers].
///
/// Custom http client can be used by passing [httpClient] parameter.
///
/// [storageRetryAttempts] specifies how many retry attempts there should be to
/// upload a file to Supabase storage when failed due to network interruption.
///
/// [realtimeClientOptions] specifies different options you can pass to `RealtimeClient`.
/// [realtimeClientOptions], [authOptions], [storageOptions],
/// [postgrestOptions] specify different options you can pass to
/// [RealtimeClient], [GoTrueClient], [SupabaseStorageClient],
/// [PostgrestClient].
///
/// [accessToken] Optional function for using a third-party authentication system with Supabase.
/// The function should return an access token or ID token (JWT) by obtaining
Expand All @@ -38,8 +37,6 @@ import 'counter.dart';
/// Pass an instance of `YAJsonIsolate` to [isolate] to use your own persisted
/// isolate instance. A new instance will be created if [isolate] is omitted.
///
/// Pass an instance of [gotrueAsyncStorage] and set the [authFlowType] to
/// `AuthFlowType.pkce`in order to perform auth actions with pkce flow.
/// {@endtemplate}
class SupabaseClient {
final String _supabaseKey;
Expand Down Expand Up @@ -144,11 +141,7 @@ class SupabaseClient {
_httpClient = httpClient,
_isolate = isolate ?? (YAJsonIsolate()..initialize()),
_hasCustomIsolate = isolate != null {
_authInstance = _initSupabaseAuthClient(
autoRefreshToken: authOptions.autoRefreshToken,
gotrueAsyncStorage: authOptions.pkceAsyncStorage,
authFlowType: authOptions.authFlowType,
);
_authInstance = _initSupabaseAuthClient(authOptions: authOptions);
_authHttpClient =
AuthHttpClient(_supabaseKey, httpClient ?? Client(), _getAccessToken);
rest = _initRestClient();
Expand Down Expand Up @@ -284,22 +277,21 @@ class SupabaseClient {
_authInstance?.dispose();
}

GoTrueClient _initSupabaseAuthClient({
bool? autoRefreshToken,
required GotrueAsyncStorage? gotrueAsyncStorage,
required AuthFlowType authFlowType,
}) {
GoTrueClient _initSupabaseAuthClient(
{required AuthClientOptions authOptions}) {
final authHeaders = {...headers};
authHeaders['apikey'] = _supabaseKey;
authHeaders['Authorization'] = 'Bearer $_supabaseKey';

return GoTrueClient(
url: _authUrl,
headers: authHeaders,
autoRefreshToken: autoRefreshToken,
autoRefreshToken: authOptions.autoRefreshToken,
httpClient: _httpClient,
asyncStorage: gotrueAsyncStorage,
flowType: authFlowType,
asyncStorage: authOptions.asyncStorage,
storageKey: authOptions.storageKey,
persistSession: authOptions.persistSession,
flowType: authOptions.authFlowType,
);
}

Expand Down
Loading
Loading