How to use Sinon to stub out knex calls in Hapi/Lab test? - sinon

I'm trying to set up a testing pattern for a new Hapi app. I've used Mocha and Chai in the past with Express, but I'm trying to use Lab and Code to stay in the Hapi ecosystem. I'm also using Bookshelf and Knex to handle database interaction.
So I have a simple health endpoint I'd like to test.
'use strict';
const controller = require('../controller/healthController');
module.exports = [
{
method: 'GET',
path: '/health',
config: {
handler: controller.health,
description: 'The health endpoint returns 200',
tags: ['api', 'health']
}
}
];
In the handler it just does a quick query to make sure it can connect to the DB.
'use strict';
const bookshelf = require('../config/db');
const knex = bookshelf.knex;
module.exports = {
health: function (request, reply) {
knex.raw('SELECT version()').then(() => {
reply('service is running');
}).catch((err) => {
reply({err: err, code: 500});
});
}
};
As far as I understand it, requiring the server and then using server.inject doesn't actually start the server so I don't believe I should have a db connection, which would mean I should have to mock it out the db call. What is odd to me is that this test passes:
'use strict';
const Code = require('code');
const Lab = require('lab');
const lab = exports.lab = Lab.script();
const describe = lab.describe;
const it = lab.test;
const expect = Code.expect;
const before = lab.before;
let server;
describe('health controller', () => {
before((done) => {
server = require('../../server');
done();
});
it('health check replies 200 when successful call to db', (done) => {
const options = {
method: 'GET',
url: '/health'
};
server.inject(options, (res) => {
expect(res.payload).to.include('is running');
expect(res.statusCode).to.equal(200);
done();
});
});
});
So I have two problems. First, I feel like the test above shouldn't really pass. Unless it's loading everything up and thus connecting to the db I suppose. Maybe I should be testing just the controller/handler method? But I haven't found any examples of that.
Second, I've tried to stub out the knex.raw call anyway and when I try to do it like below I get a 500 error.
'use strict';
const Code = require('code');
const Lab = require('lab');
const Sinon = require('sinon');
const lab = exports.lab = Lab.script();
const describe = lab.describe;
const it = lab.test;
const expect = Code.expect;
const before = lab.before;
let server;
let knex = require('../../app/config/db').knex;
describe('health controller', () => {
before((done) => {
server = require('../../server');
done();
});
it('health check replies 200 when successful call to db', (done) => {
const stub = Sinon.stub(knex, 'raw').returns({});
const options = {
method: 'GET',
url: '/health'
};
server.inject(options, (res) => {
expect(res.payload).to.include('is running');
expect(res.statusCode).to.equal(200);
expect(stub.calledOnce).to.be.true();
done();
});
});
});
I'm not really sure why that's happening.

I think Sinon.stub(knex, 'raw').resolves({}); is a better solution

server.inject works exactly as if you made a request to a real server. So if your database is up when the test are run, the endpoint will return data from the database just like it does when you start the server manually.
Sinon.stub(knex, 'raw').returns({}); won't work. knex.raw(…) is expected to return a Promise, not an empty object. Please try the following:
Sinon.stub(knex, 'raw').returns(Promise.resolve({}));
Just a side note: Remember to call server.stop() after each test in order to ensure there's no state persisted between tests. In general, I think you can take a look at example test files in Hapi University repository.

Related

Asp.Net Core Web Api project is blocking calls from front end Vue.Js project (Cors Error) [duplicate]

