Visitar URL original
fix(firestore): avoid completing transaction future twice (#18554) · firebase/flutterfire@94e36f1 · GitHub
Skip to content

Commit 94e36f1

Browse files
authored
fix(firestore): avoid completing transaction future twice (#18554)
Re-check transaction completion after storing a Dart-side failure because native may report an error while that asynchronous call is pending.
1 parent b0585f0 commit 94e36f1

2 files changed

Lines changed: 143 additions & 1 deletion

File tree

‎packages/cloud_firestore/cloud_firestore_platform_interface/lib/src/method_channel/method_channel_firestore.dart‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,12 @@ class MethodChannelFirebaseFirestore extends FirebaseFirestorePlatform {
287287
null,
288288
);
289289

290-
// Allow the [runTransaction] method to listen to an error.
290+
// Native may report an error while the result is being stored.
291+
if (completer.isCompleted) {
292+
return;
293+
}
291294

295+
// Allow the [runTransaction] method to listen to an error.
292296
completer.completeError(error, stack);
293297

294298
return;
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
// Copyright 2017, the Chromium project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import 'dart:async';
6+
7+
import 'package:cloud_firestore_platform_interface/cloud_firestore_platform_interface.dart';
8+
import 'package:cloud_firestore_platform_interface/src/method_channel/method_channel_firestore.dart';
9+
import 'package:firebase_core/firebase_core.dart';
10+
import 'package:firebase_core_platform_interface/test.dart';
11+
import 'package:flutter/services.dart';
12+
import 'package:flutter_test/flutter_test.dart';
13+
import 'package:mockito/mockito.dart';
14+
15+
import 'pigeon/test_api.dart';
16+
17+
class _MockFirebaseFirestoreHostApi extends Mock
18+
implements TestFirebaseFirestoreHostApi {
19+
final Completer<void> storeResultCalled = Completer<void>();
20+
final Completer<void> releaseStoreResult = Completer<void>();
21+
22+
@override
23+
Future<String> transactionCreate(
24+
FirestorePigeonFirebaseApp app,
25+
int timeout,
26+
int maxAttempts,
27+
) async {
28+
return 'transaction-id';
29+
}
30+
31+
@override
32+
Future<void> transactionStoreResult(
33+
String transactionId,
34+
InternalTransactionResult resultType,
35+
List<InternalTransactionCommand?>? commands,
36+
) async {
37+
storeResultCalled.complete();
38+
await releaseStoreResult.future;
39+
}
40+
}
41+
42+
void main() {
43+
TestWidgetsFlutterBinding.ensureInitialized();
44+
setupFirebaseCoreMocks();
45+
46+
const transactionId = 'transaction-id';
47+
const channelName =
48+
'plugins.flutter.io/firebase_firestore/transaction/$transactionId';
49+
const codec = StandardMethodCodec(PigeonCodec());
50+
51+
late TestDefaultBinaryMessenger messenger;
52+
late _MockFirebaseFirestoreHostApi hostApi;
53+
late Completer<void> eventChannelListened;
54+
late FirebaseApp app;
55+
56+
setUpAll(() async {
57+
app = await Firebase.initializeApp(
58+
name: 'test-app',
59+
options: const FirebaseOptions(
60+
apiKey: 'api-key',
61+
appId: 'app-id',
62+
messagingSenderId: 'sender-id',
63+
projectId: 'project-id',
64+
),
65+
);
66+
});
67+
68+
setUp(() {
69+
messenger =
70+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger;
71+
hostApi = _MockFirebaseFirestoreHostApi();
72+
eventChannelListened = Completer<void>();
73+
74+
TestFirebaseFirestoreHostApi.setUp(hostApi);
75+
messenger.setMockMessageHandler(channelName, (ByteData? message) async {
76+
if (!eventChannelListened.isCompleted) {
77+
eventChannelListened.complete();
78+
}
79+
return codec.encodeSuccessEnvelope(null);
80+
});
81+
});
82+
83+
tearDown(() {
84+
TestFirebaseFirestoreHostApi.setUp(null);
85+
messenger.setMockMessageHandler(channelName, null);
86+
});
87+
88+
test(
89+
'does not complete the transaction future twice when native reports an '
90+
'error while storing a failure',
91+
() async {
92+
final firestore = MethodChannelFirebaseFirestore(
93+
app: app,
94+
databaseId: '(default)',
95+
);
96+
97+
final transactionFuture = firestore.runTransaction<void>(
98+
(_) => throw StateError('handler failed'),
99+
);
100+
101+
await eventChannelListened.future;
102+
unawaited(
103+
messenger.handlePlatformMessage(
104+
channelName,
105+
codec.encodeSuccessEnvelope(<String, Object?>{
106+
'appName': 'test-app',
107+
}),
108+
(_) {},
109+
),
110+
);
111+
await hostApi.storeResultCalled.future;
112+
113+
unawaited(
114+
messenger.handlePlatformMessage(
115+
channelName,
116+
codec.encodeSuccessEnvelope(<String, Object?>{
117+
'error': <String, Object?>{
118+
'code': 'deadline-exceeded',
119+
'message': 'Transaction timed out',
120+
},
121+
}),
122+
(_) {},
123+
),
124+
);
125+
126+
await expectLater(
127+
transactionFuture,
128+
throwsA(
129+
isA<FirebaseException>()
130+
.having((error) => error.code, 'code', 'deadline-exceeded'),
131+
),
132+
);
133+
134+
hostApi.releaseStoreResult.complete();
135+
await Future<void>.delayed(Duration.zero);
136+
},
137+
);
138+
}

0 commit comments

Comments
 (0)