Repository: firebase/firebase-tools (ts)

Issue:
Auth Emulator batchGet should return nextPageToken if page is full
## Expected Behavior

If there are enough users to fill a whole page size (whatever that is), a `nextPageToken` should be returned even if the next page would be empty. (I've tested in production and this is the behavior indeed.)

## Actual Behavior

The Auth Emulator returns no page token if number of users is same as page size. This causes tests failures like https://github.com/firebase/firebase-admin-node/pull/1204/checks?check_run_id=2189478758.

We had to disable that tests for now (https://github.com/firebase/firebase-admin-node/pull/1205) and that should be reverted after the fix land in a release.

Pull request:
Fix nextPageToken edge case in batchGet.
Fixes #3231

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4279575a5..3285c77f5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,2 +1,3 @@
 - Fixes native module issues by removing `fast-crc32c` (#3247, #3239)
 - Update to Cloud Firestore emulator v1.11.13 which includes a bug fix for documents which are created and deleted in a single transaction.
+- Fixes an edge case with nextPageToken in batchGet in Auth Emulator (#3231).
diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts
index b729beb45..d018dacb4 100644
--- a/src/emulator/auth/operations.ts
+++ b/src/emulator/auth/operations.ts
@@ -449,7 +449,7 @@ function batchGet(
   let newPageToken: string | undefined = undefined;
 
   // As a non-standard behavior, passing in limit=-1 will return all users.
-  if (limit >= 0 && users.length > limit) {
+  if (limit >= 0 && users.length >= limit) {
     users.length = limit;
     if (users.length) {
       newPageToken = users[users.length - 1].localId;


Test patch:
diff --git a/src/test/emulators/auth/batch.spec.ts b/src/test/emulators/auth/batch.spec.ts
index bff8810f6..74aa4d80b 100644
--- a/src/test/emulators/auth/batch.spec.ts
+++ b/src/test/emulators/auth/batch.spec.ts
@@ -63,6 +63,7 @@ describeAuthEmulator("accounts:batchGet", ({ authApi }) => {
   it("should allow specifying maxResults and pagination", async () => {
     const user1 = await registerAnonUser(authApi());
     const user2 = await registerUser(authApi(), { email: "foo@example.com", password: "foobar" });
+    const localIds = [user1.localId, user2.localId].sort();
 
     const nextPageToken = await authApi()
       .get(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:batchGet`)
@@ -71,9 +72,7 @@ describeAuthEmulator("accounts:batchGet", ({ authApi }) => {
       .then((res) => {
         expectStatusCode(200, res);
         expect(res.body.users).to.have.length(1);
-        expect(res.body.users[0].localId).to.equal(
-          user1.localId < user2.localId ? user1.localId : user2.localId
-        );
+        expect(res.body.users[0].localId).to.equal(localIds[0]);
 
         expect(res.body).to.have.property("nextPageToken").which.is.a("string");
         return res.body.nextPageToken as string;
@@ -86,9 +85,7 @@ describeAuthEmulator("accounts:batchGet", ({ authApi }) => {
       .then((res) => {
         expectStatusCode(200, res);
         expect(res.body.users).to.have.length(1);
-        expect(res.body.users[0].localId).to.equal(
-          user1.localId > user2.localId ? user1.localId : user2.localId
-        );
+        expect(res.body.users[0].localId).to.equal(localIds[1]);
 
         // No more accounts after this, so no page token returned.
         expect(res.body).not.to.have.property("nextPageToken");
@@ -110,6 +107,41 @@ describeAuthEmulator("accounts:batchGet", ({ authApi }) => {
         expect(res.body).not.to.have.property("nextPageToken");
       });
   });
+
+  it("should always return a page token if page is full", async () => {
+    const user1 = await registerAnonUser(authApi());
+    const user2 = await registerUser(authApi(), { email: "foo@example.com", password: "foobar" });
+    const localIds = [user1.localId, user2.localId].sort();
+
+    const nextPageToken = await authApi()
+      .get(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:batchGet`)
+      .query({ maxResults: 2 }) // Return first two users only.
+      .set("Authorization", "Bearer owner")
+      .then((res) => {
+        expectStatusCode(200, res);
+        expect(res.body.users).to.have.length(2);
+        expect(res.body.users[0].localId).to.equal(localIds[0]);
+        expect(res.body.users[1].localId).to.equal(localIds[1]);
+
+        // Even if there are no more users after this page, we should still
+        // return a page token to match production behavior. See:
+        // https://github.com/firebase/firebase-tools/issues/3231
+        expect(res.body).to.have.property("nextPageToken").which.is.a("string");
+        return res.body.nextPageToken as string;
+      });
+
+    await authApi()
+      .get(`/identitytoolkit.googleapis.com/v1/projects/${PROJECT_ID}/accounts:batchGet`)
+      .query({ nextPageToken })
+      .set("Authorization", "Bearer owner")
+      .then((res) => {
+        expectStatusCode(200, res);
+        console.log(nextPageToken, res.body.users);
+        // Empty page with no page token returned.
+        expect(res.body.users || []).to.have.length(0);
+        expect(res.body).not.to.have.property("nextPageToken");
+      });
+  });
 });
 
 describeAuthEmulator("accounts:batchCreate", ({ authApi }) => {