fusionauth-issues icon indicating copy to clipboard operation
fusionauth-issues copied to clipboard

[Bug]: total value inaccurate when adding and removing users

Open mooreds opened this issue 2 weeks ago • 2 comments

What happened?

This is a weird one. It occurs when standing up an instance locally using Docker but not when using the sandbox.

Replication steps (assuming you have node installed 22.15.1):

  • create a new directory
  • run npm install @fusionauth/typescript-client
  • set up these files:
import FusionAuthClient from '@fusionauth/typescript-client';

// Configuration constants
const FUSIONAUTH_URL = 'http://localhost:9011';
const API_KEY = 'fwZMPKz9E8SXhujVidgimpPT_p9Vz40ZSo94f26SnOrxwaJC1EwzNDlp';
const APPLICATION_ID = '3c219e58-ed0e-4b18-ad48-f4f92793ae32';
const ADMIN_ROLE_ID = 'admin'; // This should match your role ID in FusionAuth

// Initialize FusionAuth client
const client = new FusionAuthClient(API_KEY, FUSIONAUTH_URL);


async function countUsersWithAdminRole(): Promise<number> {
  console.log('Searching for users with admin role...');
  
  // Query for users with the admin role
  const searchResponse = await client.searchUsersByQuery({
    search: {
      queryString: `registrations.applicationId:${APPLICATION_ID} AND registrations.roles:${ADMIN_ROLE_ID}`,
    }
  });

  if (!searchResponse.wasSuccessful()) {
    throw new Error(`Failed to search users: ${JSON.stringify(searchResponse.exception)}`);
  }

  const count = searchResponse.response.total || 0
  console.log(`Found ${count} users with admin role`);
  return count;
}

async function main() {
  try {
    // Step 1: Find initial count of users with admin role
    console.log('\n=== Step 1: Initial count ===');
    const initialCount = await countUsersWithAdminRole();

    // Step 2: Create a new user with admin role
    console.log('\n=== Step 2: Creating new user ===');
    const newUserId = crypto.randomUUID();
    const newUser = {
      user: {
        email: `test-admin-${Date.now()}@example.com`,
        username: `testadmin${Date.now()}`,
        password: 'TempPassword123!',
        firstName: 'Test',
        lastName: 'Admin'
      },
      registration: {
        applicationId: APPLICATION_ID,
        roles: [ADMIN_ROLE_ID]
      }
    };

    const createResponse = await client.register(newUserId, newUser);
    
    if (!createResponse.wasSuccessful()) {
      throw new Error(`Failed to create user: ${JSON.stringify(createResponse.exception)}`);
    }

    const createdUserId = createResponse.response.user?.id;
    console.log(`User created successfully with ID: ${createdUserId}`);

    // Step 3: Verify the count increased
    //console.log('\n=== Step 3: Count after creation ===');
    //const countAfterCreation = await countUsersWithAdminRole();
    //console.log(`Count increased by: ${countAfterCreation - initialCount}`);

    // Step 4: Delete the user
    console.log('\n=== Step 4: Deleting user ===');
    const deleteResponse = await client.deleteUser(createdUserId!);
    
    if (!deleteResponse.wasSuccessful()) {
      throw new Error(`Failed to delete user: ${JSON.stringify(deleteResponse.exception)}`);
    }
    
    console.log(`User ${createdUserId} deleted successfully`);

    // Step 5: Verify the count returned to original
    console.log('\n=== Step 5: Final count ===');
    const finalCount = await countUsersWithAdminRole();
    console.log(`Final count matches initial count: ${finalCount === initialCount}`);

    console.log('\n=== Summary ===');
    console.log(`Initial count: ${initialCount}`);
    //console.log(`After creation: ${countAfterCreation}`);
    console.log(`Final count: ${finalCount}`);

  } catch (error) {
    console.error('Error occurred:', error);
    process.exit(1);
  }
}

// Run the script
main();

in script.ts

and

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "strict": true,
    "resolveJsonModule": true,
    "outDir": "./dist"
  },
  "include": ["*.ts"],
  "exclude": ["node_modules"]
}

in tsconfig.json

  • install the default Docker install for FusionAuth as documented here: https://fusionauth.io/docs/get-started/download-and-install/docker
  • complete the setup wizard and create a user
  • create an API key with the value fwZMPKz9E8SXhujVidgimpPT_p9Vz40ZSo94f26SnOrxwaJC1EwzNDlp
  • run the script: npx ts-node script; you see the value 0
  • run it again, you see the value 1
  • run it again, you see the value 2

If you reindex or use searchResponse.response.users?.length instead of searchResponse.response.total or wait for a while, the value appears to be correct.

So I don't know if our default docker configuration of opensearch needs to change or something.

But it was weird and confusing for me.

Version

1.61.2

Affects Versions

No response

Alternatives / Workarounds

If you reindex or use searchResponse.response.users?.length instead of searchResponse.response.total or wait for a while, the value appears to be correct.

mooreds avatar Dec 13 '25 19:12 mooreds

using the accurateTotal doesn't help.

mooreds avatar Dec 13 '25 19:12 mooreds

I wonder if the issue is related to https://github.com/FusionAuth/fusionauth-issues/issues/3270 because I'm seeing that in the docker error output when I run the script.

mooreds avatar Dec 13 '25 19:12 mooreds