Skip to content

Commit 3051c60

Browse files
committed
feat(firebase_auth, emulator): implement useEmulutor
Works in unit testing and when configured for e2e testing, except for the expected test failures on disabled users, OOB and SMS codes as the test suite needs a port to work against an empty auth emulator vs the pre-configured data in the cloud Prior art doing the auth tests is noted, but needs a port to javascript
1 parent 3e7bf73 commit 3051c60

File tree

18 files changed

+164
-0
lines changed

18 files changed

+164
-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

+15
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,18 @@ private Task<Void> signOut(Map<String, Object> arguments) {
764764
});
765765
}
766766

767+
private Task<Void> useEmulator(Map<String, Object> arguments) {
768+
return Tasks.call(
769+
cachedThreadPool,
770+
() -> {
771+
FirebaseAuth firebaseAuth = getAuth(arguments);
772+
String host = (String) arguments.get(Constants.HOST);
773+
int port = (int) arguments.get(Constants.PORT);
774+
firebaseAuth.useEmulator(host, port);
775+
return null;
776+
});
777+
}
778+
767779
private Task<Map<String, Object>> verifyPasswordResetCode(Map<String, Object> arguments) {
768780
return Tasks.call(
769781
cachedThreadPool,
@@ -1196,6 +1208,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
11961208
case "Auth#signOut":
11971209
methodCallTask = signOut(call.arguments());
11981210
break;
1211+
case "Auth#useEmulator":
1212+
methodCallTask = useEmulator(call.arguments());
1213+
break;
11991214
case "Auth#verifyPasswordResetCode":
12001215
methodCallTask = verifyPasswordResetCode(call.arguments());
12011216
break;

packages/firebase_auth/firebase_auth/example/android/app/src/main/AndroidManifest.xml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
additional functionality it is fine to subclass or reimplement
1010
FlutterApplication and put your custom class here. -->
1111
<application
12+
android:usesCleartextTraffic="true"
1213
android:name="io.flutter.app.FlutterApplication"
1314
android:label="firebaseauthexample"
1415
android:icon="@mipmap/ic_launcher">

packages/firebase_auth/firebase_auth/example/ios/Runner/Info.plist

+5
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@
4141
<string>$(FLUTTER_BUILD_NUMBER)</string>
4242
<key>LSRequiresIPhoneOS</key>
4343
<true/>
44+
<key>NSAppTransportSecurity</key>
45+
<dict>
46+
<key>NSAllowsArbitraryLoads</key>
47+
<true/>
48+
</dict>
4449
<key>UILaunchStoryboardName</key>
4550
<string>LaunchScreen</string>
4651
<key>UIMainStoryboardFile</key>

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

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
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'; // Only needed if you configure the Auth Emulator below
56
import 'package:firebase_core/firebase_core.dart';
67
import 'package:flutter/material.dart';
78
import 'package:flutter_signin_button/button_builder.dart';
@@ -12,6 +13,8 @@ import './signin_page.dart';
1213
Future<void> main() async {
1314
WidgetsFlutterBinding.ensureInitialized();
1415
await Firebase.initializeApp();
16+
// Uncomment this to use the auth emulator for testing
17+
// await FirebaseAuth.instance.useEmulator('http://localhost:9099');
1518
runApp(AuthExampleApp());
1619
}
1720

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

+8
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// BSD-style license that can be found in the LICENSE file.
66

77
import 'package:drive/drive.dart' as drive;
8+
//import 'package:firebase_auth/firebase_auth.dart'; // only needed if you use the Auth Emulator
89
import 'package:firebase_core/firebase_core.dart';
910
import 'package:flutter_test/flutter_test.dart';
1011

@@ -17,6 +18,13 @@ bool USE_EMULATOR = false;
1718
void testsMain() {
1819
setUpAll(() async {
1920
await Firebase.initializeApp();
21+
22+
// Configure the Auth test suite to use the Auth Emulator
23+
// This may not be enabled until the test suite is ported to:
24+
// - have ability to create disabled users
25+
// - have ability to fetch OOB and SMS verification codes
26+
// JS implementation to port to dart here: https://github.com/invertase/react-native-firebase/pull/4552/commits/4c688413cb6516ecfdbd4ea325103d0d8d8d84a8#diff-44ccd5fb03b0d9e447820032866f2494c5a400a52873f0f65518d06aedafe302
27+
// await FirebaseAuth.instance.useEmulator('http://localhost:9099');
2028
});
2129

2230
runInstanceTests();

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"] integerValue]];
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

+36
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,42 @@ class FirebaseAuth extends FirebasePluginPlatform {
8383
return null;
8484
}
8585

86+
/// Changes this instance to point to an Auth emulator running locally.
87+
///
88+
/// Set the [origin] of the local emulator, such as "http://localhost:9099"
89+
///
90+
/// Note: Must be called immediately, prior to accessing auth methods.
91+
/// Do not use with production credentials as emulator traffic is not encrypted.
92+
/// Returns host and port parsed from origin string, for confirmation/testing
93+
Future<Map<String, dynamic>> useEmulator(String origin) async {
94+
assert(origin != null);
95+
assert(origin.isNotEmpty);
96+
97+
// Android considers localhost as 10.0.2.2 - automatically handle this for users.
98+
if (defaultTargetPlatform == TargetPlatform.android) {
99+
if (origin.startsWith('http://localhost')) {
100+
origin = origin.replaceFirst('http://localhost', 'http://10.0.2.2');
101+
} else if (origin.startsWith('http://127.0.0.1')) {
102+
origin = origin.replaceFirst('http://127.0.0.1', 'http://10.0.2.2');
103+
}
104+
}
105+
106+
// Native calls take the host and port split out
107+
final hostPortRegex = RegExp(r'^http:\/\/([\w\d.]+):(\d+)$');
108+
if (!hostPortRegex.hasMatch(origin)) {
109+
throw ArgumentError(
110+
'firebase.auth().useEmulator() unable to parse host and port from url');
111+
}
112+
final match = hostPortRegex.firstMatch(origin);
113+
final host = match.group(1);
114+
final port = int.parse(match.group(2));
115+
116+
await _delegate.useEmulator(host, port);
117+
118+
// Return is used to test origin -> host/port parse
119+
return {'host': host, 'port': port};
120+
}
121+
86122
/// Applies a verification code sent to the user by email or other out-of-band
87123
/// mechanism.
88124
///

packages/firebase_auth/firebase_auth/test/firebase_auth_test.dart

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

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

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

+13
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,19 @@ class MethodChannelFirebaseAuth extends FirebaseAuthPlatform {
255255
return this;
256256
}
257257

258+
@override
259+
Future<void> useEmulator(String host, int port) async {
260+
try {
261+
await channel.invokeMethod<void>('Auth#useEmulator', <String, dynamic>{
262+
'appName': app.name,
263+
'host': host,
264+
'port': port,
265+
});
266+
} catch (e) {
267+
throw convertPlatformException(e);
268+
}
269+
}
270+
258271
@override
259272
Future<void> applyActionCode(String code) async {
260273
try {

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
@@ -115,6 +115,17 @@ abstract class FirebaseAuthPlatform extends PlatformInterface {
115115
throw UnimplementedError("sendAuthChangesEvent() is not implemented");
116116
}
117117

118+
/// Changes this instance to point to an Auth emulator running locally.
119+
///
120+
/// Set the [host] and [port] of the local emulator, such as "http://localhost"
121+
/// with port 9099
122+
///
123+
/// Note: Must be called immediately, prior to accessing auth methods.
124+
/// Do not use with production credentials as emulator traffic is not encrypted.
125+
Future<void> useEmulator(String host, int port) {
126+
throw UnimplementedError("useEmulator() is not implemented");
127+
}
128+
118129
/// Applies a verification code sent to the user by email or other out-of-band
119130
/// mechanism.
120131
///

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

+17
Original file line numberDiff line numberDiff line change
@@ -831,6 +831,23 @@ 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+
'appName': defaultFirebaseAppName,
843+
'host': 'example.com',
844+
'port': 31337,
845+
},
846+
),
847+
]);
848+
});
849+
});
850+
834851
group('verifyPasswordResetCode()', () {
835852
const String testCode = 'testCode';
836853
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
@@ -332,6 +332,18 @@ class FirebaseAuthWeb extends FirebaseAuthPlatform {
332332
}
333333
}
334334

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

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

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

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

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)