How to mock s3 putObject with promise function with jest - javascript

I see the answer here Mocking aws-sdk S3#putObject instance method using jest, but it's not working as-is and it's not explained at all so I don't know what's going on.
I have a function:
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
exports.saveImageToS3 = async (params) => {
try {
const s3resp = await s3.putObject(params).promise();
/* Happy path response looks like this:
data = {
ETag: "\"6805f2cfc46c0f04559748bb039d69ae\"",
VersionId: "psM2sYY4.o1501dSx8wMvnkOzSBB.V4a" // version optional
}*/
if (s3resp.hasOwnProperty('ETag')) { // expected successful response
return { success: true, key: params.Key } // returning key of item if we have reason to think this was successful.
} else {
console.warn("Unexpected s3 response format: ", s3resp)
return s3resp;
}
} catch (err) {
throw err;
}
}
I'd like to test if it works.
I do not understand how to mock the s3.putObject function.
I have tried:
describe('FUNCTION: saveImageToS3', () => {
const mockedPutObject = jest.fn();
jest.mock('aws-sdk', () => {
return class S3 {
putObject(params, cb) {
console.log("Mocked putObject function");
mockedPutObject(params, cb);
}
}
});
test('returns success object with correct key value', async () => {
await expect(await utils.saveImageToS3(params)).toEqual({ success: true, key: `original/testCamId/testCamId___2022-04-06T06-30-59Z.jpg` })
})
})
per the above-linked answer, but the test fails (times out, actually) and the output "Mocked putObject function" never is written to the console, telling me the mocked aws-sdk isn't being used...

Related

Writing tests for an async function with Jest

I am trying to test the following function:
import * as config from "./config.js";
export const state = {
recipe: {},
};
export async function loadRecipe(id) {
let result;
let data;
try {
result = await fetch(`${config.API_URL}/${id}`);
data = await result.json();
} catch (e) {
console.log(e);
}
console.log(result.status);
if (!result.status === 200) {
console.log("here");
throw new Error(`${data.message} (${result.status})`);
console.log("1here");
}
const { recipe } = data.data;
state.recipe = {
id: recipe.id,
title: recipe.title,
publisher: recipe.publisher,
sourceUrl: recipe.source_url,
image: recipe.image_url,
servings: recipe.servings,
cookingTime: recipe.cooking_time,
ingredients: recipe.ingredients,
};
}
Here are the tests I have written. I am using jest-fetch-mock to mock the global fetch function. If I comment-out the second test and run it, I get the expected results. Now I want to test if a bad id is entered. So I created a second test with bad data and am mocking the result from the API:
"use strict()";
import * as model from "../model.js";
import * as apiResponse from "../__fixtures__/apiResponse.js";
import * as recipes from "../__fixtures__/recipes.js";
beforeEach(() => {
fetch.resetMocks();
});
describe("Request from the api", () => {
test("Received valid data", async () => {
fetch.mockResponseOnce(
JSON.stringify(apiResponse.id_5ed6604591c37cdc054bca85)
);
const res = await model.loadRecipe("5ed6604591c37cdc054bca85");
expect(model.state.recipe).toStrictEqual(
recipes.recipe_5ed6604591c37cdc054bca85
);
expect(fetch).toHaveBeenCalledTimes(1);
});
test("Requested an invalid id", () => {
const body = apiResponse.invalid_5ed6604591c37cdc054bca85zzzzz;
const init = { status: 400, statusText: "Bad Request" };
fetch.mockResponseOnce(JSON.stringify(body), init);
expect(async () => {
await model.loadRecipe("5ed6604591c37cdc054bca85zzzzz");
}).toThrowError();
expect(fetch).toHaveBeenCalledTimes(1);
});
});
Whenever the second test is run I get the following error from yarn:
RUNS src/js/__tests__/model.test.js
node:internal/process/promises:225
triggerUncaughtException(err, true /* fromPromise */);
^
[UnhandledPromiseRejection: This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). The promise rejected with the reason "TypeError: Cannot destructure property 'recipe' of '((cov_24wkscmv5p(...).s[10]++) , data.data)' as it is undefined.".] {
code: 'ERR_UNHANDLED_REJECTION'
}
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
Please help me understand what is causing the issue.
Finally! I got it to work. I had to add rejects to catch the error. I got it from this page: https://eloquentcode.com/expect-a-function-to-throw-an-exception-in-jest
test("Requested an invalid id", () => {
const body = apiResponse.invalid_5ed6604591c37cdc054bca85zzzzz;
const init = { status: 400, statusText: "Bad Request" };
fetch.mockResponseOnce(JSON.stringify(body), init);
expect(async () => {
await model.loadRecipe("5ed6604591c37cdc054bca85zzzzz");
}).rejects.toThrowError();
expect(fetch).toHaveBeenCalledTimes(1);
});
Basically that error is due to your reject block/case.
So, when using async await, you could better keep it inside the try catch block to capture the reject case.
Consider following snippet for example -
var prom = function(p_param) {
return new Promise((resolve,reject) => {
setTimeout(()=>{
if(p_param%2 == 0){
resolve('Data good');
} else {
reject('Bad Data');
}
}, 3000);
});
}
async function runMain(p_data){
console.log('Verifying Data - '+p_data);
try {
var t = await prom(p_data); //resolve
console.log(t);
} catch(err) {
console.log(err); //reject
}
}
runMain(5);
This snippet would result in 'reject', Output:
Verifying Data - 5
Bad Data

