chill117 / express-mysql-session

A MySQL session store for the express framework in node
MIT License
313 stars 109 forks source link

Tests in Jest using Supertest break when using this store, but still work as designed in production #147

Open M-Scott-Lassiter opened 1 year ago

M-Scott-Lassiter commented 1 year ago

Current Behavior

I have a test suite that uses Jest. When I use the default memory session storage, all my tests work fine. When I upgraded to this package for session storage, a bunch of the tests broke. However, with manual testing everything is still working as designed.

I inserted a bunch of console.log calls everywhere and I've narrowed the problem down to when I call the req.login function in my route (simplified example code):

The test looks something like this:

const request = require('supertest')

beforeAll(async () => {
    // Make the connection using my database handler
    db.connect()
})

test('Sets a cookie and redirects to / after logging in.', async () => {
    // This is a synchronous function that just returns an object with keys `id` and `password`
    const newCredentials = newValidUserCredentials() 
    // An asynchronous function that creates this user in the database for testing
    await processNewUserRegistrationData(newCredentials)

    const agent = request.agent(app)

    await agent
        .post('/login')
        .send(newCredentials)
        .expect(302)
        .expect('set-cookie', /sessionID=/)
        .expect('Location', '/')
        .then(async (resp) => {
            // Shows the 'set-cookie' field when using MemoryStore, but when using MySQLStore it never even console.logs
            console.log(resp.header) 
            // It is important to access the cookie here so I can call `agent` again in tests that require authentication
        })

})

afterAll(async () => {
    // Close the connection using my database handler
    db.close()
})

The route is setup something like this:

loginRouter.post(
    '/',
    someMiddleware(req, res, next),
    async function authenticateMiddleware(req, res, next) {
        await passport.authenticate('local', async (err, user, errorMessage) => {
            if (errorMessage) {
                // Do some error event logging stuff
                // ...
                return res.render('pages/login', {
                    failureMessage:
                        'Unable to authenticate with that email and password combination'
                })
            }
            console.log('SUCCESSFULLY AUTHENTICATED:', user)

            // There is an existing user, and this user has successfully authenticated. Log them in.
            console.log('LOGGING IN USER')
            req.login(user, (error) => {
                console.log('OPERATING INSIDE THE LOGIN CALLBACK FUNCTION NOW...')
                // Do some event logging stuff
                // ...
                return next()
            })
        })(req, res, next)
    },
    moreMiddleware(req, res, next),
    (req, res) => {
        console.log('FINAL REQUEST COOKIE IN HEADER:', req.headers.cookie)
        res.redirect('/')
    }
)

The app.js has a session store configuration like this.

// ...
app.use(
    session({
        store: new MySQLStore({
            host: config.EXPRESS_HOST,
            port: config.DATABASE_PORT,
            user: config.DATABASE_USER,
            password: config.DATABASE_PASSWORD,
            database: config.DATABASE_NAME,
            createDatabaseTable: false,
            clearExpired: true,
            checkExpirationInterval: 900 * 1000,
            schema: {
                tableName: 'SESSIONS',
                columnNames: {
                    session_id: 'SESSION_ID',
                    expires: 'EXPIRES',
                    data: 'DATA'
                }
            }
        }),
        name: 'sessionID',
        secret: config.EXPRESS_SESSION_SECRET,
        resave: false,
        saveUninitialized: false,
        rolling: true,
        cookie: {
            secure: false, // Dev environments on 'localhost' run on http so secure won't work. Set to true on the production server
            httpOnly: true,
            domain: config.EXPRESS_HOST,
            maxAge: config.EXPRESS_SESSION_LIMIT_DAYS * 24 * 60 * 60 * 1000
        }
    })
    // ...
// ...

Commenting out the store key reverts Express to using the MemoryStore method and the test works again with all console.log statements outputting as expected.

When the MySQLStore store is used during the test, it console logs all the way up to LOGGING IN USER, then nothing. The test then fails with the error

thrown: "Exceeded timeout of 5000 ms for a test. Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."

as well as telling me

A worker process has failed to exit gracefully and has been force exited. This is likely caused by tests leaking due to improper teardown. Try running with --detectOpenHandles to find leaks. Active timers can also cause this, ensure that .unref() was called on them.

Expected Behavior

The test should successfully complete just like the MemoryStore test does. If this is not possible, there should be documentation to show how to test the functionality works as desired.

Cross reference to Stack Overflow post: Express App Test Suite with Supertest Breaks when switching from MemoryStore to express-mysql-session

chill117 commented 11 months ago

I am not familiar with supertest, but it looks like your beforeAll and afterAll hooks aren't waiting for db.connect to finish. The database connection is synchronous when using the memory store, but asynchronous when using MySQL store.