Skip to content

Commit

Permalink
feat(firebase_auth, emulator): implement useEmulutor [WIP]
Browse files Browse the repository at this point in the history
Note that this passes unit tests but currently e2e tests do *not* correctly
use the emulator - the useEmulator() call seems to never happen? And if it were to
happen then many of the tests would fail

Publishing for visibility as others may know better how to hook it up
  • Loading branch information
mikehardy committed Jan 7, 2021
1 parent 6fbf625 commit e3e4d7b
Show file tree
Hide file tree
Showing 18 changed files with 149 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,6 @@ public class Constants {
public static final String HANDLE_CODE_IN_APP = "handleCodeInApp";
public static final String ACTION_CODE_SETTINGS = "actionCodeSettings";
public static final String AUTO_RETRIEVED_SMS_CODE_FOR_TESTING = "autoRetrievedSmsCodeForTesting";
public static final String HOST = "host";
public static final String PORT = "port";
}
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,18 @@ private Task<Void> signOut(Map<String, Object> arguments) {
});
}

private Task<Void> useEmulator(Map<String, Object> arguments) {
return Tasks.call(
cachedThreadPool,
() -> {
FirebaseAuth firebaseAuth = getAuth(arguments);
String host = (String) arguments.get(Constants.HOST);
int port = (int) arguments.get(Constants.PORT);
firebaseAuth.useEmulator(host, port);
return null;
});
}