I have an app made with React, Node.js and Socket.io
I deployed Node backend to heroku , frontend to Netlify
I know that CORS errors is related to server but no matter what I add, it just cant go through that error in the picture below.
I also added proxy script to React's package.json as "proxy": "https://googledocs-clone-sbayrak.herokuapp.com/"
And here is my server.js file;
const mongoose = require('mongoose');
const Document = require('./Document');
const dotenv = require('dotenv');
const path = require('path');
const express = require('express');
const http = require('http');
const socketio = require('socket.io');
dotenv.config();
const app = express();
app.use(cors());
const server = http.createServer(app);
const io = socketio(server, {
cors: {
origin: 'https://googledocs-clone-sbayrak.netlify.app/',
methods: ['GET', 'POST'],
},
});
app.get('/', (req, res) => {
res.status(200).send('hello!!');
});
const connectDB = async () => {
try {
const connect = await mongoose.connect(process.env.MONGODB_URI, {
useUnifiedTopology: true,
useNewUrlParser: true,
});
console.log('MongoDB Connected...');
} catch (error) {
console.error(`Error : ${error.message}`);
process.exit(1);
}
};
connectDB();
let defaultValue = '';
const findOrCreateDocument = async (id) => {
if (id === null) return;
const document = await Document.findById({ _id: id });
if (document) return document;
const result = await Document.create({ _id: id, data: defaultValue });
return result;
};
io.on('connection', (socket) => {
socket.on('get-document', async (documentId) => {
const document = await findOrCreateDocument(documentId);
socket.join(documentId);
socket.emit('load-document', document.data);
socket.on('send-changes', (delta) => {
socket.broadcast.to(documentId).emit('receive-changes', delta);
});
socket.on('save-document', async (data) => {
await Document.findByIdAndUpdate(documentId, { data });
});
});
console.log('connected');
});
server.listen(process.env.PORT || 5000, () =>
console.log(`Server has started.`)
);
and this is where I make request from frontend;
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
import { useParams } from 'react-router-dom';
import { io } from 'socket.io-client';
const SAVE_INTERVAL_MS = 2000;
const TextEditor = () => {
const [socket, setSocket] = useState();
const [quill, setQuill] = useState();
const { id: documentId } = useParams();
useEffect(() => {
const s = io('https://googledocs-clone-sbayrak.herokuapp.com/');
setSocket(s);
return () => {
s.disconnect();
};
}, []);
/* below other functions */
/* below other functions */
/* below other functions */
}
TL;DR
https://googledocs-clone-sbayrak.netlify.app/ is not an origin. Drop that trailing slash.
More details about the problem
No trailing slash allowed in the value of the Origin header
According to the CORS protocol (specified in the Fetch standard), browsers never set the Origin request header to a value with a trailing slash. Therefore, if a page at https://googledocs-clone-sbayrak.netlify.app/whatever issues a cross-origin request, that request's Origin header will contain
https://googledocs-clone-sbayrak.netlify.app
without any trailing slash.
Byte-by-byte comparison on the server side
You're using Socket.IO, which relies on the Node.js cors package. That package won't set any Access-Control-Allow-Origin in the response if the request's origin doesn't exactly match your CORS configuration's origin value (https://googledocs-clone-sbayrak.netlify.app/).
Putting it all together
Obviously,
'https://googledocs-clone-sbayrak.netlify.app' ===
'https://googledocs-clone-sbayrak.netlify.app/'
evaluates to false, which causes the cors package not to set any Access-Control-Allow-Origin header in the response, which causes the CORS check to fail in your browser, hence the CORS error you observed.
Example from the Fetch Standard
Section 3.2.5 of the Fetch Standard even provides an enlightening example of this mistake,
Access-Control-Allow-Origin: https://rabbit.invalid/
and explains why it causes the CORS check to fail:
A serialized origin has no trailing slash.
Looks like you haven't imported the cors package. Is it imported anywhere else?
var cors = require('cors') // is missing

firebase functions, cors-anywhere and uploading

I am using cors-anywhere within the firebase functions platform and it works perfectly for my needs, until i need to upload a file via the proxy to an external address.
From what I have gathered over struggling with this for a few days this is due to the way that firebase parses the request body, specifically the multipart/form-data. I have seen examples of people using busboy or multer to extract the buffers to store in firebase storage or similar, but not any examples that would allow the function to reverse proxy the file through to the targetted address.
I am quite desperate to come up with a solution for this and am at my wits end.
for my firebase function (working great for everything except posting multipart/form data):
const { app } = require("firebase-admin");
const functions = require("firebase-functions");
const cors_proxy = require('cors-anywhere').createServer({
originWhitelist: [], // Allow all origins temporarily whilst in development
requireHeader: [],
removeHeaders: [],
redirectSameOrigin: true,
httpProxyOptions: {
xfwd: false,
},
});
exports.app = functions.https.onRequest(function(req, res) {
functions.logger.info('method', req.method);
functions.logger.info('raw body', req.rawBody);
functions.logger.info('body', req.body);
req.url = '/https:/' + req.url;
functions.logger.info(req.url);
cors_proxy.emit('request', req, res);
});
and the method i am using to send from my client application:
static UploadItems$ = (reviewId, files, artist, description) => {
const progressSubscriber = new Subject();
const request$ = of(files).pipe(
concatAll(),
switchMap(f => {
const formdata = new FormData();
console.log(f.name, f);
formdata.append("reviewFile", f, f.name);
formdata.append("artist", artist);
formdata.append("description", description);
return ajax({
url: SyncsketchPosts.UploadFile(reviewId),
method: 'POST',
headers: UploadHeaders,
body: formdata,
crossDomain: true
//progressSubscriber,
})
}),
tap(console.log),
).subscribe(res => console.log(res))
}

