I've written testcase for the below code where we try to delete the genre document if present in the DB, otherwise throw error with status code 404.
Code:
router.delete('/:id',[endpoint_middleware,admin_middleware], async (req, res) => {
const doc_to_delete = await CollectionClass.findByIdAndRemove(req.params.id);
if (!doc_to_delete) return res.status(404).send('The genre with the given ID was not found.');
res.send(doc_to_delete);
});
Testcase:
describe('Delete genre', () => {
let token;
let curr_genre;
let id;
beforeEach(async () => {
curr_genre = new CollectionClass({name: 'genre1'});
await curr_genre.save();
id = curr_genre._id;
token = new UserCollectionClass({isAdmin : true}).generateToken();
});
const delete_genre = async () => {
return await supertest(server)
.delete('/api/genres/'+id)
.set('x-jwtInHeader',token)
.send();
};
it('return error for deleting invalid id', async () => {
id = 1;
const response = await delete_genre();
expect(response.status).toBe(404);
});
});
The expected response status is 404 since the id is invalid. But the received response status is 500. Other test scenarios work as expected. Kindly help.
Looks like the issue can be related to tests being not strictly isolated. Lets just try to debug a bit and improve the current code.
you have your delete handler, but I see it is error-prone as await operation is not handled well.
can you please try to replace it with this code and run tests again, you should see the exact error
router.delete("/:id", [endpoint_middleware, admin_middleware], async (req, res) => {
try {
const doc_to_delete = await CollectionClass.findByIdAndRemove(req.params.id);
if (!doc_to_delete) return res.status(404).send("The genre with the given ID was not found.");
res.send(doc_to_delete);
} catch (error) {
console.log(error)
throw new Error(error)
}
});
Related
I am trying to mock the implementation of an asynchronous function.
While writing the expect statements,
describe('Test all behaviours for ProductController.fetchProductByName', () => {
it('Should fetch product when called with valid Product Name', async() => {
req.params.name = 'ValidCategoryName';
const spy = jest.spyOn(ProductRepository, 'fetchProductsByCriteria')
.mockImplementation(({}) => Promise.resolve(newProduct))
await ProductController.fetchProuctsByName(req, res);
expect(spy).toHaveBeenCalledWith({
where : {
name: req.params.name
}
});
expect(ProductRepository.fetchProductsByCriteria).toHaveBeenCalled();
expect(res.status).toHaveBeenCalledWith(200);
expect(res.send).toHaveBeenCalledWith(newProduct);
});
it('Should send 500 as status, when there is some error in fetch Product by name', async() => {
req.params.name = 'AnyRandomName';
const spy = jest.spyOn(ProductRepository, 'fetchProductsByCriteria')
.mockRejectedValue(new Error('Error in processing request!'));
await ProductController.fetchProuctsByName(req, res);
await expect(spy).toHaveBeenCalled(); // --- this case
expect(res.status).toHaveBeenCalledWith(500);
expect(res.send).toHaveBeenCalledWith({message: 'Error occured in proccessing the request, Please try again after sometime!'});
});
})
The expect(res.status) function works fine even without awaiting the expect(spy) in the first case, but it fails in the second scenario.
Can someone please help me understand why this is the case?
I hope you're having a good day.
First of all, I'm sorry if you think this is a stupid question but I'm genuinely curious as to what you do in your own projects or let's say in a production environment.
So I've been using express for quite some time now and I usually send my errors back to the client manually in each request handler, when something isn't expected or an error occurs as such:
userController.js
const ERRORS = require('../utils/errors.js');
const userController = {
updateUser: (req, res) => {
let id = req.params.id;
if(!validUserId(id))
return res.status(400).json({error:ERRORS.INVALID_USER_ID});
let user = await findUserById(id);
if(!user)
return res.status(404).json({error: ERRORS.USER_NOT_FOUND});
try {
let updatedUser = await updateUser(id);
return res.status(201).json({data: updatedUser});
}
catch(e){
return res.status(500).json({error:ERRORS.FAILED_UPDATE_USER});
}
}
}
As you can see this can get really unmanageable really quickly and just overall makes the code less readable, especially if you're doing this for every request.
My question here is: does using a catch all global error handler middleware slow down express or create potential problems I'm not aware of or is it actually a suggested approach by some?
The code would then look like:
errorHandler.js
const errorHandler = (err, req, res, next) => {
// assume this function returns details about an error given an error message
// such as if the custom error message is USER_NOT_FOUND
// => it returns {status:404, message: "User not found"}
const {status, message} = getErrorDetails(err.message);
if(!res.headersSent)
res.status(status).json({error: message})
}
userController.js
const ERRORS = require('../utils/errors.js');
const userController = {
updateUser: (req, res) => {
let id = req.params.id;
if(!validUserId(id))
throw new Error(ERRORS.INVALID_USER_ID);
let user = await findUserById(id);
if(!user)
throw new Error(ERRORS.USER_NOT_FOUND);
try {
let updatedUser = await updateUser(id);
return res.status(201).json({data: updatedUser});
}
catch(e){
throw new Error(ERRORS.FAILED_UPDATE_USER);
}
}
}
Thank you. Your insights are very much appreciated.
I'm new to fetching and posting data using an API, and I can't work out how to do something once my Post has been completed.
I have a function that calls the API with the Post data. I need to set the loading state to false once the Post has been completed. Everything works apart from that, the data gets sent to Mongo, I just need to turn off my loading spinner once it has completed.
How do I do this, please?
This is how I'm trying to do it:
const postData = async () => {
setLoading(true)
await axios.post('/api/addData',form)
.then(response => {
setLoading(false)
})
}
And this is the API bit:
import { connectToDatabase } from "util/mongodb"
export default async (req, res) => {
const { db } = await connectToDatabase()
await db
.collection("posts")
.insertOne(req.body);
}
There is two potential problem in your code, first you're not sending any data back to the front in your backend code. Usually you send back the id of the inserted element (It can be usefull to do some mutation in your front), you'll also need to try catch your call to the db to notify that something went wrong to the front end side :
import { connectToDatabase } from "util/mongodb"
export default async (req, res) => {
try {
const { db } = await connectToDatabase()
const insertedPost = await db
.collection("posts")
.insertOne(req.body);
res.status(201).send(insertedPost.insertedId);
// again it's up to you to know what can be usefull to your front-end to use
// Look at http status code online to know what's the best fit
} catch (err) {
res.status(500).send(err.message);
// send whatever that can be usefull for your front end to handle the error
}
}
In your front-end code you're using await with .then, it's weird usage. You can put your setLoading(false) after the await without the .then but you'll still need to try catch it. What I prefer to do is using the finally block to stop loading, so if my api call fail the loading is still stopped :
const postData = async () => {
setLoading(true)
try {
const response = await axios.post('/api/addData',form)
// do something with response
} catch (err) {
// notify user that something went wrong
} finally {
setLoading(false);
}
}
const postData = () => {
setLoading(true)
axios.post('/api/addData',form)
.then(response => {
// do something with response
})
.catch((err) => {
// notify user that something went wrong
})
.finally(() => {
setLoading(false);
})
}
Building a NodeJS REST API.
Trying to send load data from FireBase collection, then sending it to the user (as API response).
Looks like the problem is that it's not waits for the firebase fetch to resolve, but send back a response without the collection data. (tried to use ASYNC-AWAIT but its not working)
exports.getChatMessages = async (req, res, next) => {
const chatId = req.params.chatId
const getChatData = () => {
db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot((snapshot) => {
snapshot.docs.forEach(msg => {
console.log(msg.data().messageContent)
return {
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
}
})
})
}
try {
const chatData = await getChatData()
console.log(chatData)
res.status(200).json({
message: 'Chat Has Found',
chatData: chatData
})
} catch (err) {
if (!err.statusCode) {
err.statusCode(500)
}
next(err)
}
}
As you can see, I've used 2 console.logs to realize what the problem, Terminal logs looks like:
[] (from console.logs(chatData))
All messages (from console.log(msg.data().messageContent))
Is there any way to block the code unti the firebase data realy fetched?
If I correctly understand, you want to send back an array of all the documents present in the messages subcollection. The following should do the trick.
exports.getChatMessages = async (req, res, next) => {
const chatId = req.params.chatId;
const collectionRef = db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc');
try {
const chatsQuerySnapshot = await collectionRef.get();
const chatData = [];
chatsQuerySnapshot.forEach((msg) => {
console.log(msg.data().messageContent);
chatData.push({
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
});
});
console.log(chatData);
res.status(200).json({
message: 'Chat Has Found',
chatData: chatData,
});
} catch (err) {
if (!err.statusCode) {
err.statusCode(500);
}
next(err);
}
};
The asynchronous get() method returns a QuerySnapshot on which you can call forEach() for enumerating all of the documents in the QuerySnapshot.
You can only await a Promise. Currently, getChatData() does not return a Promise, so awaiting it is pointless. You are trying to await a fixed value, so it resolves immediately and jumps to the next line. console.log(chatData) happens. Then, later, your (snapshot) => callback happens, but too late.
const getChatData = () => new Promise(resolve => { // Return a Promise, so it can be awaited
db.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot(resolve) // Equivalent to .onSnapshot((snapshot) => resolve(snapshot))
})
const snapshot = await getChatData();
console.log(snapshot)
// Put your transform logic out of the function that calls the DB. A function should only do one thing if possible : call or transform, not both.
const chatData = snapshot.map(msg => ({
authorID: msg.data().authorID,
messageContent: msg.data().messageContent,
timeStamp: msg.data().timeStamp,
}));
res.status(200).json({
message: 'Chat Has Found',
chatData
})
Right now, getChatData is this (short version):
const getChatData = () => {
db
.collection('chats')
.doc(chatId)
.collection('messages')
.orderBy('timeStamp', 'asc')
.onSnapshot((snapshot) => {}) // some things inside
}
What that means is that the getChatData function calls some db query, and then returns void (nothing). I bet you'd want to return the db call (hopefully it's a Promise), so that your await does some work for you. Something along the lines of:
const getChatData = async () =>
db
.collection('chats')
// ...
Which is the same as const getChatData = async() => { return db... }
Update: Now that I've reviewed the docs once again, I see that you use onSnapshot, which is meant for updates and can fire multiple times. The first call actually makes a request, but then continues to listen on those updates. Since that seems like a regular request-response, and you want it to happen only once - use .get() docs instead of .onSnapshot(). Otherwise those listeners would stay there and cause troubles. .get() returns a Promise, so the sample fix that I've mentioned above would work perfectly and you don't need to change other pieces of the code.
I've been using firebase functions test to do some testing on my functions. I have some code that is supposed to post a thing to firestore, basically in the same way that the examples show to do in the realtime database examples:
exports.addMessage = functions.https.onRequest((req, res) => {
const original = req.query.text;
admin.firestore()
.collection('messages')
.add({ original })
.then(documentReference => res.send(documentReference))
.catch(error => res.send(error));
});
For my test, I've spoofed some basic functionality using sinon, mocha and chai. Here is my current test, which is failing with the error message: TypeError: firestoreService.snapshot_ is not a function
describe('addMessage', () => {
// add message should add a message to the database
let oldDatabase;
before(() => {
// Save the old database method so it can be restored after the test.
oldDatabase = admin.firestore;
});
after(() => {
// Restoring admin.database() to the original method.
admin.firestore = oldDatabase;
});
it('should return the correct data', (done) => {
// create stubs
const refStub = sinon.stub();
// create a fake request object
const req = {
query : {
text: 'fly you fools!'
}
};
const snap = test.firestore.makeDocumentSnapshot({ original: req.query.text }, 'messages/1234');
// create a fake document reference
const fakeDocRef = snap._ref;
// create a fake response object
const res = {
send: returnedDocRef => {
// test the result
assert.equal(returnedDocRef, fakeDocRef);
done();
}
};
// spoof firestore
const adminStub = sinon.stub(admin, 'firestore').get(() => () => {
return {
collection: () => {
return {
add: (data) => {
const secondSnap = test.firestore.makeDocumentSnapshot(data, 'messages/1234');
const anotherFakeDocRef = secondSnap._ref;
return Promise.resolve(anotherFakeDocRef);
}
}
}
}
});
// call the function to execute the test above
myFunctions.addMessage(req, res);
});
});
My question is how the heck do I fix this?
I previously had a test that was just passing the first snap and fakeDocRef, and my test was passing fine, but as soon as I resolve the promise with the new fake document reference, it fails...
Any help would be appreciated! Thanks!
There are three different types of the calls, that are different:
Operating on the Collections.
Operating on the Documents.
Operating on the results of the query.
They have to be used consistently.
Please refer a documentation to see the difference operation on the collection and the document.