diff --git a/melos.yaml b/melos.yaml index d524a300fc4f..ead9e18a68a4 100644 --- a/melos.yaml +++ b/melos.yaml @@ -199,6 +199,17 @@ scripts: run: git clean -x -d -f -q description: Clean things very deeply, can be used to establish "pristine checkout" status. + test:e2e:windows: + run: | + melos exec -c 1 --fail-fast -- \ + "flutter test integration_test/e2e_test.dart -d windows" + description: | + Run all e2e tests. + packageFilters: + dirExists: + - integration_test + scope: '*tests*' + qualitycheck: run: | melos run clean:deep && \ diff --git a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp index 230dd6e082ac..17983bb25166 100644 --- a/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp +++ b/packages/firebase_auth/firebase_auth/windows/firebase_auth_plugin.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -605,33 +606,47 @@ firebase::auth::Credential getCredentialFromArguments( return firebase::auth::Credential(); } - std::string idToken = std::get(arguments[kArgumentIdToken]); - std::string accessToken = - std::get(arguments[kArgumentAccessToken]); + // Lambda function to extract an optional string from the arguments map. This + // allows us to pass nullptr if no value exists + auto getStringOpt = + [&](const std::string& key) -> std::optional { + auto it = arguments.find(key); + if (it != arguments.end() && + std::holds_alternative(it->second)) { + return std::get(it->second); + } + return std::nullopt; + }; + + std::optional idToken = getStringOpt(kArgumentIdToken); + std::optional accessToken = getStringOpt(kArgumentAccessToken); // Facebook Auth if (signInMethod == kSignInMethodFacebook) { return firebase::auth::FacebookAuthProvider::GetCredential( - accessToken.c_str()); + accessToken.value().c_str()); } // Google Auth if (signInMethod == kSignInMethodGoogle) { + // Both accessToken and idToken arguments can be null. You can use one or + // the other return firebase::auth::GoogleAuthProvider::GetCredential( - idToken.c_str(), accessToken.c_str()); + idToken ? idToken.value().c_str() : nullptr, + accessToken ? accessToken.value().c_str() : nullptr); } // Twitter Auth if (signInMethod == kSignInMethodTwitter) { std::string secret = std::get(arguments[kArgumentSecret]); - return firebase::auth::TwitterAuthProvider::GetCredential(idToken.c_str(), - secret.c_str()); + return firebase::auth::TwitterAuthProvider::GetCredential( + idToken.value().c_str(), secret.c_str()); } // GitHub Auth if (signInMethod == kSignInMethodGithub) { return firebase::auth::GitHubAuthProvider::GetCredential( - accessToken.c_str()); + accessToken.value().c_str()); } // OAuth @@ -639,7 +654,8 @@ firebase::auth::Credential getCredentialFromArguments( std::string providerId = std::get(arguments[kArgumentProviderId]); return firebase::auth::OAuthProvider::GetCredential( - providerId.c_str(), idToken.c_str(), accessToken.c_str()); + providerId.c_str(), idToken.value().c_str(), + accessToken.value().c_str()); } // If no known auth method matched diff --git a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart index b59cf249c15c..294322bcad88 100644 --- a/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart +++ b/tests/integration_test/firebase_auth/firebase_auth_instance_e2e_test.dart @@ -121,76 +121,81 @@ void main() { }); }); - group('userChanges()', () { - late StreamSubscription subscription; - tearDown(() async { - await subscription.cancel(); - }); - - test('fires once on first initialization of FirebaseAuth', () async { - // Fixes a very specific bug: https://github.com/firebase/flutterfire/issues/3628 - // If the first initialization of FirebaseAuth involves the listeners userChanges() or idTokenChanges() - // the user will receive two events. Why? The native SDK listener will always fire an event upon initial - // listen. FirebaseAuth also sends an initial synthetic event. We send a synthetic event because, ordinarily, the user will - // not use a listener as the first occurrence of FirebaseAuth. We, therefore, mimic native behavior by sending an - // event. This test proves the logic of PR: https://github.com/firebase/flutterfire/pull/6560 - - // Requires a fresh app. - FirebaseApp second = await Firebase.initializeApp( - name: 'test-init', - options: DefaultFirebaseOptions.currentPlatform, - ); - - Stream stream = - FirebaseAuth.instanceFor(app: second).userChanges(); + group( + 'userChanges()', + () { + late StreamSubscription subscription; + tearDown(() async { + await subscription.cancel(); + }); - subscription = stream.listen( - expectAsync1( - (User? user) {}, - count: 1, - reason: 'Stream should only call once', - ), - ); + test('fires once on first initialization of FirebaseAuth', () async { + // Fixes a very specific bug: https://github.com/firebase/flutterfire/issues/3628 + // If the first initialization of FirebaseAuth involves the listeners userChanges() or idTokenChanges() + // the user will receive two events. Why? The native SDK listener will always fire an event upon initial + // listen. FirebaseAuth also sends an initial synthetic event. We send a synthetic event because, ordinarily, the user will + // not use a listener as the first occurrence of FirebaseAuth. We, therefore, mimic native behavior by sending an + // event. This test proves the logic of PR: https://github.com/firebase/flutterfire/pull/6560 + + // Requires a fresh app. + FirebaseApp second = await Firebase.initializeApp( + name: 'test-init', + options: DefaultFirebaseOptions.currentPlatform, + ); - await Future.delayed(const Duration(seconds: 2)); - }); + Stream stream = + FirebaseAuth.instanceFor(app: second).userChanges(); - test('calls callback with the current user and when user state changes', - () async { - await ensureSignedIn(testEmail); + subscription = stream.listen( + expectAsync1( + (User? user) {}, + count: 1, + reason: 'Stream should only call once', + ), + ); - Stream stream = FirebaseAuth.instance.userChanges(); - int call = 0; + await Future.delayed(const Duration(seconds: 2)); + }); - subscription = stream.listen( - expectAsync1( - (User? user) { - call++; - if (call == 1) { - expect(user!.displayName, isNull); // initial user - } else if (call == 2) { - expect( - user!.displayName, - equals('updatedName'), - ); // updated profile - } else { - fail('Should not have been called'); - } - }, - count: 2, - reason: 'Stream should only have been called 2 times', - ), - ); + test( + 'calls callback with the current user and when user state changes', + () async { + await ensureSignedIn(testEmail); + + Stream stream = FirebaseAuth.instance.userChanges(); + int call = 0; + + subscription = stream.listen( + expectAsync1( + (User? user) { + call++; + if (call == 1) { + expect(user!.displayName, isNull); // initial user + } else if (call == 2) { + expect( + user!.displayName, + equals('updatedName'), + ); // updated profile + } else { + fail('Should not have been called'); + } + }, + count: 2, + reason: 'Stream should only have been called 2 times', + ), + ); - await FirebaseAuth.instance.currentUser! - .updateDisplayName('updatedName'); + await FirebaseAuth.instance.currentUser! + .updateDisplayName('updatedName'); - expect( - FirebaseAuth.instance.currentUser!.displayName, - equals('updatedName'), - ); - }); - }, skip: !kIsWeb && Platform.isWindows,); + expect( + FirebaseAuth.instance.currentUser!.displayName, + equals('updatedName'), + ); + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); group('currentUser', () { test('should return currentUser', () async { @@ -200,47 +205,59 @@ void main() { }); }); - group('applyActionCode', () { - test('throws if invalid code', () async { - try { - await FirebaseAuth.instance.applyActionCode('!!!!!!'); - fail('Should have thrown'); - } on FirebaseException catch (e) { - expect(e.code, equals('invalid-action-code')); - } catch (e) { - fail(e.toString()); - } - }); - }, skip: !kIsWeb && Platform.isWindows,); + group( + 'applyActionCode', + () { + test('throws if invalid code', () async { + try { + await FirebaseAuth.instance.applyActionCode('!!!!!!'); + fail('Should have thrown'); + } on FirebaseException catch (e) { + expect(e.code, equals('invalid-action-code')); + } catch (e) { + fail(e.toString()); + } + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); - group('checkActionCode()', () { - test('throws on invalid code', () async { - try { - await FirebaseAuth.instance.checkActionCode('!!!!!!'); - fail('Should have thrown'); - } on FirebaseException catch (e) { - expect(e.code, equals('invalid-action-code')); - } catch (e) { - fail(e.toString()); - } - }); - }, skip: !kIsWeb && Platform.isWindows,); + group( + 'checkActionCode()', + () { + test('throws on invalid code', () async { + try { + await FirebaseAuth.instance.checkActionCode('!!!!!!'); + fail('Should have thrown'); + } on FirebaseException catch (e) { + expect(e.code, equals('invalid-action-code')); + } catch (e) { + fail(e.toString()); + } + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); - group('confirmPasswordReset()', () { - test('throws on invalid code', () async { - try { - await FirebaseAuth.instance.confirmPasswordReset( - code: '!!!!!!', - newPassword: 'thingamajig', - ); - fail('Should have thrown'); - } on FirebaseException catch (e) { - expect(e.code, equals('invalid-action-code')); - } catch (e) { - fail(e.toString()); - } - }); - }, skip: !kIsWeb && Platform.isWindows,); + group( + 'confirmPasswordReset()', + () { + test('throws on invalid code', () async { + try { + await FirebaseAuth.instance.confirmPasswordReset( + code: '!!!!!!', + newPassword: 'thingamajig', + ); + fail('Should have thrown'); + } on FirebaseException catch (e) { + expect(e.code, equals('invalid-action-code')); + } catch (e) { + fail(e.toString()); + } + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); group('createUserWithEmailAndPassword', () { test('should create a user with an email and password', () async { @@ -324,36 +341,41 @@ void main() { }); }); - group('fetchSignInMethodsForEmail()', () { - test('should return password provider for an email address', () async { - var providers = - // ignore: deprecated_member_use - await FirebaseAuth.instance.fetchSignInMethodsForEmail(testEmail); - expect(providers, isList); - expect(providers.contains('password'), isTrue); - }); + group( + 'fetchSignInMethodsForEmail()', + () { + test('should return password provider for an email address', + () async { + var providers = await FirebaseAuth.instance + // ignore: deprecated_member_use + .fetchSignInMethodsForEmail(testEmail); + expect(providers, isList); + expect(providers.contains('password'), isTrue); + }); - test('should return empty array for a not found email', () async { - var providers = await FirebaseAuth.instance - // ignore: deprecated_member_use - .fetchSignInMethodsForEmail(generateRandomEmail()); + test('should return empty array for a not found email', () async { + var providers = await FirebaseAuth.instance + // ignore: deprecated_member_use + .fetchSignInMethodsForEmail(generateRandomEmail()); - expect(providers, isList); - expect(providers, isEmpty); - }); + expect(providers, isList); + expect(providers, isEmpty); + }); - test('throws for a bad email address', () async { - try { - // ignore: deprecated_member_use - await FirebaseAuth.instance.fetchSignInMethodsForEmail('foobar'); - fail('Should have thrown'); - } on FirebaseAuthException catch (e) { - expect(e.code, equals('invalid-email')); - } catch (e) { - fail(e.toString()); - } - }); - }, skip: !kIsWeb && Platform.isWindows,); + test('throws for a bad email address', () async { + try { + // ignore: deprecated_member_use + await FirebaseAuth.instance.fetchSignInMethodsForEmail('foobar'); + fail('Should have thrown'); + } on FirebaseAuthException catch (e) { + expect(e.code, equals('invalid-email')); + } catch (e) { + fail(e.toString()); + } + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); group('isSignInWithEmailLink()', () { test('should return true or false', () { @@ -384,74 +406,82 @@ void main() { }); }); - group('sendPasswordResetEmail()', () { - test('should not error', () async { - var email = generateRandomEmail(); + group( + 'sendPasswordResetEmail()', + () { + test('should not error', () async { + var email = generateRandomEmail(); - try { - await FirebaseAuth.instance.createUserWithEmailAndPassword( - email: email, - password: testPassword, - ); + try { + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); - await FirebaseAuth.instance.sendPasswordResetEmail(email: email); - await FirebaseAuth.instance.currentUser!.delete(); - } catch (e) { - await FirebaseAuth.instance.currentUser!.delete(); - fail(e.toString()); - } - }); + await FirebaseAuth.instance.sendPasswordResetEmail(email: email); + await FirebaseAuth.instance.currentUser!.delete(); + } catch (e) { + await FirebaseAuth.instance.currentUser!.delete(); + fail(e.toString()); + } + }); - test('fails if the user could not be found', () async { - try { - await FirebaseAuth.instance - .sendPasswordResetEmail(email: 'does-not-exist@bar.com'); - fail('Should have thrown'); - } on FirebaseAuthException catch (e) { - expect(e.code, equals('user-not-found')); - } catch (e) { - fail(e.toString()); - } - }); - }, skip: !kIsWeb && Platform.isWindows,); + test('fails if the user could not be found', () async { + try { + await FirebaseAuth.instance + .sendPasswordResetEmail(email: 'does-not-exist@bar.com'); + fail('Should have thrown'); + } on FirebaseAuthException catch (e) { + expect(e.code, equals('user-not-found')); + } catch (e) { + fail(e.toString()); + } + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); - group('sendSignInLinkToEmail()', () { - test('should send email successfully', () async { - const email = 'email-signin-test@example.com'; - const continueUrl = 'http://action-code-test.com'; + group( + 'sendSignInLinkToEmail()', + () { + test('should send email successfully', () async { + const email = 'email-signin-test@example.com'; + const continueUrl = 'http://action-code-test.com'; - await FirebaseAuth.instance.createUserWithEmailAndPassword( - email: email, - password: testPassword, - ); + await FirebaseAuth.instance.createUserWithEmailAndPassword( + email: email, + password: testPassword, + ); - final actionCodeSettings = ActionCodeSettings( - url: continueUrl, - handleCodeInApp: true, - ); + final actionCodeSettings = ActionCodeSettings( + url: continueUrl, + handleCodeInApp: true, + ); - await FirebaseAuth.instance.sendSignInLinkToEmail( - email: email, - actionCodeSettings: actionCodeSettings, - ); + await FirebaseAuth.instance.sendSignInLinkToEmail( + email: email, + actionCodeSettings: actionCodeSettings, + ); - // Confirm with the emulator that it triggered an email sending code. - final oobCode = await emulatorOutOfBandCode( - email, - EmulatorOobCodeType.emailSignIn, - ); - expect(oobCode, isNotNull); - expect(oobCode?.email, email); - expect(oobCode?.type, EmulatorOobCodeType.emailSignIn); + // Confirm with the emulator that it triggered an email sending code. + final oobCode = await emulatorOutOfBandCode( + email, + EmulatorOobCodeType.emailSignIn, + ); + expect(oobCode, isNotNull); + expect(oobCode?.email, email); + expect(oobCode?.type, EmulatorOobCodeType.emailSignIn); - // Confirm the continue url was passed through to backend correctly. - final url = Uri.parse(oobCode!.oobLink!); - expect( - url.queryParameters['continueUrl'], - Uri.encodeFull(continueUrl), - ); - }); - }, skip: !kIsWeb && Platform.isWindows,); + // Confirm the continue url was passed through to backend correctly. + final url = Uri.parse(oobCode!.oobLink!); + expect( + url.queryParameters['continueUrl'], + Uri.encodeFull(continueUrl), + ); + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); group('languageCode', () { test('should change the language code', () async { @@ -487,68 +517,80 @@ void main() { ); }); - group('setPersistence()', () { + group( + 'setPersistence()', + () { + test( + 'throw an unimplemented error', + () async { + try { + await FirebaseAuth.instance.setPersistence(Persistence.LOCAL); + fail('Should have thrown'); + } catch (e) { + expect(e, isInstanceOf()); + } + }, + skip: kIsWeb || defaultTargetPlatform == TargetPlatform.macOS, + ); + + test( + 'should set persistence', + () async { + try { + await FirebaseAuth.instance.setPersistence(Persistence.LOCAL); + } catch (e) { + fail('unexpected error thrown'); + } + }, + skip: !kIsWeb, + ); + }, + skip: !kIsWeb && Platform.isWindows, + ); + + group('signInAnonymously()', () { test( - 'throw an unimplemented error', + 'should sign in anonymously', () async { - try { - await FirebaseAuth.instance.setPersistence(Persistence.LOCAL); - fail('Should have thrown'); - } catch (e) { - expect(e, isInstanceOf()); + Future successCallback(UserCredential currentUserCredential) async { + final currentUser = currentUserCredential.user; + + expect(currentUser, isA()); + expect(currentUser?.uid, isA()); + expect(currentUser?.email, isNull); + expect(currentUser?.isAnonymous, isTrue); + expect( + currentUser?.uid, + equals(FirebaseAuth.instance.currentUser!.uid), + ); + + var additionalUserInfo = currentUserCredential.additionalUserInfo; + expect(additionalUserInfo, isInstanceOf()); + + await FirebaseAuth.instance.signOut(); } + + final userCred = await FirebaseAuth.instance.signInAnonymously(); + await successCallback(userCred); }, - skip: kIsWeb || defaultTargetPlatform == TargetPlatform.macOS, + skip: !kIsWeb && Platform.isWindows, ); + }); + group('signInWithCredential()', () { test( - 'should set persistence', + 'should login with email and password', () async { - try { - await FirebaseAuth.instance.setPersistence(Persistence.LOCAL); - } catch (e) { - fail('unexpected error thrown'); - } + final credential = EmailAuthProvider.credential( + email: testEmail, + password: testPassword, + ); + await FirebaseAuth.instance + .signInWithCredential(credential) + .then(commonSuccessCallback); }, - skip: !kIsWeb, + skip: !kIsWeb && Platform.isWindows, ); - }, skip: !kIsWeb && Platform.isWindows,); - - group('signInAnonymously()', () { - test('should sign in anonymously', () async { - Future successCallback(UserCredential currentUserCredential) async { - final currentUser = currentUserCredential.user; - - expect(currentUser, isA()); - expect(currentUser?.uid, isA()); - expect(currentUser?.email, isNull); - expect(currentUser?.isAnonymous, isTrue); - expect( - currentUser?.uid, - equals(FirebaseAuth.instance.currentUser!.uid), - ); - - var additionalUserInfo = currentUserCredential.additionalUserInfo; - expect(additionalUserInfo, isInstanceOf()); - - await FirebaseAuth.instance.signOut(); - } - - final userCred = await FirebaseAuth.instance.signInAnonymously(); - await successCallback(userCred); - }, skip: !kIsWeb && Platform.isWindows,); - }); - - group('signInWithCredential()', () { - test('should login with email and password', () async { - final credential = EmailAuthProvider.credential( - email: testEmail, - password: testPassword, - ); - await FirebaseAuth.instance - .signInWithCredential(credential) - .then(commonSuccessCallback); - }, skip: !kIsWeb && Platform.isWindows,); test('throws if login user is disabled', () async { final credential = EmailAuthProvider.credential( @@ -613,40 +655,80 @@ void main() { fail(e.toString()); } }); + + test( + 'throw Exception when using incorrect auth details with GoogleAuthProvider', + () async { + final credential = GoogleAuthProvider.credential( + idToken: 'incorrect idToken', + ); + + await expectLater( + FirebaseAuth.instance.signInWithCredential(credential), + throwsA( + isA().having( + (e) => e.code, + 'code', + contains('invalid-credential'), + ), + ), + ); + + final credential2 = GoogleAuthProvider.credential( + accessToken: 'incorrect accessToken', + ); + + await expectLater( + FirebaseAuth.instance.signInWithCredential(credential2), + throwsA( + isA(), + // Live project has this error code, emulator throws "internal-error" + // .having( + // (e) => e.code, + // 'code', + // contains('invalid-credential'), + // ), + ), + ); + }); }); - group('signInWithCustomToken()', () { - test('signs in with custom auth token', () async { - final userCredential = - await FirebaseAuth.instance.signInAnonymously(); - final uid = userCredential.user!.uid; - final claims = { - 'roles': [ - {'role': 'member'}, - {'role': 'admin'}, - ], - }; + group( + 'signInWithCustomToken()', + () { + test('signs in with custom auth token', () async { + final userCredential = + await FirebaseAuth.instance.signInAnonymously(); + final uid = userCredential.user!.uid; + final claims = { + 'roles': [ + {'role': 'member'}, + {'role': 'admin'}, + ], + }; - await ensureSignedOut(); + await ensureSignedOut(); - expect(FirebaseAuth.instance.currentUser, null); + expect(FirebaseAuth.instance.currentUser, null); - final customToken = emulatorCreateCustomToken(uid, claims: claims); + final customToken = emulatorCreateCustomToken(uid, claims: claims); - final customTokenUserCredential = - await FirebaseAuth.instance.signInWithCustomToken(customToken); + final customTokenUserCredential = + await FirebaseAuth.instance.signInWithCustomToken(customToken); - expect(customTokenUserCredential.user!.uid, equals(uid)); - expect(FirebaseAuth.instance.currentUser!.uid, equals(uid)); + expect(customTokenUserCredential.user!.uid, equals(uid)); + expect(FirebaseAuth.instance.currentUser!.uid, equals(uid)); - final idTokenResult = - await FirebaseAuth.instance.currentUser!.getIdTokenResult(); + final idTokenResult = + await FirebaseAuth.instance.currentUser!.getIdTokenResult(); - expect(idTokenResult.claims!['roles'], isA()); - expect(idTokenResult.claims!['roles'][0], isA()); - expect(idTokenResult.claims!['roles'][0]['role'], 'member'); - }); - }, skip: !kIsWeb && Platform.isWindows,); + expect(idTokenResult.claims!['roles'], isA()); + expect(idTokenResult.claims!['roles'][0], isA()); + expect(idTokenResult.claims!['roles'][0]['role'], 'member'); + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); group('signInWithEmailAndPassword()', () { test('should login with email and password', () async { @@ -728,18 +810,22 @@ void main() { }); }); - group('verifyPasswordResetCode()', () { - test('throws on invalid code', () async { - try { - await FirebaseAuth.instance.verifyPasswordResetCode('!!!!!!'); - fail('Should have thrown'); - } on FirebaseException catch (e) { - expect(e.code, equals('invalid-action-code')); - } catch (e) { - fail(e.toString()); - } - }); - }, skip: !kIsWeb && Platform.isWindows,); + group( + 'verifyPasswordResetCode()', + () { + test('throws on invalid code', () async { + try { + await FirebaseAuth.instance.verifyPasswordResetCode('!!!!!!'); + fail('Should have thrown'); + } on FirebaseException catch (e) { + expect(e.code, equals('invalid-action-code')); + } catch (e) { + fail(e.toString()); + } + }); + }, + skip: !kIsWeb && Platform.isWindows, + ); group( 'verifyPhoneNumber()', @@ -832,7 +918,9 @@ void main() { skip: kIsWeb || defaultTargetPlatform != TargetPlatform.android, ); }, - skip: defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.windows || kIsWeb, + skip: defaultTargetPlatform == TargetPlatform.macOS || + defaultTargetPlatform == TargetPlatform.windows || + kIsWeb, ); group('setSettings()', () {