// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import 'dart:async'; import 'package:amplify_auth_cognito_dart/amplify_auth_cognito_dart.dart' hide InternalErrorException; import 'package:amplify_auth_cognito_dart/src/credentials/cognito_keys.dart'; import 'package:amplify_auth_cognito_dart/src/flows/hosted_ui/hosted_ui_platform.dart'; import 'package:amplify_auth_cognito_dart/src/sdk/cognito_identity_provider.dart'; import 'package:amplify_auth_cognito_dart/src/state/cognito_state_machine.dart'; import 'package:amplify_auth_cognito_dart/src/state/state.dart'; import 'package:amplify_auth_cognito_test/common/mock_clients.dart'; import 'package:amplify_auth_cognito_test/common/mock_config.dart'; import 'package:amplify_auth_cognito_test/common/mock_hosted_ui.dart'; import 'package:amplify_auth_cognito_test/common/mock_secure_storage.dart'; import 'package:amplify_core/amplify_core.dart'; import 'package:amplify_secure_storage_dart/amplify_secure_storage_dart.dart'; import 'package:test/test.dart'; final throwsSignedOutException = throwsA(isA()); // Follows signOut test cases: // https://github.com/aws-amplify/amplify-android/tree/main/aws-auth-cognito/src/test/resources/feature-test/testsuites/signOut void main() { final userPoolKeys = CognitoUserPoolKeys(userPoolConfig); final identityPoolKeys = CognitoIdentityPoolKeys(identityPoolConfig); const hostedUiKeys = HostedUiKeys(hostedUiConfig); late AmplifyAuthCognitoDart plugin; late CognitoAuthStateMachine stateMachine; late MockSecureStorage secureStorage; late StreamController hubEventsController; late Stream hubEvents; final testAuthRepo = AmplifyAuthProviderRepository(); final emitsSignOutEvent = emitsThrough( isA().having( (event) => event.type, 'type', AuthHubEventType.signedOut, ), ); group('AmplifyAuthCognitoDart', () { setUp(() async { secureStorage = MockSecureStorage(); SecureStorageInterface storageFactory(scope) => secureStorage; stateMachine = CognitoAuthStateMachine() ..addBuilder( createHostedUiFactory( signIn: ( HostedUiPlatform platform, CognitoSignInWithWebUIPluginOptions options, AuthProvider? provider, ) async {}, signOut: ( HostedUiPlatform platform, CognitoSignInWithWebUIPluginOptions options, ) async {}, ), ); plugin = AmplifyAuthCognitoDart(secureStorageFactory: storageFactory) ..stateMachine = stateMachine; hubEventsController = StreamController(); hubEvents = hubEventsController.stream; Amplify.Hub.listen(HubChannel.Auth, hubEventsController.add); }); tearDown(() { hubEventsController.close(); Amplify.Hub.close(); }); group('signOut', () { test('completes when already signed out', () async { await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); expect(plugin.signOut(), completion(isA())); expect(hubEvents, emitsSignOutEvent); }); test('does not clear AWS creds when already signed out', () async { seedStorage(secureStorage, identityPoolKeys: identityPoolKeys); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); await expectLater( plugin.signOut(), completion(isA()), ); expect(hubEvents, emitsSignOutEvent); final credentials = await stateMachine.loadCredentials(); expect( credentials, isA() .having((result) => result.userPoolTokens, 'tokens', isNull) .having( (result) => result.awsCredentials, 'awsCreds', isNotNull, ), ); }); test('clears credential store when signed in & HTTP succeeds', () async { seedStorage( secureStorage, userPoolKeys: userPoolKeys, identityPoolKeys: identityPoolKeys, ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: () async => GlobalSignOutResponse(), revokeToken: () async => RevokeTokenResponse(), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut(), completion(isA()), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test('clears credential store when signed in & global sign out fails', () async { seedStorage( secureStorage, userPoolKeys: userPoolKeys, identityPoolKeys: identityPoolKeys, ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: expectAsync0(() async => throw InternalErrorException()), revokeToken: () async => RevokeTokenResponse(), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut( options: const SignOutOptions(globalSignOut: true), ), completion( isA() .having( (res) => res.signedOutLocally, 'signedOutLocally', isTrue, ) .having( (res) => res.globalSignOutException, 'globalSignOutException', isA(), ) .having( (res) => res.revokeTokenException, 'revokeTokenException', isA().having( (e) => e.underlyingException.toString(), 'underlyingException', contains('not attempted'), ), ), ), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test('clears credential store when signed in & revoke token fails', () async { seedStorage( secureStorage, userPoolKeys: userPoolKeys, identityPoolKeys: identityPoolKeys, ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: () async => GlobalSignOutResponse(), revokeToken: expectAsync0(() async => throw InternalErrorException()), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut( options: const SignOutOptions(globalSignOut: true), ), completion( isA() .having( (res) => res.signedOutLocally, 'signedOutLocally', isTrue, ) .having( (res) => res.globalSignOutException, 'globalSignOutException', isNull, ) .having( (res) => res.revokeTokenException, 'revokeTokenException', isA().having( (e) => e.refreshToken, 'refreshToken', refreshToken, ), ), ), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test('can sign out in user pool-only mode', () async { seedStorage(secureStorage, userPoolKeys: userPoolKeys); await plugin.configure( config: userPoolOnlyConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: () async => GlobalSignOutResponse(), revokeToken: () async => RevokeTokenResponse(), ); stateMachine.addInstance(mockIdp); expect(plugin.signOut(), completion(isA())); }); group('hosted UI', () { test('clears credential store when signed in & HTTP succeeds', () async { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, hostedUiKeys: hostedUiKeys, ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: () async => GlobalSignOutResponse(), revokeToken: () async => RevokeTokenResponse(), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut(), completion(isA()), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test('clears credential store when signed in & global sign out fails', () async { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, hostedUiKeys: hostedUiKeys, ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: expectAsync0(() async => throw InternalErrorException()), revokeToken: () async => RevokeTokenResponse(), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut( options: const SignOutOptions(globalSignOut: true), ), completion( isA().having( (res) => res.globalSignOutException, 'globalSignOutException', isA(), ), ), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test('clears credential store when signed in & revoke token fails', () async { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, hostedUiKeys: hostedUiKeys, ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: () async => GlobalSignOutResponse(), revokeToken: () async => throw InternalErrorException(), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut( options: const SignOutOptions(globalSignOut: true), ), completion( isA().having( (res) => res.revokeTokenException, 'revokeTokenException', isA(), ), ), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test('fails with platform exception', () async { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, hostedUiKeys: hostedUiKeys, ); stateMachine.addBuilder( createHostedUiFactory( signIn: ( HostedUiPlatform platform, CognitoSignInWithWebUIPluginOptions options, AuthProvider? provider, ) async {}, signOut: ( HostedUiPlatform platform, CognitoSignInWithWebUIPluginOptions options, ) async => throw _HostedUiException(), ), ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); final mockIdp = MockCognitoIdentityProviderClient( globalSignOut: () async => GlobalSignOutResponse(), revokeToken: () async => RevokeTokenResponse(), ); stateMachine.addInstance(mockIdp); await expectLater(plugin.stateMachine.getUserPoolTokens(), completes); await expectLater( plugin.signOut(), completion( isA().having( (res) => res.hostedUiException, 'hostedUiException', isA().having( (e) => e.underlyingException, 'underlyingException', isA<_HostedUiException>(), ), ), ), ); expect( plugin.stateMachine.getUserPoolTokens(), throwsSignedOutException, ); expect(hubEvents, emitsSignOutEvent); }); test( 'fails hard for user cancellation', () async { seedStorage( secureStorage, identityPoolKeys: identityPoolKeys, hostedUiKeys: hostedUiKeys, ); stateMachine.addBuilder( createHostedUiFactory( signIn: ( HostedUiPlatform platform, CognitoSignInWithWebUIPluginOptions options, AuthProvider? provider, ) async {}, signOut: ( HostedUiPlatform platform, CognitoSignInWithWebUIPluginOptions options, ) async => throw const UserCancelledException(''), ), ); await plugin.configure( config: mockConfig, authProviderRepo: testAuthRepo, ); await expectLater( plugin.stateMachine.getUserPoolTokens(), completes, ); await expectLater( plugin.signOut(), completion( isA().having( (res) => res.exception, 'exception', isA(), ), ), ); expect( plugin.stateMachine.getUserPoolTokens(), completes, reason: 'Credentials were not cleared', ); unawaited(hubEventsController.close()); expect(hubEvents, neverEmits(emitsSignOutEvent)); }, skip: zIsWeb ? 'User cancellation is not possible on Web' : null, ); }); }); }); } class _HostedUiException implements Exception {}