Mock function in Firebase local emulator

Such as described here, I'm using local emulator (on-line) to make tests im my cloud functions.
Index.js:
var status = 200;
exports.saveAndSendMail = functions.https.onCall( async (req, res) => {
try{
let jsons = req.body;
await saveInfirestore(jsons);
await sendMail("Data saved", jsons);
} finally {
closeConnection(res, status);
}
async function saveInfirestore(json) {
//execute business logic and save in firestore (irrelevant for this question)
}
function closeConnection (res, status){
res.sendStatus(status);
res.end();
}
async function sendMail(title, message) {
try {
AWS.config.loadFromPath('./config_mail.json');
// Create sendEmail params
var params = {
Destination: {
ToAddresses: [
'mymail#gmail.com'
]
},
Message: { /* required */
Body: { /* required */
Html: {
Charset: "UTF-8",
Data: JSON.stringfy(message);
}
},
Subject: {
Charset: 'UTF-8',
Data: title
}
},
Source: '"Origin" <origin#gmail.com>',
ReplyToAddresses: [
'origin#gmail.com'
]
};
// Create the promise and SES service object
var sendPromise = new AWS.SES({apiVersion: '2022-17-01'}).sendEmail(params).promise();
}
catch(e){
throw e;
}
// Handle promise's fulfilled/rejected states
sendPromise.then(
function(data) {
console.log(data.MessageId);
}).catch(
function(err) {
console.error(err, err.stack);
});
}
index.test.js
const { expect } = require("chai");
const admin = require("firebase-admin");
const test = require("firebase-functions-test")({
projectId: process.env.GCLOUD_PROJECT,
});
const myFunctions = require("../index");
describe("Unit tests", () => {
after(() => {
test.cleanup();
});
it("test if save is correct", async () => {
const wrapped = test.wrap(myFunctions.saveAndSendMail);
const req = {
body: [{
value: 5,
name: 'mario'
}]
};
const result = await wrapped(req);
let snap = await db.collection("collection_data").get();
expect(snap.size).to.eq(1);
snap.forEach(doc => {
let data = doc.data();
expect(data.value).to.eql(5);
expect(data.name).to.eql('mario');
});
});
I execute it with: firebase emulators:exec "npm run test"
I have 2 problems.
1 - When execute, I receive the error TypeError: res.sendStatus is not a function. If I comment closeConnection call in block finally (index.js), this code run perfectly and all tests and "expect" run with success. But, this correct way is mock this method or mock 'res' calls. I tried mock with something like this:
const res = {
sendStatus: (status) => {
},
end: () => {
}
}
const result = await wrapped(req, res);
But, I receive this error:
Error: Options object {} has invalid key "sendStatus"
at /home/linuxuser/my-project/firebase/functions/myfolder/node_modules/firebase-functions-test/lib/main.js:99:19
at Array.forEach (<anonymous>)
at _checkOptionValidity (node_modules/firebase-functions-test/lib/main.js:97:26)
at wrapped (node_modules/firebase-functions-test/lib/main.js:57:13)
at Context.<anonymous> (test/index.test.js:50:26)
at processImmediate (node:internal/timers:464:21)
Problem 2:
I'm not wish receive an e-mail each time that tests executes. How I mock sendMail function?
Something very important to point out is that you are currently trying to use a Firebase callable function, as shown by the function heading functions.https.onCall(() => {});. Since you want to work with requests and response codes, the correct type of function to use is an HTTP function. You would only need to change the heading in your index.js:
exports.saveAndSendMail = functions.https.onRequest(async (req, res) => {
// function body
});
Now, your first problem can then be solved by correctly mocking the res object that is passed to the function (inside index.test.js). When testing HTTP functions, you must not use test.wrap() when calling the function, nor expect the result as you were doing with const result = await wrapped(req); This is since Wrap being only supported for testing onCall functions. You can see another snippet of how to call an HTTP function for testing in the documentation.
it("test if save is correct", async () => {
const req = {
body: [{
value: 5,
name: 'mario'
}]
};
// mocking the response object that is returned from the function:
const res = {
sendStatus: (code) => {
expect(code).to.eql(200); // asserting that we get 200 back as the response code
},
end: () => {
}
};
const result = await myFunctions.saveAndSendMail(req, res); // mocking a call to an HTTP function, without test.wrap()
// rest of the function…
For your second problem, I haven’t used AWS SES before, but it seems this library offers ways to mock the functions so that you won’t have to actually send emails during your tests.

excel4node fetch request with next.js api routes not triggering download

I am generating an excel file and want it to be downloaded for the client by triggering an API route using the Next.js framework. I am having trouble triggering the download by using fetch. The download can be triggered by window.open(//urlhere, '_self') but the API call using fetch gives this response on request:
API resolved without sending a response for /api/download?Students= this may result in stalled requests.
The excel4node documentation says we can send an excel document through an API like this:
// sends Excel file to web client requesting the / route
// server will respond with 500 error if excel workbook cannot be generated
var express = require('express');
var app = express();
app.get('/', function(req, res) {
wb.write('ExcelFile.xlsx', res);
});
app.listen(3000, function() {
console.log('Example app listening on port 3000!');
});
Here is my backend download.js which lives in pages/api/:
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import Link from "next/link";
import getPartners from "../../components/main";
import excel from "excel4node";
export default function handler(req, res) {
const students = req.query.Students.split(",");
const partners = JSON.stringify(getPartners(students));
let workbook = createExcelList(partners);
workbook.write("PartnerList.xlsx", res);
}
const createExcelList = (partnersJSON) => {
const workbook = new excel.Workbook();
const partnersObject = JSON.parse(partnersJSON);
/* Using excel4node a workbook is generated formatted the way I want */
return workbook;
};
export const config = {
api: {
bodyParser: true,
},
};
And here is the function that is triggered on a button press in the front end.
const makePartners = async () => {
let queryStudents = studentList.join(",");
const url = "http://localhost:3000/api/download?Students=" + queryStudents;
if (studentList.length !== 0) {
try {
const res = await fetch(url, {
headers: {
"Content-Type":
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
},
});
console.log(res);
} catch (e) {
console.log(e);
}
}
};
Which does not trigger the download. But using window.open(url, '_self) does. So, I can trigger the download by changing the function to the following. However I don't think this is the correct way of doing things and would like to be able to understand how to use fetch correctly.
const makePartners = () => {
let queryStudents = studentList.join(",");
const url = "http://localhost:3000/api/download?Students=" + queryStudents;
if (studentList.length !== 0) {
window.open(url, "_Self");
}
};
I am not sure if this is a Next.js issue or not. Does anyone have any insight? Any help would be appreciated.

How do you unit test a firebase function wrapped with express?

With firebase functions, you can utilize express to achieve nice functionality, like middleware etc. I have used this example to get inspiration of how to write https firebase functions, powered by express.
However, my issue is that the official firebase documentation on how to do unit testing, does not include a https-express example.
So my question is how to unit test the following function (typescript)?:
// omitted init. of functions
import * as express from 'express';
const cors = require('cors')({origin: true});
const app = express();
app.use(cors);
// The function to test
app.get('helloWorld, (req, res) => {
res.send('hello world');
return 'success';
});
exports.app = functions.https.onRequest(app);
This works with Jest
import supertest from 'supertest'
import test from 'firebase-functions-test'
import sinon from 'sinon'
import admin from 'firebase-admin'
let undertest, adminInitStub, request
const functionsTest = test()
beforeAll(() => {
adminInitStub = sinon.stub(admin, 'initializeApp')
undertest = require('../index')
// inject with the exports.app methode from the index.js
request = supertest(undertest.app)
})
afterAll(() => {
adminInitStub.restore()
functionsTest.cleanup()
})
it('get app', async () => {
let actual = await request.get('/')
let { ok, status, body } = actual
expect(ok).toBe(true)
expect(status).toBeGreaterThanOrEqual(200)
expect(body).toBeDefined()
})
You can use supertest paired with the guide from Firebase. Below is a very basic example of testing your app, however, you can make it more complex/better by integrating mocha.
import * as admin from 'firebase-admin'
import * as testFn from 'firebase-functions-test'
import * as sinon from 'sinon'
import * as request from 'supertest'
const test = testFn()
import * as myFunctions from './get-tested' // relative path to functions code
const adminInitStub = sinon.stub(admin, 'initializeApp')
request(myFunctions.app)
.get('/helloWorld')
.expect('hello world')
.expect(200)
.end((err, res) => {
if (err) {
throw err
}
})
Would something like mock-express work for you? It should allow you to test paths without actually forcing you to make the express server.
https://www.npmjs.com/package/mock-express
I've gotten this to work using firebase-functions-test and node-mocks-http.
I have this utility class FunctionCaller.js :
'use strict';
var httpMocks = require('node-mocks-http');
var eventEmitter = require('events').EventEmitter;
const FunctionCaller = class {
constructor(aYourFunctionsIndex) {
this.functions_index = aYourFunctionsIndex;
}
async postFunction(aFunctionName,aBody,aHeaders,aCookies) {
let url = (aFunctionName[0]=='/') ? aFunctionName : `/${aFunctionName}`;
let options = {
method: 'POST',
url: url,
body: aBody
};
if (aHeaders)
options.headers = aHeaders;
if (aCookies) {
options.cookies = {};
for (let k in aCookies) {
let v = aCookies[k];
if (typeof(v)=='string') {
options.cookies[k] = {value: v};
} else if (typeof(v)=='object') {
options.cookies[k] = v;
}
}
}
var request = httpMocks.createRequest(options);
var response = httpMocks.createResponse({eventEmitter: eventEmitter});
var me = this;
await new Promise(function(resolve){
response.on('end', resolve);
if (me.functions_index[aFunctionName])
me.functions_index[aFunctionName](request, response);
else
me.functions_index.app(request, response);
});
return response;
}
async postObject(aFunctionName,aBody,aHeaders,aCookies) {
let response = await this.postFunction(aFunctionName,aBody,aHeaders,aCookies);
return JSON.parse(response._getData());
}
async getFunction(aFunctionName,aParams,aHeaders,aCookies) {
let url = (aFunctionName[0]=='/') ? aFunctionName : `/${aFunctionName}`;
let options = {
method: 'GET',
url: url,
query: aParams // guessing here
};
if (aHeaders)
options.headers = aHeaders;
if (aCookies) {
options.cookies = {};
for (let k in aCookies) {
let v = aCookies[k];
if (typeof(v)=='string') {
options.cookies[k] = {value: v};
} else if (typeof(v)=='object') {
options.cookies[k] = v;
}
}
}
var request = httpMocks.createRequest(options);
var response = httpMocks.createResponse({eventEmitter: eventEmitter});
var me = this;
await new Promise(function(resolve){
response.on('end', resolve);
if (me.functions_index[aFunctionName])
me.functions_index[aFunctionName](request, response);
else
me.functions_index.app(request, response);
});
return response;
}
async getObject(aFunctionName,aParams,aHeaders,aCookies) {
let response = await this.getFunction(aFunctionName,aParams,aHeaders,aCookies);
return JSON.parse(response._getData());
}
};
module.exports = FunctionCaller;
and my app is mounted as app :
exports.app = functions.https.onRequest(expressApp);
and my firebase.json contains :
"rewrites": [
:
:
:
{
"source": "/path/to/function", "function": "app"
}
]
In my test file at the top I do :
const FunctionCaller = require('../FunctionCaller');
let fire_functions = require('../index');
const fnCaller = new FunctionCaller(fire_functions);
and then in the test I do :
let response = await fnCaller.postFunction('/path/to/function',anObject);
and it calls my function with anObject as request.body and returns the response object.
I'm using node 8 on Firebase to get async/await etc.
For local & no-network unit tests, you could refactor the app.get("helloWorld", ...) callback into a separate function and call it with mock objects.
A general approach would be something like this:
main.js:
// in the Firebase code:
export function helloWorld(req, res) { res.send(200); }
app.get('helloWorld', helloWorld);
main.spec.js: using jasmine & sinon
// in the test:
import { helloWorld } from './main.js';
import sinon from 'sinon';
const reqMock = {};
const resMock = { send: sinon.spy() };
it('always responds with 200', (done) => {
helloWorld(reqMock, resMock);
expect(resMock.send.callCount).toBe(1);
expect(resMock.send).toHaveBeenCalledWith(200);
});
Testing is about building up confidence or trust.
I would start by Unit Testing the function in FireBase. Without more requirements defined, I would follow the documentation. Once those Unit Tests pass you can consider what type of testing you want at the Express level. Keeping in mind that you have already tested the function, the only thing to test at the Express level is if the mapping is correct. A few tests at that level should be sufficient to ensure that the mappings have not become "stale" as a result of some set of changes.
If you want to test the Express level and above without having to involve the DB, then you would look at a mocking framework to act like the database for you.
Hope this helps you think about what tests you need.
You can use postman application for unit test.
Enter following url with your project name
https://us-central1-your-project.cloudfunctions.net/hello
app.get('/hello/',(req, res) => {
res.send('hello world');
return 'success';
});

Resources