firebase-js-sdk icon indicating copy to clipboard operation
firebase-js-sdk copied to clipboard

Can't use Timestamp in Firestore rules unit tests

Open ebeloded opened this issue 3 years ago • 18 comments

[REQUIRED] Describe your environment

  • Operating System version: Mac OS
  • Firebase SDK version: "firebase": "9.6.8", "firebase-admin": "10.0.2", "@firebase/rules-unit-testing": "2.0.2",
  • Firebase Product: Firestore

[REQUIRED] Describe the problem

Steps to reproduce:

While writing unit tests for Firestore rules, I am trying to set a timestamp on a document, but this results in the following error:

  • for serverTimestamp():

'Function DocumentReference.set() called with invalid data. Unsupported field value: a custom ServerTimestampFieldValueImpl object (found in field timestamp in document users/new-user)

  • for Timestamp.now() and for adminFirebase.firestore.Timestamp.now():

'Function DocumentReference.set() called with invalid data. Unsupported field value: a custom Timestamp object (found in field timestamp in document users/new-user)'

Relevant Code:


import { serverTimestamp, Timestamp } from 'firebase/firestore'
import firebase from 'firebase-admin'

async function test() {
  const testEnv = await initializeTestEnvironment({
    //...
  })

  testEnv.withSecurityRulesDisabled(async (ctx) => {
    // I tried three ways of generating the timestamp, none of them worked:
    const timestamp = serverTimestamp()
    // const timestamp = firebase.firestore.Timestamp.now()
    // const timestamp = Timestamp.now()

    await ctx.firestore().doc('users/new-user').set({ timestamp })
  })
}

Update

It turns out that I can provide new Date() as timestamp, which doesn't throw an error. However, the timestamp generated in this case doesn't pass the security rule that verifies that the time:

allow create: request.resource.data.timestamp == request.time

ebeloded avatar Mar 17 '22 04:03 ebeloded

@ebeloded Can you run npm list to see if you have two different versions of the Firebase SDK?

schmidt-sebastian avatar Mar 17 '22 16:03 schmidt-sebastian

This is what gets listed:

@firebase/rules-unit-testing 2.0.2
firebase 9.6.8
firebase-admin 10.0.2

I've noticed that Timestamp returned by firebase-admin is different from the one returned from firebase/firestore. The admin library generates object with same fields, but starting with an underscore.

BTW, the error only happens in tests. When doing the same in the app (against Emulator or not), everything works and the time checking rule is respected.

ebeloded avatar Mar 17 '22 23:03 ebeloded

Yes, the Timestamp object are unfortunately not compatible. Are you able to use the Timestamp from firebase-admin (firebase.firestore.Timestamp)?

schmidt-sebastian avatar Mar 18 '22 19:03 schmidt-sebastian

No, Timestamp from the admin library doesn't work either.

I've tried serverTimestamp() and Timestamp.now() from client library, as well as adminFirebase.firestore.Timestamp.now() from firebase-admin. Nothing worked.

Furthermore, I tried another FieldValue (arrayUnion), and it didn't work either.

ebeloded avatar Mar 21 '22 02:03 ebeloded

I'm having the same issue.

schultek avatar Mar 23 '22 17:03 schultek

Having the same issue.

harshmandan avatar Mar 24 '22 03:03 harshmandan

@ebeloded I had this issue a long time ago. With Jest, I am mocking firestore.serverTimestamp() as a workaround. If I remember well that was needed to make the rules pass when using new Date() as mentioned in your update.

The TypeScript code below should do that, but I’m using my own client compatibility layer of firestore-admin mocks instead of the jest.requireActual() line. So I cannot guarantee it will work as jest.requireActual() does not work with ESM. So this code is just here to give you the general idea :wink: Hope it will help…

import type { Timestamp } from '@firebase/firestore-types'

// ...

    jest.unstable_mockModule('@firebase/firestore', () => {
        return {
            ...<any>jest.requireActual('@firebase/firestore'),
            serverTimestamp: jest.fn(() => <Timestamp><unknown>new Date())
        }
    })

laurentpayot avatar Mar 28 '22 10:03 laurentpayot

Thanks @laurentpayot for pointing out the possible workaround. The problem is that this doesn't work if the Firestore rules are checking timestamp integrity. Like this:

allow create: request.resource.data.timestamp == request.time

This rule won't work unless you use proper serverTimestamp in the code. Using Date results in a slight difference in timestamp values.

ebeloded avatar Apr 11 '22 07:04 ebeloded

I had the same problem, and was able to fix it by not using firebase/firestore and firebase-admin. You have to use the firebase from firebase/compat

