Search code examples
node.jsunit-testingchaikoachai-http

Chai-http: cannot set value of nodes ctx.state


I am creating an API with NodeJS and KOA. For testing, I use chai (chai-http) and mocha. The problem arises when I use const { username } = ctx.state.user in my controllers to get the username of the user that sent the request. When accessing them with my application (with Flutter) or with Postman, it works, but when running the tests with mocha, i get the error TypeError: Cannot destructure property 'username' of 'undefined' or 'null'. When debugging the code, i found that ctx has a key for state but the value for that key was empty. I tried the methods .set(...) and .send(...) but they only modified values inside ctx.request.header and ctx.request.body.

So my question is: is it possible to set a value for ctx.state with chai and if so, how? I would like to put in something like {user: {username: 'chai'}}.

Here are the 2 main parts, the controller section to be tested and the test method:

async function bar(ctx, next) {
    const { username } = ctx.state.user;
    const { value } = ctx.request.body;

    // do something with value and username

    ctx.status = 200;
}
it("With correct gameKey: should return the rating of the game", done => {
    chai
        .request('http://localhost:3000')
        .post('/foo/bar')
        .set('content-type', 'application/json')
        .send({value: 3});
        .end((err, res) => {
            // do some tests
            done();
        });
});

Here is the whole code from the server index and the test file:

const Koa = require('koa');
const Jwt = require('koa-jwt');
const Router = require('koa-router');
const Cors = require('@koa/cors');
const BodyParser = require('koa-bodyparser');
const Helmet = require('koa-helmet');

const app = new Koa();
const router = new Router();
router.post('/foo/bar', bar);

async function bar(ctx, next) {
    const { username } = ctx.state.user;
    const { value } = ctx.request.body;

    // do something with value and username

    ctx.status = 200;
}

app.use(Helmet());
app.use(Cors());
app.use(BodyParser({
    enableTypes: ['json'],
    strict: true,
    onerror(err, ctx) {
        ctx.throw('Request body could not be parsed', 422);
    },
}));
app.use(Jwt({ secret: process.env.SECRET }).unless({
    path: [
        // Whitelist routes that don't require authentication
        /^\/auth/,
    ],
}));

app.use(router.routes());
app.use(router.allowedMethods());

app.listen(3000, () => console.log(`API server started on localhost:3000`));
const chai = require("chai");
const chaiHttp = require("chai-http");
chai.use(chaiHttp);
const expect = require('chai').expect;

const userCredentials = {
  username: 'chai', 
  password: 'chai'
}

describe("Route: games/", () => {
    before(() => {
        chai
            .request('http://localhost:3000')
            .post('/auth')
            .set('content-type', 'application/json')
            .send(userCredentials)
            .end((err, res) => {
                expect(res.status).to.be.eql(200);
            });
    });

    describe("Sub-route: GET /", () => {
        describe("Sub-route: PUT /:gameKey/rating", () => {
            it("With correct gameKey: should return the rating of the game", done => {
                chai
                    .request('http://localhost:3000')
                    .post('/foo/bar')
                    .set('content-type', 'application/json')
                    .send({value: 3});
                    .end((err, res) => {
                        expect(res.status).to.be.eql(200);
                        done();
                    });
            });
        });
    });
});

Solution

  • I figured it out thanks to the comment of Sandeep Patel. I had to use the middleware, so i saved the token retreived with the request in the before() method and added it to the other requests with .set("Authorization", "Bearer " + token).

    The working test then looks like this:

    const chai = require("chai");
    const chaiHttp = require("chai-http");
    chai.use(chaiHttp);
    const expect = require('chai').expect;
    
    const userCredentials = {
      username: 'chai', 
      password: 'chai'
    }
    
    describe("Route: games/", () => {
        var token;
        before(() => {
            chai
                .request('http://localhost:3000')
                .post('/auth')
                .set('content-type', 'application/json')
                .send(userCredentials)
                .end((err, res) => {
                    expect(res.status).to.be.eql(200);
                    token = res.body.token;  // Added this
                });
        });
    
        describe("Sub-route: GET /", () => {
            describe("Sub-route: PUT /:gameKey/rating", () => {
                it("With correct gameKey: should return the rating of the game", done => {
                    chai
                        .request('http://localhost:3000')
                        .post('/foo/bar')
                        .set("Authorization", "Bearer " + token)  // Added this
                        .set('content-type', 'application/json')
                        .send({value: 3});
                        .end((err, res) => {
                            expect(res.status).to.be.eql(200);
                            done();
                        });
                });
            });
        });
    });