How to unit-test timeout in Flutter? - firebase

I have a facade function that reloads the current firebase user and returns it. The thing is that the user reloading part has a timeout and it needs to be tested.
Function:
Future<Option<User>> getSignedInUser() async {
// Reload currentUser if possible
// it mustn't throw [TimeoutException] for whole function,
// this is what this try/catch does
try {
await reloadCurrentUser().timeout(const Duration(seconds: 20));
} catch (e) {
log(e.toString(), name: TAG);
}
return optionOf(_auth.currentUser);
}
reloadCurrentUser() function:
Future<Either<AuthFailure, Unit>> reloadCurrentUser() async {
try {
await _auth.currentUser?.reload();
return right(unit);
} catch (e) {
log(e.toString(), name: TAG);
return left(const AuthFailure.userReloadingError());
}
}
The question is how to test reloadCurrentUser() timeout? I'm trying to throw a TimeoutException when this function is called, but then it throws an error for the whole test.
Current Test function:
test(
'Reaches timeout when reloading currentUser, '
'throws TimeoutException, but function continues '
'and returns optionOf currentUser', () async {
reset(fakeFirebaseAuth);
reset(fakeFacebookAuth);
reset(fakeGoogleSignIn);
final currentUser = FakeUser();
// It says that currentUser exists and *IS* authenticated
when(() => fakeFirebaseAuth.currentUser).thenReturn(currentUser);
when(() => firebaseAuthFacade.reloadCurrentUser())
.thenThrow(TimeoutException('timeout', const Duration(seconds: 20)));
final result = await firebaseAuthFacade.getSignedInUser();
expect(result, isA<Some<User>>());
});
Maybe it's better to remove timeout and use some connectivity package to ensure that the user has a network connection and only then reload the current user?
For testing I'm using mocktail package.

You can use the fake_async package.
Here's a simple example from their docs that you can modify for your use case:
import 'dart:async';
import 'package:fake_async/fake_async.dart';
import 'package:test/test.dart';
void main() {
test("Future.timeout() throws an error once the timeout is up", () {
// Any code run within [fakeAsync] is run within the context of the
// [FakeAsync] object passed to the callback.
fakeAsync((async) {
// All asynchronous features that rely on timing are automatically
// controlled by [fakeAsync].
expect(Completer().future.timeout(Duration(seconds: 5)),
throwsA(isA<TimeoutException>()));
// This will cause the timeout above to fire immediately, without waiting
// 5 seconds of real time.
async.elapse(Duration(seconds: 5));
});
});
}

Related

Npm fetch freezes program when I execute response clone

I have a function :
public async execute(): Promise<Response> {
const result = await fetch(this.url, this.params);
const result2 = result.clone(); // if I have this, my console.log(await result.json().message) won't get executed, if I remove this line it will be executed.
// if (result2.status !== 200) {
// logger.warn({
// msg: await result2.json(),
// function: `fetch-request result for ${this.url}, status: ${result.status}`,
// });
// }
return result;
}
And I use it:
public static async getSth() {
return await request.execute(); // I ommit imports and stuff here does not matter
}
const result = await getSth();
console.log(await result.json().message);
My problem is I want to log things when request status !== 200, but I also want to log the message after I consumed the body. So I thought I will use clone() and consume another body of the clone, but when I try this, my second console log does not even gets executed. Why ?

Cloud Function with Multiple Triggers - Scheduled and onCall

I have a cloud function that I would like to run whenever a user performs a set of actions on the web app AND daily at a specified time. In the interest of not duplicating code and future features/bug fixes, I'd like to run both from one function/file.
Any suggestions/references on this flow would be greatly appreciated!
You can write your business logic in one function, that you call from the two Cloud Functions. Something along the following lines, with an asynchronous business logic and the use of async/await:
exports.myFunctionCalledFromTheApp = functions.https.onCall(async (data, context) => {
try {
const result = await asyncBusinessLogic();
return { result: result }
} catch (error) {
// ...
}
});
exports.myFunctionCalledByScheduler = functions.pubsub.schedule('every 24 hours').onRun(async (context) => {
try {
await asyncBusinessLogic();
return null;
} catch (error) {
// ...
return null;
}
});
async function asyncBusinessLogic() {
const result = await anAsynchronousJob();
return result;
}

