Can't use Timestamp in Firestore rules unit tests
[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 foradminFirebase.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 Can you run npm list to see if you have two different versions of the Firebase SDK?
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.
Yes, the Timestamp object are unfortunately not compatible. Are you able to use the Timestamp from firebase-admin (firebase.firestore.Timestamp)?
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.
I'm having the same issue.
Having the same issue.
@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())
}
})
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.
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.
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.
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.
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
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)
}
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 ?
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...
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?
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.
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 :)