diff --git a/packages/core/src/lib/user.test.ts b/packages/core/src/lib/user.test.ts new file mode 100644 index 000000000..b26f6e32a --- /dev/null +++ b/packages/core/src/lib/user.test.ts @@ -0,0 +1,50 @@ +import { hasUserWithId } from '@/queries/user'; + +import { generateUserId } from './user'; + +jest.mock('@/queries/user'); + +describe('generateUserId()', () => { + afterEach(() => { + (hasUserWithId as jest.MockedFunction).mockClear(); + }); + + it('generates user ID with correct length when no conflict found', async () => { + const mockedHasUserWithId = ( + hasUserWithId as jest.MockedFunction + ).mockImplementationOnce(async () => false); + + await expect(generateUserId()).resolves.toHaveLength(12); + expect(mockedHasUserWithId).toBeCalledTimes(1); + }); + + it('generates user ID with correct length when retry limit is not reached', async () => { + // eslint-disable-next-line @silverhand/fp/no-let + let tried = 0; + const mockedHasUserWithId = ( + hasUserWithId as jest.MockedFunction + ).mockImplementation(async () => { + if (tried) { + return false; + } + + // eslint-disable-next-line @silverhand/fp/no-mutation + tried++; + return true; + }); + + await expect(generateUserId(2)).resolves.toHaveLength(12); + expect(mockedHasUserWithId).toBeCalledTimes(2); + }); + + it('rejects with correct error message when retry limit is reached', async () => { + const mockedHasUserWithId = ( + hasUserWithId as jest.MockedFunction + ).mockImplementation(async () => true); + + await expect(generateUserId(10)).rejects.toThrow( + 'Cannot generate user ID in reasonable retries' + ); + expect(mockedHasUserWithId).toBeCalledTimes(11); + }); +}); diff --git a/packages/core/src/lib/user.ts b/packages/core/src/lib/user.ts new file mode 100644 index 000000000..6718c01f4 --- /dev/null +++ b/packages/core/src/lib/user.ts @@ -0,0 +1,21 @@ +import pRetry from 'p-retry'; + +import { hasUserWithId } from '@/queries/user'; +import { buildIdGenerator } from '@/utils/id'; + +const userId = buildIdGenerator(12); + +// LOG-89: Add unit tests +export const generateUserId = async (retries = 500) => + pRetry( + async () => { + const id = userId(); + + if (!(await hasUserWithId(id))) { + return id; + } + + throw new Error('Cannot generate user ID in reasonable retries'); + }, + { retries, factor: 0 } // No need for exponential backoff + ); diff --git a/packages/core/src/routes/user.ts b/packages/core/src/routes/user.ts index 024035704..3fb91d283 100644 --- a/packages/core/src/routes/user.ts +++ b/packages/core/src/routes/user.ts @@ -1,33 +1,15 @@ import { PasswordEncryptionMethod } from '@logto/schemas'; import { nanoid } from 'nanoid'; -import pRetry from 'p-retry'; import { object, string } from 'zod'; import RequestError from '@/errors/RequestError'; +import { generateUserId } from '@/lib/user'; import koaGuard from '@/middleware/koa-guard'; -import { hasUser, hasUserWithId, insertUser } from '@/queries/user'; -import { buildIdGenerator } from '@/utils/id'; +import { hasUser, insertUser } from '@/queries/user'; import { encryptPassword } from '@/utils/password'; import { AnonymousRouter } from './types'; -const userId = buildIdGenerator(12); - -// LOG-89: Add unit tests -const generateUserId = async (retries = 500) => - pRetry( - async () => { - const id = userId(); - - if (!(await hasUserWithId(id))) { - return id; - } - - throw new Error('Cannot generate user ID in reasonable retries'); - }, - { retries } - ); - export default function userRoutes(router: T) { router.post( '/user',