Unable to return data from cloud function using https callable

I am calling the following cloud function from my Angular application, but the value returned is always null even though the cloud function is correctly logging the result. Not sure what I am doing incorrectly.
My Angular code is as follows:
const data = {test: testToProcess};
const process = this.fns.httpsCallable("gprocess"); // fns is Angular Fire Functions
process(data)
.toPromise() // since Angular Fire function returns an observable, Iam converting to a promise
.then(
(result) => {
console.log("function called: " + JSON.stringify(result));
},
(err) => {
console.log("Function call error " + JSON.stringify(err));
}
);
My cloud functions code is as follows:
import * as functions from "firebase-functions";
const fs = require("fs");
const { google } = require("googleapis");
const script = google.script("v1");
const scriptId = "MY_SCRIPT_ID";
export const gprocess = functions.https.onCall(async (data: any, context: any) => {
const test = data.test;
return fs.readFile("gapi_credentials.json", (err: any, content: string) => {
if (err) {return err;}
const credentials = JSON.parse(content); // load the credentials
const { client_secret, client_id, redirect_uris } = credentials.web;
const googleAuth = require("google-auth-library");
const functionsOauth2Client = new googleAuth.OAuth2Client(client_id, client_secret, redirect_uris); // Constuct an auth client
functionsOauth2Client.setCredentials({refresh_token: credentials.refresh_token,}); // Authorize a client with credentials
return runScript(functionsOauth2Client,scriptId,JSON.stringify(test))
.then((scriptData: any) => {
console.log("Script data in main function is" + JSON.stringify(scriptData));
return scriptData;
})
.catch((error) => {return error;});
});
});
function runScript(auth: any, scriptid: string, test: any) {
return new Promise(function (resolve, reject) {
script.scripts.run({
auth: auth,
scriptId: scriptid,
resource: {function: "doGet",devMode: true,parameters: test}
})
.then((err: any, respons: any) => {
if (err) {
console.log("API returned an error: " + JSON.stringify(err));
resolve(err);
} else if (respons) {
console.log("Script is run and response is " + JSON.stringify(respons));
resolve(respons);
}
});
});
}
The angular function is returning this result before the processing on the cloud function can be completed. IT is not waiting for the results to be returned from the cloud function.
detailed.component.ts:691 function called: null
After some time the results are logged on the cloud function console but this is not returned back to the angular client. The log on the cloud function is as follows and as shown below the correct result is logged:
5:53:32.633 PM gpublish Function execution started
5:53:32.694 PM gpublish Function execution took 61 ms, finished with status code: 204
5:53:33.185 PM gpublish Function execution started
5:53:33.804 PM gpublish Function execution took 620 ms, finished with status code: 200
5:54:31.494 PM gpublish Script is run and response is : {"config":... some result}
5:54:31.593 PM gpublish Script data in main function is{"config":... some result}
Please help!
Your function is not correctly returning a promise that resolves to the data to serialize and send to the client. The problem is that fs.readFile doesn't actually return a promise. It's asychronous and returns nothing, and that's what the client will receive. The callback is being invoked, but nothing inside that callback will affect what the client sees.
You will need to find another way of doing your file I/O that is either synchronous (such as fs.readFileSync), or actually works with promises instead of just an async callback.
I changed my code to use readFileSync as mentioned above (Thank you Doug!)
In addition, I stringified the data being sent back from the cloud function to the Angular client.
My cloud function now looks like this:
import * as functions from "firebase-functions";
const fs = require("fs");
const { google } = require("googleapis");
const script = google.script("v1");
const scriptId = "MY_SCRIPT_ID";
export const gprocess = functions.https.onCall(
async (data: any, context: any) => {
try {
const test = data.test;
const content = fs.readFileSync("credentials.json"); // CHANGED TO READFILESYNC
const credentials = JSON.parse(content);
const { client_secret, client_id, redirect_uris } = credentials.web;
const googleAuth = require("google-auth-library");
const functionsOauth2Client = ... some code to construct an auth client and authorise it
return runScript(functionsOauth2Client,scriptId,JSON.stringify(test)).then((scriptData: any) => {
return JSON.stringify(scriptData); // STRINGIFIED THE DATA
});
} catch (err) {
return JSON.stringify(err);
}
});
function runScript(auth: any, scriptid: string, test: any) {
return new Promise(function (resolve, reject) {
script.scripts.run({auth: auth,scriptId: scriptid,resource: {function: "doGet",parameters: test}})
.then((respons: any) => {resolve(respons.data);})
.catch((error: any) => {reject(error);});
});
}

