Skip to content

Commit 83e78bc

Browse files
committed
feat(firebase_auth, emulator): implement useEmulutor [WIP]
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
1 parent bd20916 commit 83e78bc

File tree

18 files changed

+145
-0
lines changed

18 files changed

+145
-0
lines changed

packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/Constants.java

+2
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,6 @@ public class Constants {
7777
public static final String HANDLE_CODE_IN_APP = "handleCodeInApp";
7878
public static final String ACTION_CODE_SETTINGS = "actionCodeSettings";
7979
public static final String AUTO_RETRIEVED_SMS_CODE_FOR_TESTING = "autoRetrievedSmsCodeForTesting";
80+
public static final String HOST = "host";
81+
public static final String PORT = "port";
8082
}

packages/firebase_auth/firebase_auth/android/src/main/java/io/flutter/plugins/firebase/auth/FlutterFirebaseAuthPlugin.java

+14
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,18 @@ private Task<Void> signOut(Map<String, Object> arguments) {
757757
});
758758
}
759759

760+
private Task<Void> useEmulator(Map<String, Object> arguments) {
761+
return Tasks.call(
762+
cachedThreadPool,
763+
() -> {
764+
FirebaseAuth firebaseAuth = getAuth(arguments);
765+
String host = (String) arguments.get(Constants.HOST);
766+
int port = (int) arguments.get(Constants.PORT);
767+
firebaseAuth.useEmulator(host, port);
768+
return null;
769+
});
770+
}
771+
760772
private Task<Map<String, Object>> verifyPasswordResetCode(Map<String, Object> arguments) {
761773
return Tasks.call(
762774
cachedThreadPool,
@@ -1189,6 +1201,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
11891201
case "Auth#signOut":
11901202
methodCallTask = signOut(call.arguments());
11911203
break;
1204+
case "Auth#useEmulator":
1205+
methodCallTask = useEmulator(call.arguments());
11921206
case "Auth#verifyPasswordResetCode":
11931207
methodCallTask = verifyPasswordResetCode(call.arguments());
11941208
break;

packages/firebase_auth/firebase_auth/example/lib/main.dart

+4
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,20 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:firebase_auth/firebase_auth.dart';
56
import 'package:firebase_core/firebase_core.dart';
67
import 'package:flutter/material.dart';
78
import 'package:flutter_signin_button/button_builder.dart';
89

910
import './register_page.dart';
1011
import './signin_page.dart';
1112

13+
final FirebaseAuth _auth = FirebaseAuth.instance;
14+
1215
Future<void> main() async {
1316
WidgetsFlutterBinding.ensureInitialized();
1417
await Firebase.initializeApp();
18+
_auth.useEmulator('http://localhost:9099');
1519
runApp(AuthExampleApp());
1620
}
1721

packages/firebase_auth/firebase_auth/example/test_driver/firebase_auth_e2e.dart

+2
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// for details. All rights reserved. Use of this source code is governed by a
55
// BSD-style license that can be found in the LICENSE file.
66

7+
import 'package:firebase_auth/firebase_auth.dart';
78
import 'package:drive/drive.dart' as drive;
89
import 'package:firebase_core/firebase_core.dart';
910
import 'package:flutter_test/flutter_test.dart';
@@ -17,6 +18,7 @@ bool USE_EMULATOR = false;
1718
void testsMain() {
1819
setUpAll(() async {
1920
await Firebase.initializeApp();
21+
FirebaseAuth.instance.useEmulator('http://localhost:9099');
2022
});
2123

2224
runInstanceTests();

packages/firebase_auth/firebase_auth/example/test_driver/instance_e2e.dart

+1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ void runInstanceTests() {
4040
await Firebase.initializeApp();
4141
await FirebaseAuth.instance
4242
.setSettings(appVerificationDisabledForTesting: true);
43+
FirebaseAuth.instance.useEmulator('http://localhost:9099');
4344
auth = FirebaseAuth.instance;
4445
});
4546

packages/firebase_auth/firebase_auth/example/test_driver/user_e2e.dart

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ void runUserTests() {
1818
String email = generateRandomEmail();
1919

2020
setUpAll(() async {
21+
FirebaseAuth.instance.useEmulator('http://localhost:9099');
2122
auth = FirebaseAuth.instance;
2223
if (auth.currentUser != null) {
2324
await auth.signOut();

packages/firebase_auth/firebase_auth/example/web/index.html

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
};
2323
// Initialize Firebase
2424
firebase.initializeApp(firebaseConfig);
25+
26+
// Configure web auth for emulator
27+
//firebase.auth().useEmulator('http://localhost:9099');
2528
</script>
2629
<script src="main.dart.js" type="application/javascript"></script>
2730
</body>

packages/firebase_auth/firebase_auth/ios/Classes/FLTFirebaseAuthPlugin.m

+8
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)flutter
212212
[self signInWithEmailLink:call.arguments withMethodCallResult:methodCallResult];
213213
} else if ([@"Auth#signOut" isEqualToString:call.method]) {
214214
[self signOut:call.arguments withMethodCallResult:methodCallResult];
215+
} else if ([@"Auth#useEmulator" isEqualToString:call.method]) {
216+
[self useEmulator:call.arguments withMethodCallResult:methodCallResult];
215217
} else if ([@"Auth#verifyPasswordResetCode" isEqualToString:call.method]) {
216218
[self verifyPasswordResetCode:call.arguments withMethodCallResult:methodCallResult];
217219
} else if ([@"Auth#verifyPhoneNumber" isEqualToString:call.method]) {
@@ -550,6 +552,12 @@ - (void)signOut:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult
550552
}
551553
}
552554

555+
- (void)useEmulator:(id)arguments withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
556+
FIRAuth *auth = [self getFIRAuthFromArguments:arguments];
557+
[auth useEmulatorWithHost:arguments[@"host"] port:arguments[@"port"]];
558+
result.success(nil);
559+
}
560+
553561
- (void)verifyPasswordResetCode:(id)arguments
554562
withMethodCallResult:(FLTFirebaseMethodCallResult *)result {
555563
FIRAuth *auth = [self getFIRAuthFromArguments:arguments];

packages/firebase_auth/firebase_auth/lib/firebase_auth.dart

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:async';
99
import 'package:firebase_core_platform_interface/firebase_core_platform_interface.dart';
1010
import 'package:firebase_auth_platform_interface/firebase_auth_platform_interface.dart';
1111
import 'package:firebase_core/firebase_core.dart';
12+
import 'package:flutter/foundation.dart';
1213
import 'package:flutter/material.dart';
1314
import 'package:meta/meta.dart';
1415

packages/firebase_auth/firebase_auth/lib/src/firebase_auth.dart

+34
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,40 @@ class FirebaseAuth extends FirebasePluginPlatform {
6464
return FirebaseAuth.instanceFor(app: app);
6565
}
6666

67+
/// Changes this instance to point to an Auth emulator running locally.
68+
///
69+
/// Set the [origin] of the local emulator, such as "http://localhost:9099"
70+
///
71+
/// Note: Must be called immediately, prior to accessing auth methods.
72+
/// Do not use with production credentials as emulator traffic is not encrypted.
73+
Map<String, dynamic> useEmulator(String origin) {
74+
assert(origin.isNotEmpty);
75+
76+
// Android considers localhost as 10.0.2.2 - automatically handle this for users.
77+
if (defaultTargetPlatform == TargetPlatform.android) {
78+
if (origin.startsWith('http://localhost')) {
79+
origin = origin.replaceFirst('http://localhost', 'http://10.0.2.2');
80+
} else if (origin.startsWith('http://127.0.0.1')) {
81+
origin = origin.replaceFirst('http://127.0.0.1', 'http://10.0.2.2');
82+
}
83+
}
84+
85+
// Native calls take the host and port split out
86+
final hostPortRegex = RegExp(r'^http:\/\/([\w\d.]+):(\d+)$');
87+
if (!hostPortRegex.hasMatch(origin)) {
88+
throw ArgumentError(
89+
'firebase.auth().useEmulator() unable to parse host and port from url');
90+
}
91+
final match = hostPortRegex.firstMatch(origin);
92+
final host = match.group(1);
93+
final port = int.parse(match.group(2));
94+
95+
_delegate.useEmulator(host, port);
96+
97+
// Return is used to test origin -> host/port parse
98+
return {'host': host, 'port': port};
99+
}
100+
67101
@Deprecated('Deprecated in favor of `authStateChanges`')
68102
// ignore: public_member_api_docs
69103
Stream<User> get onAuthStateChanged {

packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart

+8
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,14 @@ void main() {
168168
await auth.signInAnonymously();
169169
});
170170

171+
group('emulator', () {
172+
test('useEmulator()', () {
173+
Map<String, dynamic> result = auth.useEmulator('http://foo.com:31337');
174+
expect(result['host'], equals('foo.com'));
175+
expect(result['port'], equals(31337));
176+
});
177+
});
178+
171179
group('currentUser', () {
172180
test('get currentUser', () {
173181
User user = auth.currentUser;

packages/firebase_auth/firebase_auth_platform_interface/lib/src/method_channel/method_channel_firebase_auth.dart

+8
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,14 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform {
254254
return this;
255255
}
256256

257+
@override
258+
Future<void> useEmulator(String host, int port) async {
259+
await channel.invokeMethod<void>('Auth#useEmulator', <String, dynamic>{
260+
'host': host,
261+
'port': port,
262+
}).catchError(catchPlatformException);
263+
}
264+
257265
@override
258266
Future<void> applyActionCode(String code) async {
259267
await channel.invokeMethod<void>('Auth#applyActionCode', <String, dynamic>{

packages/firebase_auth/firebase_auth_platform_interface/lib/src/platform_interface/platform_interface_firebase_auth.dart

+11
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,17 @@ abstract class FirebaseAuthPlatform extends PlatformInterface {
8888
throw UnimplementedError('setInitialValues() is not implemented');
8989
}
9090

91+
/// Changes this instance to point to an Auth emulator running locally.
92+
///
93+
/// Set the [host] and [port] of the local emulator, such as "http://localhost"
94+
/// with port 9099
95+
///
96+
/// Note: Must be called immediately, prior to accessing auth methods.
97+
/// Do not use with production credentials as emulator traffic is not encrypted.
98+
Future<void> useEmulator(String host, int port) {
99+
throw UnimplementedError("useEmulator() is not implemented");
100+
}
101+
91102
/// Returns the current [User] if they are currently signed-in, or `null` if
92103
/// not.
93104
///

packages/firebase_auth/firebase_auth_platform_interface/test/method_channel_tests/method_channel_firebase_auth_test.dart

+16
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,22 @@ void main() {
831831
});
832832
});
833833

834+
group('useEmulator()', () {
835+
test('calls useEmulator correctly', () async {
836+
await auth.useEmulator('example.com', 31337);
837+
// check native method was called
838+
expect(log, <Matcher>[
839+
isMethodCall(
840+
'Auth#useEmulator',
841+
arguments: <String, dynamic>{
842+
'host': 'example.com',
843+
'port': 31337,
844+
},
845+
),
846+
]);
847+
});
848+
});
849+
834850
group('verifyPasswordResetCode()', () {
835851
const String testCode = 'testCode';
836852
test('returns a successful result', () async {

packages/firebase_auth/firebase_auth_platform_interface/test/platform_interface_tests/platform_interface_auth_test.dart

+10
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,16 @@ void main() {
401401
fail('Should have thrown an [UnimplementedError]');
402402
});
403403

404+
test('throws if .useEmulator', () {
405+
try {
406+
firebaseAuthPlatform.useEmulator('http://localhost', 9099);
407+
} on UnimplementedError catch (e) {
408+
expect(e.message, equals('useEmulator() is not implemented'));
409+
return;
410+
}
411+
fail('Should have thrown an [UnimplementedError]');
412+
});
413+
404414
test('throws if verifyPasswordResetCode()', () async {
405415
try {
406416
await firebaseAuthPlatform.verifyPasswordResetCode('test');

packages/firebase_auth/firebase_auth_web/lib/firebase_auth_web.dart

+12
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,18 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform {
333333
}
334334
}
335335

336+
@override
337+
Future<void> useEmulator(String host, int port) async {
338+
try {
339+
// The generic platform interface is with host and port split to
340+
// centralize logic between android/ios native, but web takes the
341+
// origin as a single string
342+
await _webAuth.useEmulator('http://' + host + ':' + port.toString());
343+
} catch (e) {
344+
throw getFirebaseAuthException(e);
345+
}
346+
}
347+
336348
@override
337349
Future<String> verifyPasswordResetCode(String code) async {
338350
try {

packages/firebase_auth/firebase_auth_web/lib/src/interop/auth.dart

+9
Original file line numberDiff line numberDiff line change
@@ -594,6 +594,15 @@ class Auth extends JsObjectWrapper<auth_interop.AuthJsImpl> {
594594
/// Signs out the current user.
595595
Future signOut() => handleThenable(jsObject.signOut());
596596

597+
/// Configures the Auth instance to work with a local emulator
598+
///
599+
/// Call with [origin] like 'http://localhost:9099'
600+
///
601+
/// Note: must be called before using auth methods, do not use
602+
/// with production credentials as local connections are unencrypted
603+
Future useEmulator(String origin) =>
604+
handleThenable(jsObject.useEmulator(origin));
605+
597606
/// Sets the current language to the default device/browser preference.
598607
void useDeviceLanguage() => jsObject.useDeviceLanguage();
599608

packages/firebase_auth/firebase_auth_web/lib/src/interop/auth_interop.dart

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ abstract class AuthJsImpl {
5656
AuthProviderJsImpl provider);
5757
external PromiseJsImpl<void> signInWithRedirect(AuthProviderJsImpl provider);
5858
external PromiseJsImpl<void> signOut();
59+
external PromiseJsImpl<void> useEmulator(String origin);
5960
external void useDeviceLanguage();
6061
external PromiseJsImpl<String> verifyPasswordResetCode(String code);
6162
}

0 commit comments

Comments
 (0)