Flutter: How to Notify main widget after inserting all data into database

In my StatefulWidget in initState i have a method:
#override
void initState() {
super.initState();
getMyChannels();
}
getMyChannels method run a http method to get data from service and store data into database:
void getMyChannels() async {
// get data from servise and store them
_myChannel = await MyToolsProvider()
.getChannelMe("df6b88b6-f47d****");
getToolsRelToChannels(); // get data from database
setState(() {});
}
As you can see i have getToolsRelToChannels method. This method fetch data from local database. This data must be stored by await MyToolsProvider()
.getChannelMe("df6b88b6-f47d****"); method into database.
This is .getChannelMe method:
Future<ProgramsByActiveToolsModel> getChannelMe(String auth) async {
Map<String, dynamic> header = {
'Content-Type': "application/json",
"Authorization": 'Bearer $auth'
};
try {
var result = await NetworkCLient()
.getRequest(url: '$URL/api/me', header: header);
if (result != null) {
var programsByActiveToolsModel =
ProgramsByActiveToolsModel.fromJson(result);
if (programsByActiveToolsModel.responseCode == 200) {
programsByActiveToolsModel.data.forEach((item) async {
await DBProvider.db.addMyTools(item);
saveToolsbyChannelId(header, item.id);
});
return programsByActiveToolsModel;
} else
return null;
}
} catch (e) {
throw e;
}
}
In addMyTools method i store each data in one table of my database and i call saveToolsbyChannelId method for each item. This is main data that I need too.
Future<void> saveToolsbyChannelId(Map header, int channelId) async {
header["Authorization"] = 'Bearer 92122926-****';
try {
var count = await DBProvider.db.getCountToolsbyChannelId(channelId);
if (count == 0) {
var result = await NetworkCLient().getRequest(
url: '$URL/api/channel/$channelId', header: header);
if (result != null) {
var logToolsRunModel = LogTools.LogToolsRunModel.fromJson(result);
if (logToolsRunModel.responseCode == 200) {
logToolsRunModel.data.forEach((item) {
DBProvider.db.addTools(item);
});
}
}
}
} catch (e) {
throw e;
}
}
After fetching data from my service i sore these data into sqlite database .Now it's await MyToolsProvider().getChannelMe job is done!
It's time to explain getToolsRelToChannels();:
void getToolsRelToChannels() async {
_toolsRun =
await MyToolsProvider().getToolsRelatedMyChannel(_selectedChannel);
setState(() {});
}
getToolsRelatedMyChannel this method must be wait till all data in this method DBProvider.db.addTools(item) added into database and after inserting my widget must be recreated.
Future<List<ToolsByChannelIdDbModel>> getToolsRelatedMyChannel(
int channelId) async {
List<ToolsByChannelIdDbModel> list = List<ToolsByChannelIdDbModel>();
try {
var result = await DBProvider.db.getToolsById(channelId);
result.forEach((item) {
list.add(ToolsByChannelIdDbModel.fromJson(item));
});
return list;
} catch (e) {
print(e);
}
}
}
but my code is wrong because after running await MyToolsProvider().getChannelMe(***) getToolsRelToChannels method is executed and nothing is stored into database to fetching yet!!!
How could i notify my main widget after finishing database inserting???
I can not to use FutureBuilder because when run for first time, my database is empty !!!
You should await saveToolsbyChannelId in getChannelMe and await DBProvider.db.addTools(item); in saveToolsbyChannelId, otherwise you are trying to read from the database before the data has been written to it. This is assuming the rest of your code is correct, which we cannot tell for sure because there are lots of variables such as _selectedChannel that we know nothing about.
UPDATED - Check below.
What you want is to await ALL async operations. In your case
#override
void initState() async {
super.initState();
await getMyChannels();
}
and
await saveToolsbyChannelId(header, item.id);
and if DBProvider.db.addTools is asynchronous, then
await DBProvider.db.addTools(item);
UPDATE:
Since its not possible to make initState() async, you can use a callback in the future:
#override
void initState() {
super.initState();
var channelsFuture = getMyChannels();
channelsFuture.then((resp){
setState(() {});
});
}
I'd suggest that you reconsider your whole approach (from a clean architecture point of view). Take a look at the BLoC pattern. It basically boils down to this :
There's another class (called the BLoC) which handles the business logic (getting data from network and adding to the database). The widget class only handles the UI.
You kick off your asynchronous processing in the BLoC class from the widget's initState(). There's a stream listener in the widget that listens to the completion of the task by BLoC.
As soon as the BLoC completes the async task, it notifies the widget by using a stream. The stream listener in the widget knows that the data is ready and it updates the UI by calling setState();
Done !!