How to fix ajv schema not being checked correctly while testing with Jest

Basically I am currently writing a unit test for a function which checks if a json-file is valid, using an AJV Schema. The problem is, that the checking against the schema works in the browser, but not in the test.
InvalidFileError
export class InvalidFileError extends Error {
constructor(message) {
super(message)
this.name = "InvalidFileError"
}
}
The function I'm trying to test
export function importFile(e, importScenarios, importDevices) {
const file = e.target.files[0]
const fileReader = new FileReader()
fileReader.readAsText(file)
fileReader.onload = () => { // File loaded
const fileContent = JSON.parse(fileReader.result)
const ajv = new Ajv({allErrors: true})
const validate = ajv.compile(schema)
const contentIsValid = validate(fileContent)
console.log("Content is valid: ", contentIsValid)
if (contentIsValid) {
importScenarios(fileContent.scenarios)
importDevices(fileContent.devices)
} else {
throw new InvalidFileError("This file doesn't match the schema")
}
}
}
The current test I have written
describe("Does Importing a file work properly?", () => {
let file
let e = {
target: {
files: []
}
}
let importScenarios = () => {}
let importDevices = () => {}
test("Does it work with a file matching the schema?", () => {
file = new Blob(correctTestContent, { type: "application/json" })
e.target.files.push(file)
expect(() => {
FileManager.importFile(e, importScenarios, importDevices)
}).not.toThrow(InvalidFileError)
})
test("Does it work with a file not matching the schema??", () => {
file = new Blob(incorrectTestContent, { type: "application/json" })
e.target.files.push(file)
expect(() => {
FileManager.importFile(e, importScenarios, importDevices)
}).toThrow(InvalidFileError)
})
afterEach(() => {
e.target.files = []
})
})
When I use this function in the browser, by uploading an invalid file, it throws an error, and it if i upload a valid file, it does not.
This should be the exact same in the test, but unfortunately it is not.
The problem is that the code you are trying to test is asynchronous, while the tests you have written are not.
When you run the tests, the onload callback of the FileReader is not being executed during the execution of the corresponding test. Instead, it is being called after the test has executed. In fact, because you have the statement:
console.log("Content is valid: ", contentIsValid)
inside the importFile method, you should be seeing in console a message like this one:
Cannot log after tests are done. Did you forget to wait for something async in your test?
You need to make your tests asynchronous, so that they wait for the onload callback execution. Unfortunately, your code is difficult to test as it is, because you have no way to know when the onload callback has been executed so it is also difficult to wait in the test until that moment.
One way to solve this problem would be to wrap your asynchronous code in a Promise and return it so that we can wait until the promise is finished. With this approach, your importFile would look something like:
export function importFile(e, importScenarios, importDevices) {
const file = e.target.files[0]
const fileReader = new FileReader()
fileReader.readAsText(file)
return new Promise((resolve, reject) => {
fileReader.onload = () => { // File loaded
const fileContent = JSON.parse(fileReader.result)
const ajv = new Ajv({allErrors: true})
const validate = ajv.compile(schema)
const contentIsValid = validate(fileContent)
if (contentIsValid) {
importScenarios(fileContent.scenarios)
importDevices(fileContent.devices)
resolve()
} else {
reject(new InvalidFileError("This file doesn't match the schema"))
}
}
});
}
Then, you can test this method by returning the Promise in the test (so that jest knows that it has to wait until the promise is resolved or rejected):
let importScenarios = jest.fn()
let importDevices = jest.fn()
test("Does it work with a file matching the schema?", () => {
expect.assertions(2);
file = new Blob(correctTestContent, { type: "application/json" })
e.target.files.push(file)
return FileManager.importFile(e, importScenarios, importDevices).then(() => {
expect(importScenarios).toHaveBeenCalledTimes(1);
expect(importDevices).toHaveBeenCalledTimes(1);
});
});
test('Does it work with a file not matching the schema??', () => {
expect.assertions(1);
file = new Blob(incorrectTestContent, { type: "application/json" })
e.target.files.push(file)
return FileManager.importFile(e, importScenarios, importDevices).catch((e) => {
expect(e).toBeInstanceOf(InvalidFileError);
});
});
Note that I have redefined the variables importScenarios and importDevices so that they are mock functions and we can check if they are called. Also, note the use of expect.assertions to verify that a certain number of assertions are called.
Lastly, take into account that if you redefine your importFile so that it returns a promise, you'll likely have to change the places where you call it to treat the rejection case. Where you have:
try {
FileManager.importFile(e, importScenarios, importDevices)
} catch(e) {
// Some treatment of your exception
}
you will need:
FileManager.importFile(e, importScenarios, importDevices).catch(e => {
// Some treatment of your exception
})