private Task<Map<String, Object>> verifyPasswordResetCode(Map<String, Object> arguments) {
return Tasks.call(
cachedThreadPool,
Expand Down Expand Up @@ -1189,6 +1201,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
case "Auth#signOut":
methodCallTask = signOut(call.arguments());
break;
case "Auth#useEmulator":
methodCallTask = useEmulator(call.arguments());
case "Auth#verifyPasswordResetCode":
methodCallTask = verifyPasswordResetCode(call.arguments());
break;
Expand Down
4 changes: 4 additions & 0 deletions packages/firebase_auth/firebase_auth/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,20 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter_signin_button/button_builder.dart';

import './register_page.dart';
import './signin_page.dart';

final FirebaseAuth _auth = FirebaseAuth.instance;

Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
_auth.useEmulator('http://localhost:9099');
runApp(AuthExampleApp());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

import 'package:firebase_auth/firebase_auth.dart';
import 'package:drive/drive.dart' as drive;
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter_test/flutter_test.dart';
Expand All @@ -17,6 +18,7 @@ bool USE_EMULATOR = false;
void testsMain() {
setUpAll(() async {
await Firebase.initializeApp();
FirebaseAuth.instance.useEmulator('http://localhost:9099');
});

runInstanceTests();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ void runInstanceTests() {
await Firebase.initializeApp();
await FirebaseAuth.instance
.setSettings(appVerificationDisabledForTesting: true);
FirebaseAuth.instance.useEmulator('http://localhost:9099');
auth = FirebaseAuth.instance;
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ void runUserTests() {
String email = generateRandomEmail();

setUpAll(() async {
FirebaseAuth.instance.useEmulator('http://localhost:9099');
auth = FirebaseAuth.instance;
if (auth.currentUser != null) {
await auth.signOut();
Expand Down
3 changes: 3 additions & 0 deletions packages/firebase_auth/firebase_auth/example/web/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);

// Configure web auth for emulator
//firebase.auth().useEmulator('http://localhost:9099');
</script>
<script src="main.dart.js" type="application/javascript"></script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter
[self signInWithEmailLink:call.arguments withMethodCallResult:methodCallResult];
} else if ([@"Auth#signOut" isEqualToString:call.method]) {
[self signOut:call.arguments withMethodCallResult:methodCallResult];
} else if ([@"Auth#useEmulator" isEqualToString:call.method]) {
[self useEmulator:call.arguments withMethodCallResult:methodCallResult];
} else if ([@"Auth#verifyPasswordResetCode" isEqualToString:call.method]) {
[self verifyPasswordResetCode:call.arguments withMethodCallResult:methodCallResult];
} else if ([@"Auth#verifyPhoneNumber" isEqualToString:call.method]) {
Expand Down Expand Up @@ -550,6 +552,12 @@ - (void)signOut:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult
}
}

- (void)useEmulator:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
FIRAuth *auth = [self getFIRAuthFromArguments:arguments];
[auth useEmulatorWithHost:arguments[@"host"] port:arguments[@"port"]];
result.success(nil);
}

- (void)verifyPasswordResetCode:(id)arguments
withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
FIRAuth *auth = [self getFIRAuthFromArguments:arguments];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'dart:async';
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart';
import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:meta/meta.dart';

Expand Down
34 changes: 34 additions & 0 deletions packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,40 @@ class FirebaseAuth extends FirebasePluginPlatform {
return FirebaseAuth.instanceFor(app: app);
}

/// Changes this instance to point to an Auth emulator running locally.
///
/// Set the [origin] of the local emulator, such as "http://localhost:9099"
///
/// Note: Must be called immediately, prior to accessing auth methods.
/// Do not use with production credentials as emulator traffic is not encrypted.
Map<String, dynamic> useEmulator(String origin) {
assert(origin.isNotEmpty);

// Android considers localhost as 10.0.2.2 - automatically handle this for users.
if (defaultTargetPlatform == TargetPlatform.android) {
if (origin.startsWith('http://localhost')) {
origin = origin.replaceFirst('http://localhost', 'http://10.0.2.2');
} else if (origin.startsWith('http://127.0.0.1')) {
origin = origin.replaceFirst('http://127.0.0.1', 'http://10.0.2.2');
}
}

// Native calls take the host and port split out
final hostPortRegex = RegExp(r'^http:\/\/([\w\d.]+):(\d+)$');
if (!hostPortRegex.hasMatch(origin)) {
throw ArgumentError(
'firebase.auth().useEmulator() unable to parse host and port from url');
}
final match = hostPortRegex.firstMatch(origin);
final host = match.group(1);
final port = int.parse(match.group(2));

_delegate.useEmulator(host, port);

// Return is used to test origin -> host/port parse
return {'host': host, 'port': port};
}

@Deprecated('Deprecated in favor of `authStateChanges`')
// ignore: public_member_api_docs
Stream<User /*?*/ > get onAuthStateChanged {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,14 @@ void main() {
await auth.signInAnonymously();
});

group('emulator', () {
test('useEmulator()', () {
Map<String, dynamic> result = auth.useEmulator('http://foo.com:31337');
expect(result['host'], equals('foo.com'));
expect(result['port'], equals(31337));
});
});

group('currentUser', () {
test('get currentUser', () {
User user = auth.currentUser;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,18 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform {
return this;
}

@override
Future<void> useEmulator(String host, int port) async {
try {
await channel.invokeMethod<void>('Auth#useEmulator', <String, dynamic>{
'host': host,
'port': port,
});
} catch (e) {
throw convertPlatformException(e);
}
}

@override
Future<void> applyActionCode(String code) async {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,17 @@ abstract class FirebaseAuthPlatform extends PlatformInterface {
throw UnimplementedError('setInitialValues() is not implemented');
}

/// Changes this instance to point to an Auth emulator running locally.
///
/// Set the [host] and [port] of the local emulator, such as "http://localhost"
/// with port 9099
///
/// Note: Must be called immediately, prior to accessing auth methods.
/// Do not use with production credentials as emulator traffic is not encrypted.
Future<void> useEmulator(String host, int port) {
throw UnimplementedError("useEmulator() is not implemented");
}

/// Returns the current [User] if they are currently signed-in, or `null` if
/// not.
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -831,6 +831,22 @@ void main() {
});
});

group('useEmulator()', () {
test('calls useEmulator correctly', () async {
await auth.useEmulator('example.com', 31337);
// check native method was called
expect(log, <Matcher>[
isMethodCall(
'Auth#useEmulator',
arguments: <String, dynamic>{
'host': 'example.com',
'port': 31337,
},
),
]);
});
});

group('verifyPasswordResetCode()', () {
const String testCode = 'testCode';
test('returns a successful result', () async {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,16 @@ void main() {
fail('Should have thrown an [UnimplementedError]');
});

test('throws if .useEmulator', () {
try {
firebaseAuthPlatform.useEmulator('http://localhost', 9099);
} on UnimplementedError catch (e) {
expect(e.message, equals('useEmulator() is not implemented'));
return;
}
fail('Should have thrown an [UnimplementedError]');
});

test('throws if verifyPasswordResetCode()', () async {
try {
await firebaseAuthPlatform.verifyPasswordResetCode('test');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,18 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform {
}
}

@override
Future<void> useEmulator(String host, int port) async {
try {
// The generic platform interface is with host and port split to
// centralize logic between android/ios native, but web takes the
// origin as a single string
await _webAuth.useEmulator('http://' + host + ':' + port.toString());
} catch (e) {
throw getFirebaseAuthException(e);
}
}

@override
Future<String> verifyPasswordResetCode(String code) async {
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,15 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
/// Signs out the current user.
Future signOut() => handleThenable(jsObject.signOut());

/// Configures the Auth instance to work with a local emulator
///
/// Call with [origin] like 'http://localhost:9099'
///
/// Note: must be called before using auth methods, do not use
/// with production credentials as local connections are unencrypted
Future useEmulator(String origin) =>
handleThenable(jsObject.useEmulator(origin));

/// Sets the current language to the default device/browser preference.
void useDeviceLanguage() => jsObject.useDeviceLanguage();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ abstract class AuthJsImpl {
AuthProviderJsImpl provider);
external PromiseJsImpl<void> signInWithRedirect(AuthProviderJsImpl provider);
external PromiseJsImpl<void> signOut();
external PromiseJsImpl<void> useEmulator(String origin);
external void useDeviceLanguage();
external PromiseJsImpl<String> verifyPasswordResetCode(String code);
}
Expand Down

0 comments on commit e3e4d7b

Please sign in to comment.