| // Copyright 2017 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| import 'dart:async'; |
| |
| import 'package:firebase_core/firebase_core.dart'; |
| import 'package:firebase_database/firebase_database.dart'; |
| import 'package:flutter/services.dart'; |
| import 'package:flutter_test/flutter_test.dart'; |
| |
| void main() { |
| group('$FirebaseDatabase', () { |
| const MethodChannel channel = const MethodChannel( |
| 'plugins.flutter.io/firebase_database', |
| ); |
| |
| int mockHandleId = 0; |
| final List<MethodCall> log = <MethodCall>[]; |
| final FirebaseApp app = const FirebaseApp( |
| name: 'testApp', |
| ); |
| final String databaseURL = 'https://fake-database-url2.firebaseio.com'; |
| final FirebaseDatabase database = |
| new FirebaseDatabase(app: app, databaseURL: databaseURL); |
| |
| setUp(() async { |
| channel.setMockMethodCallHandler((MethodCall methodCall) async { |
| log.add(methodCall); |
| switch (methodCall.method) { |
| case 'Query#observe': |
| return mockHandleId++; |
| case 'FirebaseDatabase#setPersistenceEnabled': |
| return true; |
| case 'FirebaseDatabase#setPersistenceCacheSizeBytes': |
| return true; |
| case 'DatabaseReference#runTransaction': |
| Map<String, dynamic> updatedValue; |
| Future<Null> simulateEvent( |
| int transactionKey, final MutableData mutableData) async { |
| await BinaryMessages.handlePlatformMessage( |
| channel.name, |
| channel.codec.encodeMethodCall( |
| new MethodCall( |
| 'DoTransaction', |
| <String, dynamic>{ |
| 'transactionKey': transactionKey, |
| 'snapshot': <String, dynamic>{ |
| 'key': mutableData.key, |
| 'value': mutableData.value, |
| }, |
| }, |
| ), |
| ), |
| (_) { |
| updatedValue = channel.codec |
| .decodeEnvelope(_)['value'] |
| .cast<String, dynamic>(); |
| }, |
| ); |
| } |
| |
| await simulateEvent( |
| 0, |
| new MutableData.private(<String, dynamic>{ |
| 'key': 'fakeKey', |
| 'value': <String, dynamic>{'fakeKey': 'fakeValue'}, |
| })); |
| |
| return <String, dynamic>{ |
| 'error': null, |
| 'committed': true, |
| 'snapshot': <String, dynamic>{ |
| 'key': 'fakeKey', |
| 'value': updatedValue, |
| } |
| }; |
| default: |
| return null; |
| } |
| }); |
| log.clear(); |
| }); |
| |
| test('setPersistenceEnabled', () async { |
| expect(await database.setPersistenceEnabled(false), true); |
| expect(await database.setPersistenceEnabled(true), true); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'FirebaseDatabase#setPersistenceEnabled', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'enabled': false, |
| }, |
| ), |
| isMethodCall( |
| 'FirebaseDatabase#setPersistenceEnabled', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'enabled': true, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('setPersistentCacheSizeBytes', () async { |
| expect(await database.setPersistenceCacheSizeBytes(42), true); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'FirebaseDatabase#setPersistenceCacheSizeBytes', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'cacheSize': 42, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('goOnline', () async { |
| await database.goOnline(); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'FirebaseDatabase#goOnline', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('goOffline', () async { |
| await database.goOffline(); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'FirebaseDatabase#goOffline', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('purgeOutstandingWrites', () async { |
| await database.purgeOutstandingWrites(); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'FirebaseDatabase#purgeOutstandingWrites', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| group('$DatabaseReference', () { |
| test('set', () async { |
| final dynamic value = <String, dynamic>{'hello': 'world'}; |
| final int priority = 42; |
| await database.reference().child('foo').set(value); |
| await database.reference().child('bar').set(value, priority: priority); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'DatabaseReference#set', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': 'foo', |
| 'value': value, |
| 'priority': null, |
| }, |
| ), |
| isMethodCall( |
| 'DatabaseReference#set', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': 'bar', |
| 'value': value, |
| 'priority': priority, |
| }, |
| ), |
| ], |
| ); |
| }); |
| test('update', () async { |
| final dynamic value = <String, dynamic>{'hello': 'world'}; |
| await database.reference().child("foo").update(value); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'DatabaseReference#update', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': 'foo', |
| 'value': value, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('setPriority', () async { |
| final int priority = 42; |
| await database.reference().child('foo').setPriority(priority); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'DatabaseReference#setPriority', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': 'foo', |
| 'priority': priority, |
| }, |
| ), |
| ], |
| ); |
| }); |
| |
| test('runTransaction', () async { |
| final TransactionResult transactionResult = await database |
| .reference() |
| .child('foo') |
| .runTransaction((MutableData mutableData) { |
| return new Future<MutableData>(() { |
| mutableData.value['fakeKey'] = |
| 'updated ' + mutableData.value['fakeKey']; |
| return mutableData; |
| }); |
| }); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'DatabaseReference#runTransaction', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': 'foo', |
| 'transactionKey': 0, |
| 'transactionTimeout': 5000, |
| }, |
| ), |
| ], |
| ); |
| expect(transactionResult.committed, equals(true)); |
| expect(transactionResult.dataSnapshot.value, |
| equals(<String, dynamic>{'fakeKey': 'updated fakeValue'})); |
| expect( |
| database.reference().child('foo').runTransaction( |
| (MutableData mutableData) {}, |
| timeout: const Duration(milliseconds: 0), |
| ), |
| throwsA(const isInstanceOf<AssertionError>()), |
| ); |
| }); |
| }); |
| |
| group('$Query', () { |
| // TODO(jackson): Write more tests for queries |
| test('keepSynced, simple query', () async { |
| final String path = 'foo'; |
| final Query query = database.reference().child(path); |
| await query.keepSynced(true); |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'Query#keepSynced', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': path, |
| 'parameters': <String, dynamic>{}, |
| 'value': true, |
| }, |
| ), |
| ], |
| ); |
| }); |
| test('keepSynced, complex query', () async { |
| final int startAt = 42; |
| final String path = 'foo'; |
| final String childKey = 'bar'; |
| final bool endAt = true; |
| final String endAtKey = 'baz'; |
| final Query query = database |
| .reference() |
| .child(path) |
| .orderByChild(childKey) |
| .startAt(startAt) |
| .endAt(endAt, key: endAtKey); |
| await query.keepSynced(false); |
| final Map<String, dynamic> expectedParameters = <String, dynamic>{ |
| 'orderBy': 'child', |
| 'orderByChildKey': childKey, |
| 'startAt': startAt, |
| 'endAt': endAt, |
| 'endAtKey': endAtKey, |
| }; |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'Query#keepSynced', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': path, |
| 'parameters': expectedParameters, |
| 'value': false |
| }, |
| ), |
| ], |
| ); |
| }); |
| test('observing error events', () async { |
| mockHandleId = 99; |
| const int errorCode = 12; |
| const String errorDetails = 'Some details'; |
| final Query query = database.reference().child('some path'); |
| Future<Null> simulateError(String errorMessage) async { |
| await BinaryMessages.handlePlatformMessage( |
| channel.name, |
| channel.codec.encodeMethodCall( |
| new MethodCall('Error', <String, dynamic>{ |
| 'handle': 99, |
| 'error': <String, dynamic>{ |
| 'code': errorCode, |
| 'message': errorMessage, |
| 'details': errorDetails, |
| }, |
| }), |
| ), |
| (_) {}, |
| ); |
| } |
| |
| final AsyncQueue<DatabaseError> errors = |
| new AsyncQueue<DatabaseError>(); |
| |
| // Subscribe and allow subscription to complete. |
| final StreamSubscription<Event> subscription = |
| query.onValue.listen((_) {}, onError: errors.add); |
| await new Future<Null>.delayed(const Duration(seconds: 0)); |
| |
| await simulateError('Bad foo'); |
| await simulateError('Bad bar'); |
| final DatabaseError error1 = await errors.remove(); |
| final DatabaseError error2 = await errors.remove(); |
| subscription.cancel(); |
| expect(error1.code, errorCode); |
| expect(error1.message, 'Bad foo'); |
| expect(error1.details, errorDetails); |
| expect(error2.code, errorCode); |
| expect(error2.message, 'Bad bar'); |
| expect(error2.details, errorDetails); |
| }); |
| test('observing value events', () async { |
| mockHandleId = 87; |
| final String path = 'foo'; |
| final Query query = database.reference().child(path); |
| Future<Null> simulateEvent(String value) async { |
| await BinaryMessages.handlePlatformMessage( |
| channel.name, |
| channel.codec.encodeMethodCall( |
| new MethodCall('Event', <String, dynamic>{ |
| 'handle': 87, |
| 'snapshot': <String, dynamic>{ |
| 'key': path, |
| 'value': value, |
| }, |
| }), |
| ), |
| (_) {}, |
| ); |
| } |
| |
| final AsyncQueue<Event> events = new AsyncQueue<Event>(); |
| |
| // Subscribe and allow subscription to complete. |
| final StreamSubscription<Event> subscription = |
| query.onValue.listen(events.add); |
| await new Future<Null>.delayed(const Duration(seconds: 0)); |
| |
| await simulateEvent('1'); |
| await simulateEvent('2'); |
| final Event event1 = await events.remove(); |
| final Event event2 = await events.remove(); |
| expect(event1.snapshot.key, path); |
| expect(event1.snapshot.value, '1'); |
| expect(event2.snapshot.key, path); |
| expect(event2.snapshot.value, '2'); |
| |
| // Cancel subscription and allow cancellation to complete. |
| subscription.cancel(); |
| await new Future<Null>.delayed(const Duration(seconds: 0)); |
| |
| expect( |
| log, |
| <Matcher>[ |
| isMethodCall( |
| 'Query#observe', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': path, |
| 'parameters': <String, dynamic>{}, |
| 'eventType': '_EventType.value', |
| }, |
| ), |
| isMethodCall( |
| 'Query#removeObserver', |
| arguments: <String, dynamic>{ |
| 'app': app.name, |
| 'databaseURL': databaseURL, |
| 'path': path, |
| 'parameters': <String, dynamic>{}, |
| 'handle': 87, |
| }, |
| ), |
| ], |
| ); |
| }); |
| }); |
| }); |
| } |
| |
| /// Queue whose remove operation is asynchronous, awaiting a corresponding add. |
| class AsyncQueue<T> { |
| Map<int, Completer<T>> _completers = <int, Completer<T>>{}; |
| int _nextToRemove = 0; |
| int _nextToAdd = 0; |
| |
| void add(T element) { |
| _completer(_nextToAdd++).complete(element); |
| } |
| |
| Future<T> remove() { |
| return _completer(_nextToRemove++).future; |
| } |
| |
| Completer<T> _completer(int index) { |
| if (_completers.containsKey(index)) { |
| return _completers.remove(index); |
| } else { |
| return _completers[index] = new Completer<T>(); |
| } |
| } |
| } |