A very simple example:

import * as firebaseTest from "@firebase/rules-unit-testing"
import firebase from "firebase/compat"

describe ('some test') {
    test ('that we can use serverTimestamp') {
        const env = await firebaseTest.initializeTestEnvironment({ projectId: 'your-project-id' })
        const context  = env.authenticatedContext('some-user-id')
        const firestore = context.firestore()
        await firestore.collection('collectionId').doc('docId').set({
            createdAt: firebase.firestore.FieldValue.serverTimestamp()
        })
    }
}

In that case, even with the below security rule your test will pass.

allow create: request.resource.data.createdAt == request.time

I think it's the same for Timestamp and other firebase types. Take it all from firebase/compat when writing your tests.

juyaki avatar May 12 '22 00:05 juyaki

Hello all. I just want to acknowledge that we are still investigating this issue internally. I'll have a more concrete response by the end of the week.

dconeybe avatar May 17 '22 17:05 dconeybe

Hi all, Firebaser here.

@juyaki is correct in https://github.com/firebase/firebase-js-sdk/issues/6077#issuecomment-1124419965, the underlying issue is that rules-unit-testing is depending entirely on the compat bindings. We will work on finding some time to migrate this library to the modular v9 bindings in the next major version change, in the meantime if we are happy to accept PRs.

avolkovi avatar May 18 '22 18:05 avolkovi

I tried serverTimestamp as in the example at https://github.com/firebase/quickstart-testing/blob/master/unit-test-security-rules-v9/test/firestore.spec.js. It works, it can pass a rule with timestamp, for example request.resource.data.updatedAt == request.time

new-carrot avatar May 26 '22 17:05 new-carrot

UPDATE: I’m now using Vitest instead of Jest so I could not use Jest mocks anymore. Fortunately @schmidt-sebastian’s proposed solution worked for me. I use it like so:

import { Timestamp } from 'firebase-admin/firestore'

function getServerTimestamp() {
    const datesMs = new Date().getTime()
    return new Timestamp(Math.floor(datesMs / 1000), (datesMs % 1000) * 1000)
}

laurentpayot avatar Jun 06 '22 16:06 laurentpayot

Importing the type from the firestore client library does the trick :

import {Timestamp} from '@firebase/firestore';

Out of curiosity, how come admin and client Timestamp types are not compatible ? Is this on purpose ?

vpusher avatar Feb 27 '23 16:02 vpusher

Turns out we have the same issue, not just in tests but when writing documents to Firestore. Following the official instructions here we should be able to use set in order to add a Timestamp to Firestore.

This works without the converter, but if we use a converter we get the same error as the author :

Uncaught (in promise) FirebaseError: Function setDoc() called with invalid data (via `toFirestore()`). Unsupported field value: a custom nt object (found in field dateExample in document collection/123456)

I found that nt is the minified name of Timestamp from our installed dependency.

If I instead use a Date, then the file is written correctly with no error. Additionally, if i use serverTimestamp() it works too.

Therefore, I believe that this is an issue where you cannot use Timestamps in return value from toFirestore in a FirestoreDataConverter.

Current workaround for me : Just use Date object instead, we don't need nanosecond precision. It does however make it confusing to read our Converters : we send Date objects and receive Timestamp objects...

natedx avatar Mar 13 '23 11:03 natedx

I think I am facing the same issue ...

I have an app that uses @firebase/firestore and stores records using Timestamp fields. In a Cloud Function for Firebase I am using the admin SDK @firebase-admin/firestore to do some db cleansing tasks based on those timestamps.

While working with the Timestamps in the app works as expected, in my Function I only receive an empty object for every Timestamp field in my record.

Any thoughts and/or help on this?

kimamil avatar Apr 06 '23 09:04 kimamil

Some additional information I ran into while validating an object as a timestamp instance: FirebaseFirestore.Timestamp class defines required readonly 'seconds' and 'nanoseconds' properties while the Actual timestamp object contains the properties '_seconds' and '_nanoseconds'.

_ is an oldschool way to point out properties like that, but the type should match the implementation either way.

joshwash avatar Nov 08 '23 00:11 joshwash

I came across same problem to test security rules for timestamps. This is what worked for me: import { serverTimestamp } from "firebase/firestore"; and use it where it's needed, like:

await assertSucceeds(
     testDoc.set({
       email: "[email protected]",
       createdAt: serverTimestamp(),
       lastUpdate: serverTimestamp(),
       name: null,
     })
   );

Simple as that! You're welcome :)

Vav4ik avatar Dec 16 '23 21:12 Vav4ik