Jest testing mongoose model instantiation

I'm trying to test a REST API built with express and mongoose, I'm using jest and supertest for the http calls; also I'm relatively new to testing with javascript.
When testing a creation url I wan't to make sure the instantiation is called using just the req.body object but I'm not sure how to do it, after reading a lot about differences between mock objects and stubs and some of the Jest documentation my last try looks like this:
test('Should instantiate the model using req.body', done => {
const postMock = jest.fn();
const testPost = {
name: 'Test post',
content: 'Hello'
};
postMock.bind(Post); // <- Post is my model
// I mock the save function so it doesn't use the db at all
Post.prototype.save = jest.fn(cb => cb(null, testPost));
// Supertest call
request(app).post('/posts/')
.send(testPost)
.then(() => {
expect(postMock.mock.calls[0][0]).toEqual(testPost);
done();
})
.catch(err => {throw err});
});
Also I would like to know how to manually fail the test on the promise rejection, so it doesn't throws the Timeout - Async callback was not invoked within timeout specified by jasmine.DEFAULT_TIMEOUT_INTERVAL.
As it stands, you're performing more of a integration test rather than isolating the route handler function itself and testing just that.
First I would break away the handler for /posts/ to its own file (assuming you haven't done this already):
controllers/post-controller.js
const Post = require('./path/to/models/post')
exports.store = async (req, res) => {
const post = await new Post(req.body).save()
res.json({ data: post }
}
Next simply use the handler wherever you defined your routes:
const express = require('express')
const app = express()
const postController = require('./path/to/controllers/post-controller')
app.post('/posts', postController.store)
With this abstraction we can now isolate our postController.store and test that it works with req.body. Now since we need to mock mongoose to avoid hitting an actual database, you can create a mocked Post like so (using the code you already have):
path/to/models/__mocks__/post.js
const post = require('../post')
const mockedPost = jest.fn()
mockedPost.bind(Post)
const testPost = {
name: 'Test post',
content: 'Hello'
}
Post.prototype.save = jest.fn(cb => {
if (typeof cb === 'function') {
if (process.env.FORCE_FAIL === 'true') {
process.nextTick(cb(new Error(), null))
} else {
process.nextTick(cb(null, testPost))
}
} else {
return new Promise((resolve, reject) => {
if (process.env.FORCE_FAIL === 'true') {
reject(new Error())
} else {
resolve(testPost)
}
})
}
})
module.exports = mockedPost
Notice the check for process.env.FORCE_FAIL if for whatever reason you wanted to fail it.
Now we're ready to test that using the req.body works:
post-controller.test.js
// Loads anything contained in `models/__mocks__` folder
jest.mock('../location/to/models')
const postController = require('../location/to/controllers/post-controller')
describe('controllers.Post', () => {
/**
* Mocked Express Request object.
*/
let req
/**
* Mocked Express Response object.
*/
let res
beforeEach(() => {
req = {
body: {}
}
res = {
data: null,
json(payload) {
this.data = JSON.stringify(payload)
}
}
})
describe('.store()', () => {
test('should create a new post', async () => {
req.body = { ... }
await postController(req, res)
expect(res.data).toBeDefined()
...
})
test('fails creating a post', () => {
process.env.FORCE_FAIL = true
req.body = { ... }
try {
await postController.store(req, res)
} catch (error) {
expect(res.data).not.toBeDefined()
...
}
})
})
})
This code is untested, but I hope it helps in your testing.

Unit Testing redux async function with Jest

I'm pretty new to unit testing so please pardon any noobness.
I have a file api.js which has all the API call functions for the app. Each function returns its promise. Here's how it looks:
api.js
const api = {
getData() {
return superagent
.get(apiUrl)
.query({
page: 1,
});
},
}
Now coming to the redux async action that i'm trying to test. It looks something like this:
getDataAction.js
export function getData(){
return dispatch => {
api.getData()
.end((err, data) => {
if (err === null && data !== undefined) {
console.log(data);
} else if (typeof err.status !== 'undefined') {
throw new Error(`${err.status} Server response failed.`);
}
});
}
}
Now, In my test file, I've tried this:
getDataAction.test.js
jest.mock('api.js');
describe('getData Action', () => {
it('gets the data', () => {
expect(store.dispatch(getData())).toEqual(expectedAction);
});
});
This, throws me an error:
TypeError: Cannot read property 'end' of undefined
What am I doing wrong ? Now i'm able to mock api.js with default automocker of Jest, but how do I handle the case of running callback function with end ? Thanks a lot for any help !
Your mock of api needs to return a function that returns an object that has the end function:
import api from 'api' //to set the implantation of getData we need to import the api into the test
// this will turn your api into an object with the getData function
// initial this is just a dumb spy but you can overwrite its behaviour in the test later on
jest.mock('api.js', ()=> ({getData: jest.fn()}));
describe('getData Action', () => {
it('gets the data', () => {
const result = {test: 1234}
// for the success case you mock getData so that it returns the end function that calls the callback without an error and some data
api.getData.mockImplementation(() => ({end: cb => cb(null, result)}))
expect(store.dispatch(getData())).toEqual(expectedAction);
});
it('it thows on error', () => {
// for the error case you mock getData so that it returns the end function that calls the callback with an error and no data
api.getData.mockImplementation(() => ({end: cb => cb({status: 'someError'}, null)}))
expect(store.dispatch(getData())).toThrow();
});
});

Categories

Resources