How to complete login only after functions.auth.user().onCreate is finished

I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?
Is it possible to finish login user (for example using google provider) only when all stuff from
export const createCollection = functions.auth.user().onCreate(async user => {
try {
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
return 'done';
}
async function addSecond() {
// ...
}
return await Promise.all([addLanguages, addSecondCollection]);
} catch (error) {
throw new functions.https.HttpsError('unknown', error);
}
});
is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)
AFAIK it is not possible to directly couple the two processes implied in your application:
On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
On the other hand you have the Cloud Function that is executed in the back-end.
The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.
In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.
One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().
firebase.auth().signInWithPopup(provider)
.then(function(result) {
var token = result.credential.accessToken;
var user = result.user;
//Here we know the userId then we can set a listener to the doc languages/${user.uid}
firebase.firestore().collection("languages").doc(user.uid)
.onSnapshot(function(doc) {
if(doc.exists) {
console.log("Current data: ", doc.data());
//Do whatever you want with the user doc
} else {
console.log("Language document not yet created by the Cloud Function");
}
});
}).catch(function(error) {
var errorCode = error.code;
var errorMessage = error.message;
var email = error.email;
var credential = error.credential;
// ...
});
As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.
So, you may modify you CF as follows:
export const createCollection = functions.auth.user().onCreate(async user => {
try {
await addFirst();
await addSecond();
return null;
async function addFirst() {
const userRef = admin.firestore().doc(`languages/${user.uid}`);
await userRef.set(
{
language: null
},
{ merge: true }
);
}
async function addSecond() {
// ...
}
} catch (error) {
console.log(error);
return null;
}
});
Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.
const addLanguages = await addFirst();
const addSecondCollection = await addSecond();
So you just need to set the listener on the path of the second document, and you are done!
Finally, note that doing
throw new functions.https.HttpsError('unknown', error);
in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;

How to listen for specific Firestore document creation event?

I am implementing a command/response pattern where the user writes to a command collection by calling add with a payload under his own userId, and then gets the data from a similar response path.
However the code below doesn't work, because onSnapshot can not listen for a document that hasn't yet been created (document command.id in the /responses/{userId}/register collection). This would be easy to solve with an onCreate handler, which exists for cloud functions but not for the JS firebase client API it seems.
This is using redux-firestore and some of my app helper functions, but you'll get the idea. The command and response document structures use { payload, error} similar to FSA
Cloud Function
export const register = functions.firestore
.document("commands/{userId}/register/{commandId}")
.onCreate(async event => {
const payload = event.data.get("payload");
const { userId, commandId } = event.params;
const response = db.document(`responses/${userId}/register/${commandId}`)
// possibly something bad will happen
try {
// do something with payload...
return response.set({
payload: "ok" // or pass relevant response data
})
} catch(err) {
return response.set({
error: true
payload: error
})
}
});
Client
export async function register(fs: any, userId: string) {
try {
// issue a new command
const command = await fs.add(
{ collection: `commands/${userId}/register` },
{ payload: fs.firestore.FieldValue.serverTimestamp() }
);
// wait for the response to be created
fs.onSnapshot(
{ collection: `responses/${userId}/register`, doc: command.id },
function onNext(doc) {
const {error, payload} = doc.data()
if (error) {
return notify.error({ title: 'Failed to register', message: payload.message });
}
notify.json(payload);
},
function onError(err) {
notify.error(err);
}
);
} catch (err) {
notify.error(err);
}
}
Is there no such thing as onCreate for web clients?
The only scalable solution I can think of is to store the response data as a child in the command document, but I think it is not as nice, because I suspect you can not make the permissions as strict then.
I would like the user only to be able to write to the command, and only read from the response paths. If I place the response as a child of command, this would not be possible I think?
I'm wondering if I'm not overlooking some API...

Resources