Data in transaction in a cloud function is always null when run in test suite

Viewed 73

I have a Firebase cloud function that does this:

const admin = require('firebase-admin')
admin.initializeApp()

exports.setSessionState = functions.https.onCall(async (data, context) => {
    const stateId = data.stateId
    const details = data.details
    const stateRef = admin.database().ref(`state/${stateId}`)
    stateRef.transaction((state) => {
        if (state) {
            state.details = details
        }
        return state
    })
})

The code works well in the actual application and the state is updated but I run into problems when running this in a test, where the state is always null. (In other words, the details are never set.) The test uses the Mocha framework and is run against a real Firebase project and I can't see how anything would differ here but the behaviour is consistently different when calling this cloud function from the client application and when calling it from the test suite:

const chai = require('chai')
const assert = chai.assert

const test = require('firebase-functions-test')({
  databaseURL: '<redacted>',
  storageBucket: '<redacted>',
  projectId: '<redacted>',
}, '../service-account-credentials.json')

describe('CFsetSessionState', () => {
  let wrapped
  let cloudFunctions
  let admin

  before(() => {
    cloudFunctions = require('../index')
    admin = require('firebase-admin')
    wrapped = test.wrap(cloudFunctions.CFsetSessionState)
  })

  it('Test setting state', async () => {
    const stateId = 'abc'

    const data = {
      stateId: stateId,
      details: {
        name: 'New name'
      }
    }

    const context = {
      auth: {
        uid: 123
      }
    }

    const stateRef = admin.database().ref(`state/${stateId}`)

    await stateRef.set({
      name: 'Previous name',
      active: true
    })

    await wrapped(data, context)

    const snapshot = await stateRef.once('value')
    const state = snapshot.val()

    // Details do not exist here, why?
    assert.equal(state.details.name, 'New name')
  })
})

I leave the database state as is after the tests so I can see there is indeed data in the state object but the details have not been set. Experimenting a bit with setting and fetching data (using .once()) before and after the call to the cloud function can change the behaviour, making me think it might be some cache issue, but this experimenting does not give me any particular stable end state. I have no idea how the equivalent of a local cache works in cloud functions, it currently shows random behaviour.

What could cause this?

1 Answers

I haven't tried running your Cloud Function with your test, but most probably the problem comes from the fact that you incorrectly manage the life cycle of your Cloud Function. Since it is Callable one, you need to terminate it by returning a Promise (or a value when all the asynchronous work is completed). More details here and here in the doc.

You should therefore adapt your CF as follows:

exports.setSessionState = functions.https.onCall(async (data, context) => {
    const stateId = data.stateId
    const details = data.details
    const stateRef = admin.database().ref(`state/${stateId}`)
    return stateRef.transaction((state) => {   // Note the return here
        if (state) {
            state.details = details
        }
        return state
    })
})

We are returning the Promise returned by the Transaction.

Related