From aab6dcd98bd2738e99a1702ab243b3c09956e659 Mon Sep 17 00:00:00 2001 From: einhornimmond Date: Sun, 22 Jun 2025 15:14:23 +0200 Subject: [PATCH] move findUserByIdentifier and validateAlias into database inclusive refactored tests for this --- .../resolver/TransactionResolver.test.ts | 30 +--- .../graphql/resolver/TransactionResolver.ts | 2 +- .../src/graphql/resolver/UserResolver.test.ts | 166 +----------------- backend/src/graphql/resolver/UserResolver.ts | 11 +- .../resolver/util/findUserByIdentifier.ts | 65 ------- .../util/findUserByIdentifiers.test.ts | 92 ---------- bun.lock | 14 +- config-schema/test/testSetup.vitest.ts | 106 +++++++++++ core/src/index.ts | 2 +- core/src/validation/index.ts | 3 +- core/src/validation/user.test.ts | 10 +- core/src/validation/user.ts | 9 +- database/package.json | 3 +- database/src/queries/communities.test.ts | 4 +- database/src/queries/index.ts | 4 + database/src/queries/user.test.ts | 145 ++++++++++++--- database/src/queries/user.ts | 57 +++++- database/src/seeds/factory/user.ts | 12 +- database/src/seeds/utils.ts | 10 -- database/vitest.config.js | 7 + dht-node/jest.config.js | 2 +- dht-node/test/helpers.test.ts | 2 +- .../api/1_0/resolver/SendCoinsResolver.ts | 52 +++--- .../src/graphql/util/findUserByIdentifier.ts | 57 ------ federation/src/graphql/util/validateAlias.ts | 46 ----- shared/package.json | 5 +- shared/src/schema/base.schema.test.ts | 12 ++ shared/src/schema/base.schema.ts | 6 + shared/src/schema/index.ts | 1 + shared/turbo.json | 13 -- yarn.lock | 19 ++ 31 files changed, 410 insertions(+), 557 deletions(-) delete mode 100644 backend/src/graphql/resolver/util/findUserByIdentifier.ts delete mode 100644 backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts create mode 100644 config-schema/test/testSetup.vitest.ts delete mode 100644 database/src/seeds/utils.ts create mode 100644 database/vitest.config.js delete mode 100644 federation/src/graphql/util/findUserByIdentifier.ts delete mode 100644 federation/src/graphql/util/validateAlias.ts create mode 100644 shared/src/schema/base.schema.test.ts create mode 100644 shared/src/schema/base.schema.ts delete mode 100644 shared/turbo.json diff --git a/backend/src/graphql/resolver/TransactionResolver.test.ts b/backend/src/graphql/resolver/TransactionResolver.test.ts index 97b3084d9..77026b445 100644 --- a/backend/src/graphql/resolver/TransactionResolver.test.ts +++ b/backend/src/graphql/resolver/TransactionResolver.test.ts @@ -137,19 +137,11 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], + errors: [new GraphQLError('The recipient user was not found')], }), ) }) - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'wrong@email.com', - homeCom.communityUuid, - ) - }) - describe('deleted recipient', () => { it('throws an error', async () => { jest.clearAllMocks() @@ -170,18 +162,10 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], + errors: [new GraphQLError('The recipient user was not found')], }), ) }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'stephen@hawking.uk', - homeCom.communityUuid, - ) - }) }) describe('recipient account not activated', () => { @@ -204,18 +188,10 @@ describe('send coins', () => { }), ).toEqual( expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], + errors: [new GraphQLError('The recipient user was not found')], }), ) }) - - it('logs the error thrown', () => { - expect(logger.error).toBeCalledWith( - 'No user with this credentials', - 'garrick@ollivander.com', - homeCom.communityUuid, - ) - }) }) }) diff --git a/backend/src/graphql/resolver/TransactionResolver.ts b/backend/src/graphql/resolver/TransactionResolver.ts index 511c7167d..cfb4308be 100644 --- a/backend/src/graphql/resolver/TransactionResolver.ts +++ b/backend/src/graphql/resolver/TransactionResolver.ts @@ -5,6 +5,7 @@ import { Transaction as dbTransaction, TransactionLink as dbTransactionLink, User as dbUser, + findUserByIdentifier } from 'database' import { Decimal } from 'decimal.js-light' import { Args, Authorized, Ctx, Mutation, Query, Resolver } from 'type-graphql' @@ -40,7 +41,6 @@ import { LOG4JS_RESOLVER_CATEGORY_NAME } from '.' import { BalanceResolver } from './BalanceResolver' import { GdtResolver } from './GdtResolver' import { getCommunityByIdentifier, getCommunityName, isHomeCommunity } from './util/communities' -import { findUserByIdentifier } from './util/findUserByIdentifier' import { getLastTransaction } from './util/getLastTransaction' import { getTransactionList } from './util/getTransactionList' import { diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 9a8c223f1..01340f355 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -68,7 +68,7 @@ import { printTimeDuration } from '@/util/time' import { objectValuesToArray } from '@/util/utilities' import { LOG4JS_BASE_CATEGORY_NAME } from '@/config/const' -import { clearLogs, getLogger, printLogs } from 'config-schema/test/testSetup' +import { getLogger } from 'config-schema/test/testSetup' import { LOG4JS_RESOLVER_CATEGORY_NAME } from '.' import { Location2Point } from './util/Location2Point' @@ -698,9 +698,6 @@ describe('UserResolver', () => { }) describe('no users in database', () => { - beforeAll(() => { - clearLogs() - }) it('throws an error', async () => { jest.clearAllMocks() const result = await mutate({ mutation: login, variables }) @@ -712,7 +709,6 @@ describe('UserResolver', () => { }) it('logs the error found', () => { - printLogs() expect(logger.warn).toBeCalledWith( `findUserByEmail failed, user with email=${variables.email} not found`, ) @@ -2698,166 +2694,6 @@ describe('UserResolver', () => { expect(logErrorLogger.error).toBeCalledWith('401 Unauthorized') }) }) - - describe('authenticated', () => { - const uuid = uuidv4() - - beforeAll(async () => { - user = await userFactory(testEnv, bibiBloxberg) - await mutate({ - mutation: login, - variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, - }) - // first set alias to null, because updating alias isn't currently allowed - await User.update({ alias: 'BBB' }, { alias: () => 'NULL' }) - await mutate({ - mutation: updateUserInfos, - variables: { - alias: 'bibi', - }, - }) - }) - - describe('identifier is no gradido ID, no email and no alias', () => { - it('throws and logs "Unknown identifier type" error', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'identifier_is_no_valid_alias!', - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('Unknown identifier type')], - }), - ) - expect(logErrorLogger.error).toBeCalledWith( - 'Unknown identifier type', - 'identifier_is_no_valid_alias!', - ) - }) - }) - - describe('identifier is not found', () => { - it('throws and logs "No user found to given identifier" error', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: uuid, - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No user found to given identifier(s)')], - }), - ) - expect(logErrorLogger.error).toBeCalledWith( - 'No user found to given identifier(s)', - uuid, - homeCom1.communityUuid, - ) - }) - }) - - describe('identifier is found via email, but not matching community', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'bibi@bloxberg.de', - communityIdentifier: foreignCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - errors: [new GraphQLError('No user with this credentials')], - }), - ) - expect(logErrorLogger.error).toBeCalledWith( - 'No user with this credentials', - 'bibi@bloxberg.de', - foreignCom1.communityUuid, - ) - }) - }) - - describe('identifier is found via email', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'bibi@bloxberg.de', - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - user: expect.objectContaining({ - firstName: 'Bibi', - lastName: 'Bloxberg', - }), - }, - errors: undefined, - }), - ) - }) - }) - - describe('identifier is found via gradidoID', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: user.gradidoID, - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - user: expect.objectContaining({ - firstName: 'Bibi', - lastName: 'Bloxberg', - }), - }, - errors: undefined, - }), - ) - }) - }) - - describe('identifier is found via alias', () => { - it('returns user', async () => { - await expect( - query({ - query: userQuery, - variables: { - identifier: 'bibi', - communityIdentifier: homeCom1.communityUuid, - }, - }), - ).resolves.toEqual( - expect.objectContaining({ - data: { - user: expect.objectContaining({ - firstName: 'Bibi', - lastName: 'Bloxberg', - }), - }, - errors: undefined, - }), - ) - }) - }) - }) }) describe('check username', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index f7688d1ca..ad952cabe 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -6,6 +6,8 @@ import { UserContact as DbUserContact, ProjectBranding, UserLoggingView, + getHomeCommunity, + findUserByIdentifier } from 'database' import { GraphQLResolveInfo } from 'graphql' import i18n from 'i18n' @@ -93,11 +95,9 @@ import { Logger, getLogger } from 'log4js' import { FULL_CREATION_AVAILABLE } from './const/const' import { Location2Point, Point2Location } from './util/Location2Point' import { authenticateGmsUserPlayground } from './util/authenticateGmsUserPlayground' -import { getHomeCommunity } from 'database' import { compareGmsRelevantUserSettings } from './util/compareGmsRelevantUserSettings' import { getUserCreations } from './util/creations' import { extractGraphQLFieldsForSelect } from './util/extractGraphQLFields' -import { findUserByIdentifier } from './util/findUserByIdentifier' import { findUsers } from './util/findUsers' import { getKlicktippState } from './util/getKlicktippState' import { deleteUserRole, setUserRole } from './util/modifyUserRole' @@ -1153,8 +1153,11 @@ export class UserResolver { { identifier, communityIdentifier }: UserArgs, ): Promise { const foundDbUser = await findUserByIdentifier(identifier, communityIdentifier) - const modelUser = new User(foundDbUser) - return modelUser + if (!foundDbUser) { + createLogger().debug('User not found', identifier, communityIdentifier) + throw new Error('User not found') + } + return new User(foundDbUser) } // FIELD RESOLVERS diff --git a/backend/src/graphql/resolver/util/findUserByIdentifier.ts b/backend/src/graphql/resolver/util/findUserByIdentifier.ts deleted file mode 100644 index ef8b91c2c..000000000 --- a/backend/src/graphql/resolver/util/findUserByIdentifier.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { isURL } from 'class-validator' -import { Community, User as DbUser, UserContact as DbUserContact } from 'database' -import { FindOptionsWhere } from 'typeorm' -import { validate, version } from 'uuid' - -import { LogError } from '@/server/LogError' -import { isEMail, isUUID4 } from '@/util/validate' - -import { aliasSchema } from 'shared' - -/** - * - * @param identifier could be gradidoID, alias or email of user - * @param communityIdentifier could be uuid or name of community - * @returns - */ -export const findUserByIdentifier = async ( - identifier: string, - communityIdentifier: string, -): Promise => { - let user: DbUser | null - const communityWhere: FindOptionsWhere = isURL(communityIdentifier) - ? { url: communityIdentifier } - : isUUID4(communityIdentifier) - ? { communityUuid: communityIdentifier } - : { name: communityIdentifier } - - if (validate(identifier) && version(identifier) === 4) { - user = await DbUser.findOne({ - where: { gradidoID: identifier, community: communityWhere }, - relations: ['emailContact', 'community'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else if (isEMail(identifier)) { - const userContact = await DbUserContact.findOne({ - where: { - email: identifier, - emailChecked: true, - user: { - community: communityWhere, - }, - }, - relations: { user: { community: true } }, - }) - if (!userContact) { - throw new LogError('No user with this credentials', identifier, communityIdentifier) - } - user = userContact.user - user.emailContact = userContact - } else if (aliasSchema.safeParse(identifier).success) { - user = await DbUser.findOne({ - where: { alias: identifier, community: communityWhere }, - relations: ['emailContact', 'community'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else { - throw new LogError('Unknown identifier type', identifier) - } - - return user -} diff --git a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts b/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts deleted file mode 100644 index 94010b846..000000000 --- a/backend/src/graphql/resolver/util/findUserByIdentifiers.test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { ApolloServerTestClient } from 'apollo-server-testing' -import { Community as DbCommunity, User as DbUser } from 'database' -import { DataSource } from 'typeorm' - -import { cleanDB, testEnvironment } from '@test/helpers' - -import { writeHomeCommunityEntry } from '@/seeds/community' -import { userFactory } from '@/seeds/factory/user' -import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { bobBaumeister } from '@/seeds/users/bob-baumeister' -import { peterLustig } from '@/seeds/users/peter-lustig' - -import { findUserByIdentifier } from './findUserByIdentifier' - -jest.mock('@/password/EncryptorUtils') - -let con: DataSource -let testEnv: { - mutate: ApolloServerTestClient['mutate'] - query: ApolloServerTestClient['query'] - con: DataSource -} - -beforeAll(async () => { - testEnv = await testEnvironment() - con = testEnv.con - await cleanDB() -}) - -afterAll(async () => { - await cleanDB() - await con.destroy() -}) - -describe('graphql/resolver/util/findUserByIdentifier', () => { - let homeCom: DbCommunity - let communityUuid: string - let communityName: string - let userBibi: DbUser - - beforeAll(async () => { - homeCom = await writeHomeCommunityEntry() - - communityUuid = homeCom.communityUuid! - - communityName = homeCom.communityUuid! - - userBibi = await userFactory(testEnv, bibiBloxberg) - await userFactory(testEnv, peterLustig) - await userFactory(testEnv, bobBaumeister) - }) - - describe('communityIdentifier is community uuid', () => { - it('userIdentifier is gradido id', async () => { - const user = await findUserByIdentifier(userBibi.gradidoID, communityUuid) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is alias', async () => { - const user = await findUserByIdentifier(userBibi.alias, communityUuid) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is email', async () => { - const user = await findUserByIdentifier(userBibi.emailContact.email, communityUuid) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - }) - - describe('communityIdentifier is community name', () => { - it('userIdentifier is gradido id', async () => { - const user = await findUserByIdentifier(userBibi.gradidoID, communityName) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is alias', async () => { - const user = await findUserByIdentifier(userBibi.alias, communityName) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - - it('userIdentifier is email', async () => { - const user = await findUserByIdentifier(userBibi.emailContact.email, communityName) - user.userRoles = [] - expect(user).toMatchObject(userBibi) - }) - }) -}) diff --git a/bun.lock b/bun.lock index 0a2bf2b49..82616745c 100644 --- a/bun.lock +++ b/bun.lock @@ -181,7 +181,7 @@ "name": "core", "version": "2.6.0", "dependencies": { - "database": "workspace:*", + "database": "*", "esbuild": "^0.25.2", "log4js": "^6.9.1", "zod": "^3.25.61", @@ -224,6 +224,7 @@ "@types/geojson": "^7946.0.13", "@types/jest": "27.0.2", "@types/node": "^18.7.14", + "crypto-random-bigint": "^2.1.1", "jest": "27.2.4", "ts-jest": "27.0.5", "ts-node": "^10.9.2", @@ -251,8 +252,8 @@ "@types/joi": "^17.2.3", "@types/node": "^17.0.45", "@types/uuid": "^8.3.4", - "config-schema": "workspace:*", - "database": "workspace:*", + "config-schema": "*", + "database": "*", "dotenv": "10.0.0", "esbuild": "^0.25.3", "jest": "27.5.1", @@ -427,6 +428,7 @@ "@biomejs/biome": "2.0.0", "@types/node": "^17.0.21", "typescript": "^4.9.5", + "uuid": "^8.3.2", }, }, }, @@ -1605,6 +1607,8 @@ "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "crypto-random-bigint": ["crypto-random-bigint@2.1.1", "", { "dependencies": { "uint-rng": "^1.2.1" } }, "sha512-96+FDrenXybkpnLML/60S8NcG32KgJ5Y8yvNNCYPW02r+ssoXFR5XKenuIQcHLWumnGj8UPqUUHBzXNrDGkDmQ=="], + "css-functions-list": ["css-functions-list@3.2.3", "", {}, "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA=="], "css-select": ["css-select@4.3.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.0.1", "domhandler": "^4.3.1", "domutils": "^2.8.0", "nth-check": "^2.0.1" } }, "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ=="], @@ -3099,6 +3103,8 @@ "tiny-case": ["tiny-case@1.0.3", "", {}, "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q=="], + "tiny-webcrypto": ["tiny-webcrypto@1.0.3", "", {}, "sha512-LQQdNMAgz9BXNT2SKbYh3eCb+fLV0p7JB7MwUjzY6IOlQLGIadfnFqRpshERsS5Dl2OM/hs0+4I/XmSrF+RBbw=="], + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], @@ -3209,6 +3215,8 @@ "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], + "uint-rng": ["uint-rng@1.2.1", "", { "dependencies": { "tiny-webcrypto": "^1.0.2" } }, "sha512-swhDg5H+3DX2sIvnYA7VMBMXV/t8mPxvh49CjCDkwFmj/3OZIDOQwJANBgM1MPSUBrUHNIlXmU7/GcL7m4907g=="], + "uint8array-extras": ["uint8array-extras@1.4.0", "", {}, "sha512-ZPtzy0hu4cZjv3z5NW9gfKnNLjoz4y6uv4HlelAjDK7sY/xOkKZv9xK/WQpcsBB3jEybChz9DPC2U/+cusjJVQ=="], "unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="], diff --git a/config-schema/test/testSetup.vitest.ts b/config-schema/test/testSetup.vitest.ts new file mode 100644 index 000000000..5d404c806 --- /dev/null +++ b/config-schema/test/testSetup.vitest.ts @@ -0,0 +1,106 @@ +import { vi } from 'vitest' +/* + * This file is used to mock the log4js logger in the tests. + * It is used to collect all log entries in the logs array. + * If you want to debug your test, you can use `printLogs()` to print all log entries collected through the tests. + * To have only the relevant logs, call `clearLogs()` before your calling the methods you like to test and `printLogs()` after. + */ + + +type LogEntry = { + level: string; + message: string; + logger: string; + context: string; + additional: any[]; +} + +const loggers: { [key: string]: any } = {} +const logs: LogEntry[] = [] + +function addLog(level: string, message: string, logger: string, context: Map, additional: any[]) { + logs.push({ + level, + context: [...context.entries()].map(([key, value]) => `${key}=${value}`).join(' ').trimEnd(), + message, + logger, + additional + }) +} + +export function printLogs() { + for (const log of logs) { + const messages = [] + messages.push(log.message) + messages.push(log.additional.map((d) => { + if (typeof d === 'object' && d.toString() === '[object Object]') { + return JSON.stringify(d) + } + if (d) { + return d.toString() + } + }).filter((d) => d)) + process.stdout.write(`${log.logger} [${log.level}] ${log.context} ${messages.join(' ')}\n`) + } +} + +export function clearLogs(): void { + logs.length = 0 +} + +const getLoggerMocked = vi.fn().mockImplementation((param: any) => { + if (loggers[param]) { + // TODO: check if it is working when tests run in parallel + loggers[param].clearContext() + return loggers[param] + } + // console.log('getLogger called with: ', param) + const fakeLogger = { + context: new Map(), + addContext: vi.fn((key: string, value: string) => { + fakeLogger.context.set(key, value) + }), + trace: vi.fn((message: string, ...args: any[]) => { + addLog('trace', message, param, fakeLogger.context, args) + }), + debug: vi.fn((message: string, ...args: any[]) => { + addLog('debug', message, param, fakeLogger.context, args) + }), + warn: vi.fn((message: string, ...args: any[]) => { + addLog('warn', message, param, fakeLogger.context, args) + }), + info: vi.fn((message: string, ...args: any[]) => { + addLog('info', message, param, fakeLogger.context, args) + }), + error: vi.fn((message: string, ...args: any[]) => { + addLog('error', message, param, fakeLogger.context, args) + }), + fatal: vi.fn((message: string, ...args: any[]) => { + addLog('fatal', message, param, fakeLogger.context, args) + }), + removeContext: vi.fn((key: string) => { + fakeLogger.context.delete(key) + }), + clearContext: vi.fn(() => { + fakeLogger.context.clear() + }) + } + loggers[param] = fakeLogger + return fakeLogger +}) + +vi.mock('log4js', () => { + const originalModule = vi.importActual('log4js') + return { + __esModule: true, + ...originalModule, + getLogger: getLoggerMocked + } +}) + +export function getLogger(name: string) { + if (!loggers[name]) { + return getLoggerMocked(name) + } + return loggers[name] +} diff --git a/core/src/index.ts b/core/src/index.ts index 99830d342..0984ebefc 100644 --- a/core/src/index.ts +++ b/core/src/index.ts @@ -1 +1 @@ -export * from './validation' \ No newline at end of file +export * from './validation/user' \ No newline at end of file diff --git a/core/src/validation/index.ts b/core/src/validation/index.ts index 28d7d3d07..b00a2f926 100644 --- a/core/src/validation/index.ts +++ b/core/src/validation/index.ts @@ -1,4 +1,3 @@ import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const' -export const LOG_CATEGORY_SCHEMA_ALIAS = `${LOG4JS_BASE_CATEGORY_NAME}.schema` -export * from './user' \ No newline at end of file +export const LOG4JS_CATEGORY_SCHEMA_ALIAS = `${LOG4JS_BASE_CATEGORY_NAME}.schema` diff --git a/core/src/validation/user.test.ts b/core/src/validation/user.test.ts index e8faf5abe..4cfc76a7c 100644 --- a/core/src/validation/user.test.ts +++ b/core/src/validation/user.test.ts @@ -1,10 +1,10 @@ -import { validateAlias } from './user.schema' -import { getLogger } from '../../../config-schema/test/testSetup.bun' -import { LOG_CATEGORY_SCHEMA_ALIAS } from '.' +import { LOG4JS_CATEGORY_SCHEMA_ALIAS } from '.' +import { validateAlias } from './user' +import { getLogger, printLogs } from '../../../config-schema/test/testSetup.bun' import { describe, it, expect, beforeEach, mock, jest } from 'bun:test' import { aliasExists } from 'database' -const logger = getLogger(`${LOG_CATEGORY_SCHEMA_ALIAS}.alias`) +const logger = getLogger(`${LOG4JS_CATEGORY_SCHEMA_ALIAS}.alias`) mock.module('database', () => ({ aliasExists: jest.fn(), @@ -33,7 +33,7 @@ describe('validate alias', () => { minimum: 3, origin: 'string', message: 'Given alias is too short', - }),*/ + }), */ expect.objectContaining({ code: 'too_small', exact: false, diff --git a/core/src/validation/user.ts b/core/src/validation/user.ts index 85d3774d0..945b1c818 100644 --- a/core/src/validation/user.ts +++ b/core/src/validation/user.ts @@ -1,10 +1,10 @@ import { ZodError } from 'zod' import { getLogger } from 'log4js' -import { LOG_CATEGORY_SCHEMA_ALIAS } from '.' +import { LOG4JS_CATEGORY_SCHEMA_ALIAS } from '.' import { aliasExists } from 'database' import { aliasSchema } from 'shared' -const logger = getLogger(`${LOG_CATEGORY_SCHEMA_ALIAS}.alias`) +const logger = getLogger(`${LOG4JS_CATEGORY_SCHEMA_ALIAS}.alias`) export async function validateAlias(alias: string): Promise { try { @@ -12,9 +12,10 @@ export async function validateAlias(alias: string): Promise { } catch (err) { if (err instanceof ZodError || (err as Error).name === 'ZodError') { // throw only first error, but log all errors - logger.warn('invalid alias', alias, (err as ZodError).issues) - throw new Error((err as ZodError).issues[0].message) + logger.warn('invalid alias', alias, (err as ZodError).errors) + throw new Error((err as ZodError).errors[0].message) } + console.log(err) throw err } diff --git a/database/package.json b/database/package.json index ed5cd2fe3..21d027427 100644 --- a/database/package.json +++ b/database/package.json @@ -20,7 +20,7 @@ "lint": "biome check --error-on-warnings .", "lint:fix": "biome check --error-on-warnings . --write", "clear": "cross-env TZ=UTC tsx migration/index.ts clear", - "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test vitest run", + "test": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test vitest --reporter verbose --no-file-parallelism run", "test:debug": "cross-env TZ=UTC NODE_ENV=development DB_DATABASE=gradido_test node --inspect-brk node_modules/.bin/jest --bail --runInBand --forceExit --detectOpenHandles", "up": "cross-env TZ=UTC tsx migration/index.ts up", "down": "cross-env TZ=UTC tsx migration/index.ts down", @@ -40,6 +40,7 @@ "@types/geojson": "^7946.0.13", "@types/jest": "27.0.2", "@types/node": "^18.7.14", + "crypto-random-bigint": "^2.1.1", "jest": "27.2.4", "ts-jest": "27.0.5", "ts-node": "^10.9.2", diff --git a/database/src/queries/communities.test.ts b/database/src/queries/communities.test.ts index ca8ab0b12..9cfb210db 100644 --- a/database/src/queries/communities.test.ts +++ b/database/src/queries/communities.test.ts @@ -1,4 +1,4 @@ -import { Community } from '..' +import { Community as DbCommunity } from '..' import { AppDatabase } from '../AppDatabase' import { getHomeCommunity } from './communities' import { describe, expect, it, beforeAll, afterAll } from 'vitest' @@ -15,7 +15,7 @@ afterAll(async () => { describe('community.queries', () => { beforeAll(async () => { - await Community.clear() + await DbCommunity.clear() }) describe('getHomeCommunity', () => { it('should return null if no home community exists', async () => { diff --git a/database/src/queries/index.ts b/database/src/queries/index.ts index bb6255b2e..c244a4af0 100644 --- a/database/src/queries/index.ts +++ b/database/src/queries/index.ts @@ -1,2 +1,6 @@ +import { LOG4JS_BASE_CATEGORY_NAME } from '../config/const' + export * from './user' export * from './communities' + +export const LOG4JS_QUERIES_CATEGORY_NAME = `${LOG4JS_BASE_CATEGORY_NAME}.queries` diff --git a/database/src/queries/user.test.ts b/database/src/queries/user.test.ts index 4ab567a24..a14f70a3c 100644 --- a/database/src/queries/user.test.ts +++ b/database/src/queries/user.test.ts @@ -1,11 +1,18 @@ -import { User, UserContact } from '..' +import { User as DbUser, UserContact as DbUserContact, Community as DbCommunity } from '..' import { AppDatabase } from '../AppDatabase' -import { aliasExists } from './user' +import { aliasExists, findUserByIdentifier } from './user' import { userFactory } from '../seeds/factory/user' import { bibiBloxberg } from '../seeds/users/bibi-bloxberg' -import { describe, expect, it, beforeAll, afterAll } from 'vitest' +import { describe, expect, it, beforeAll, afterAll, vi } from 'vitest' +import { createCommunity } from '../seeds/homeCommunity' +import { peterLustig } from '../seeds/users/peter-lustig' +import { bobBaumeister } from '../seeds/users/bob-baumeister' +import { getLogger, printLogs, clearLogs } from '../../../config-schema/test/testSetup.vitest.ts' +import { LOG4JS_QUERIES_CATEGORY_NAME } from '.' +import { beforeEach } from 'node:test' const db = AppDatabase.getInstance() +const userIdentifierLoggerName = `${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier` beforeAll(async () => { await db.init() @@ -14,29 +21,121 @@ afterAll(async () => { await db.destroy() }) -describe('integration test mysql queries', () => { - describe('user.queries', () => { - describe('aliasExists', () => { - beforeAll(async () => { - await User.clear() - await UserContact.clear() +describe('user.queries', () => { + describe('aliasExists', () => { + beforeAll(async () => { + await DbUser.clear() + await DbUserContact.clear() - const bibi = bibiBloxberg - bibi.alias = 'b-b' - await userFactory(bibi) + const bibi = bibiBloxberg + bibi.alias = 'b-b' + await userFactory(bibi) + }) + + it('should return true if alias exists', async () => { + expect(await aliasExists('b-b')).toBe(true) + }) + + it('should return true if alias exists even with deviating casing', async () => { + expect(await aliasExists('b-B')).toBe(true) + }) + + it('should return false if alias does not exist', async () => { + expect(await aliasExists('bibi')).toBe(false) + }) + }) + + describe('findUserByIdentifier', () => { + let homeCom: DbCommunity + let communityUuid: string + let communityName: string + let userBibi: DbUser + + beforeAll(async () => { + await DbUser.clear() + await DbUserContact.clear() + await DbCommunity.clear() + + homeCom = await createCommunity(false) + communityUuid = homeCom.communityUuid! + communityName = homeCom.name! + userBibi = await userFactory(bibiBloxberg) + await userFactory(peterLustig) + await userFactory(bobBaumeister) + }) + beforeEach(() => { + clearLogs() + }) + describe('communityIdentifier is community uuid', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, communityUuid) + expect(user).toMatchObject(userBibi) }) - - it('should return true if alias exists', async () => { - expect(await aliasExists('b-b')).toBe(true) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias, communityUuid) + expect(user).toMatchObject(userBibi) }) - - it('should return true if alias exists even with deviating casing', async () => { - expect(await aliasExists('b-B')).toBe(true) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email, communityUuid) + expect(user).toMatchObject(userBibi) }) - - it('should return false if alias does not exist', async () => { - expect(await aliasExists('bibi')).toBe(false) + it('userIdentifier is unknown', async () => { + const user = await findUserByIdentifier('unknown', communityUuid) + expect(user).toBeNull() }) }) - }) -}) \ No newline at end of file + + describe('communityIdentifier is community name', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, communityName) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias, communityName) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email, communityName) + expect(user).toMatchObject(userBibi) + }) + }) + describe('communityIdentifier is unknown', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID, 'unknown') + expect(user).toBeNull() + }) + it('userIdentifier is unknown', async () => { + const user = await findUserByIdentifier('unknown', communityUuid) + expect(user).toBeNull() + }) + }) + describe('communityIdentifier is empty', () => { + it('userIdentifier is gradido id', async () => { + const user = await findUserByIdentifier(userBibi.gradidoID) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is alias', async () => { + const user = await findUserByIdentifier(userBibi.alias) + expect(user).toMatchObject(userBibi) + }) + + it('userIdentifier is email', async () => { + const user = await findUserByIdentifier(userBibi.emailContact.email) + expect(user).toMatchObject(userBibi) + }) + it('userIdentifier is unknown type', async () => { + const user = await findUserByIdentifier('sa') + printLogs() + expect(getLogger(userIdentifierLoggerName).warn).toHaveBeenCalledWith('Unknown identifier type', 'sa') + expect(user).toBeNull() + }) + }) + }) +}) + + diff --git a/database/src/queries/user.ts b/database/src/queries/user.ts index 49394ad8f..b968e9433 100644 --- a/database/src/queries/user.ts +++ b/database/src/queries/user.ts @@ -1,9 +1,62 @@ import { Raw } from 'typeorm' -import { User as DbUser } from '../entity' +import { Community, User as DbUser, UserContact as DbUserContact } from '../entity' +import { FindOptionsWhere } from 'typeorm' +import { aliasSchema, emailSchema, uuidv4Schema, urlSchema } from 'shared' +import { getLogger } from 'log4js' +import { LOG4JS_QUERIES_CATEGORY_NAME } from './index' export async function aliasExists(alias: string): Promise { const user = await DbUser.findOne({ where: { alias: Raw((a) => `LOWER(${a}) = LOWER(:alias)`, { alias }) }, }) return user !== null -} \ No newline at end of file +} + +/** + * + * @param identifier could be gradidoID, alias or email of user + * @param communityIdentifier could be uuid or name of community + * @returns + */ +export const findUserByIdentifier = async ( + identifier: string, + communityIdentifier?: string, +): Promise => { + const communityWhere: FindOptionsWhere = urlSchema.safeParse(communityIdentifier).success + ? { url: communityIdentifier } + : uuidv4Schema.safeParse(communityIdentifier).success + ? { communityUuid: communityIdentifier } + : { name: communityIdentifier } + + if (uuidv4Schema.safeParse(identifier).success) { + return DbUser.findOne({ + where: { gradidoID: identifier, community: communityWhere }, + relations: ['emailContact', 'community'], + }) + } else if (emailSchema.safeParse(identifier).success) { + const userContact = await DbUserContact.findOne({ + where: { + email: identifier, + emailChecked: true, + user: { + community: communityWhere, + }, + }, + relations: { user: { community: true } }, + }) + if (userContact) { + // TODO: remove circular reference + const user = userContact.user + user.emailContact = userContact + return user + } + } else if (aliasSchema.safeParse(identifier).success) { + return await DbUser.findOne({ + where: { alias: identifier, community: communityWhere }, + relations: ['emailContact', 'community'], + }) + } + // should don't happen often, so we create only in the rare case a logger for it + getLogger(`${LOG4JS_QUERIES_CATEGORY_NAME}.user.findUserByIdentifier`).warn('Unknown identifier type', identifier) + return null +} diff --git a/database/src/seeds/factory/user.ts b/database/src/seeds/factory/user.ts index 412d0088a..293ee4bb3 100644 --- a/database/src/seeds/factory/user.ts +++ b/database/src/seeds/factory/user.ts @@ -1,8 +1,9 @@ import { UserInterface } from '../users/UserInterface' import { User, UserContact } from '../../entity' -import { generateRandomNumber, generateRandomNumericString } from '../utils' import { v4 } from 'uuid' import { UserContactType, OptInType, PasswordEncryptionType } from 'shared' +import { getHomeCommunity } from '../../queries/communities' +import random from 'crypto-random-bigint' export const userFactory = async (user: UserInterface): Promise => { let dbUserContact = new UserContact() @@ -21,12 +22,17 @@ export const userFactory = async (user: UserInterface): Promise => { dbUser.gradidoID = v4() if (user.emailChecked) { - dbUserContact.emailVerificationCode = generateRandomNumericString(64) + dbUserContact.emailVerificationCode = random(64).toString() dbUserContact.emailOptInTypeId = OptInType.EMAIL_OPT_IN_REGISTER dbUserContact.emailChecked = true - dbUser.password = generateRandomNumber() + dbUser.password = random(64) dbUser.passwordEncryptionType = PasswordEncryptionType.GRADIDO_ID } + const homeCommunity = await getHomeCommunity() + if (homeCommunity) { + dbUser.community = homeCommunity + dbUser.communityUuid = homeCommunity.communityUuid! + } // TODO: improve with cascade dbUser = await dbUser.save() dbUserContact.userId = dbUser.id diff --git a/database/src/seeds/utils.ts b/database/src/seeds/utils.ts deleted file mode 100644 index 0aa6c2d0f..000000000 --- a/database/src/seeds/utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { randomBytes } from 'node:crypto' - -export function generateRandomNumber(): BigInt { - return BigInt(randomBytes(8).readBigUInt64LE()) -} -export function generateRandomNumericString(length: number = 64): string { - const digits = '0123456789' - const bytes = randomBytes(length / 8) - return Array.from(bytes, (b) => digits[b % 10]).join('').slice(0, length) -} \ No newline at end of file diff --git a/database/vitest.config.js b/database/vitest.config.js new file mode 100644 index 000000000..b0e269a1a --- /dev/null +++ b/database/vitest.config.js @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + setupFiles: '../config-schema/test/testSetup.vitest.ts', + }, +}) \ No newline at end of file diff --git a/dht-node/jest.config.js b/dht-node/jest.config.js index bca83b5ce..fe5fb923d 100644 --- a/dht-node/jest.config.js +++ b/dht-node/jest.config.js @@ -3,7 +3,7 @@ module.exports = { verbose: false, preset: 'ts-jest', collectCoverage: false, - collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!src/seeds/**', '!build/**'], + collectCoverageFrom: ['src/**/*.ts', '!**/node_modules/**', '!build/**'], coverageThreshold: { global: { lines: 82, diff --git a/dht-node/test/helpers.test.ts b/dht-node/test/helpers.test.ts index 69d8f3fa4..37810e878 100644 --- a/dht-node/test/helpers.test.ts +++ b/dht-node/test/helpers.test.ts @@ -1,4 +1,4 @@ -import { contributionDateFormatter } from '@test/helpers' +import { contributionDateFormatter } from './helpers' describe('contributionDateFormatter', () => { it('formats the date correctly', () => { diff --git a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts index a6eeef7f8..d3c028df3 100644 --- a/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts +++ b/federation/src/graphql/api/1_0/resolver/SendCoinsResolver.ts @@ -1,10 +1,10 @@ -import { findUserByIdentifier } from '@/graphql/util/findUserByIdentifier' import { fullName } from '@/graphql/util/fullName' import { LogError } from '@/server/LogError' import { Community as DbCommunity, PendingTransaction as DbPendingTransaction, PendingTransactionLoggingView, + findUserByIdentifier } from 'database' import Decimal from 'decimal.js-light' import { getLogger } from 'log4js' @@ -43,14 +43,14 @@ export class SendCoinsResolver { ) } let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier( - args.recipientUserIdentifier, - args.recipientCommunityUuid, - ) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier( + args.recipientUserIdentifier, + args.recipientCommunityUuid, + ) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier:') throw new LogError( `voteForSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, @@ -126,11 +126,11 @@ export class SendCoinsResolver { ) } let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + + // second check if receiver user exists in this community + receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier') throw new LogError( `revertSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, @@ -193,12 +193,11 @@ export class SendCoinsResolver { args.recipientCommunityUuid, ) } - let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + + // second check if receiver user exists in this community + const receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier') throw new LogError( `settleSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, @@ -265,13 +264,12 @@ export class SendCoinsResolver { `revertSettledSendCoins with wrong recipientCommunityUuid`, args.recipientCommunityUuid, ) - } - let receiverUser - try { - // second check if receiver user exists in this community - receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) - } catch (err) { - logger.error('Error in findUserByIdentifier:', err) + } + + // second check if receiver user exists in this community + const receiverUser = await findUserByIdentifier(args.recipientUserIdentifier) + if (!receiverUser) { + logger.error('Error in findUserByIdentifier') throw new LogError( `revertSettledSendCoins with unknown recipientUserIdentifier in the community=`, homeCom.name, diff --git a/federation/src/graphql/util/findUserByIdentifier.ts b/federation/src/graphql/util/findUserByIdentifier.ts deleted file mode 100644 index 7f5e8e329..000000000 --- a/federation/src/graphql/util/findUserByIdentifier.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { User as DbUser, UserContact as DbUserContact } from 'database' -import { validate, version } from 'uuid' - -import { LogError } from '@/server/LogError' - -import { VALID_ALIAS_REGEX } from './validateAlias' - -export const findUserByIdentifier = async ( - identifier: string, - communityIdentifier?: string, -): Promise => { - let user: DbUser | null - if (validate(identifier) && version(identifier) === 4) { - user = await DbUser.findOne({ - where: { gradidoID: identifier, communityUuid: communityIdentifier }, - relations: ['emailContact'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else if (/^.{2,}@.{2,}\..{2,}$/.exec(identifier)) { - const userContact = await DbUserContact.findOne({ - where: { - email: identifier, - emailChecked: true, - }, - relations: ['user'], - }) - if (!userContact) { - throw new LogError('No user with this credentials', identifier) - } - if (!userContact.user) { - throw new LogError('No user to given contact', identifier) - } - if (userContact.user.communityUuid !== communityIdentifier) { - throw new LogError( - 'Found user to given contact, but belongs to other community', - identifier, - communityIdentifier, - ) - } - user = userContact.user - user.emailContact = userContact - } else if (VALID_ALIAS_REGEX.exec(identifier)) { - user = await DbUser.findOne({ - where: { alias: identifier, communityUuid: communityIdentifier }, - relations: ['emailContact'], - }) - if (!user) { - throw new LogError('No user found to given identifier(s)', identifier, communityIdentifier) - } - } else { - throw new LogError('Unknown identifier type', identifier) - } - - return user -} diff --git a/federation/src/graphql/util/validateAlias.ts b/federation/src/graphql/util/validateAlias.ts deleted file mode 100644 index 4eccad63f..000000000 --- a/federation/src/graphql/util/validateAlias.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { User as DbUser } from 'database' -import { Raw } from 'typeorm' - -import { LogError } from '@/server/LogError' - -export const VALID_ALIAS_REGEX = /^(?=.{3,20}$)[a-zA-Z0-9]+(?:[_-][a-zA-Z0-9]+?)*$/ - -const RESERVED_ALIAS = [ - 'admin', - 'email', - 'gast', - 'gdd', - 'gradido', - 'guest', - 'home', - 'root', - 'support', - 'temp', - 'tmp', - 'tmp', - 'user', - 'usr', - 'var', -] - -export const validateAlias = async (alias: string): Promise => { - if (alias.length < 3) { - throw new LogError('Given alias is too short', alias) - } - if (alias.length > 20) { - throw new LogError('Given alias is too long', alias) - } - if (!alias.match(VALID_ALIAS_REGEX)) { - throw new LogError('Invalid characters in alias', alias) - } - if (RESERVED_ALIAS.includes(alias.toLowerCase())) { - throw new LogError('Alias is not allowed', alias) - } - const aliasInUse = await DbUser.find({ - where: { alias: Raw((a) => `LOWER(${a}) = "${alias.toLowerCase()}"`) }, - }) - if (aliasInUse.length !== 0) { - throw new LogError('Alias already in use', alias) - } - return true -} diff --git a/shared/package.json b/shared/package.json index 0225a39e5..88eb5b557 100644 --- a/shared/package.json +++ b/shared/package.json @@ -7,7 +7,7 @@ "exports": { ".": { "import": "./build/index.js", - "require": "./build/index.js" + "require": "./build/index.js" } }, "repository": "https://github.com/gradido/gradido/shared", @@ -26,7 +26,8 @@ "devDependencies": { "@biomejs/biome": "2.0.0", "@types/node": "^17.0.21", - "typescript": "^4.9.5" + "typescript": "^4.9.5", + "uuid": "^8.3.2" }, "dependencies": { "esbuild": "^0.25.2", diff --git a/shared/src/schema/base.schema.test.ts b/shared/src/schema/base.schema.test.ts new file mode 100644 index 000000000..3e56a9a22 --- /dev/null +++ b/shared/src/schema/base.schema.test.ts @@ -0,0 +1,12 @@ +import { describe, expect, it } from 'bun:test' +import { uuidv4Schema } from './base.schema' +import { v4 as uuidv4 } from 'uuid' + +describe('uuidv4 schema', () => { + it('should validate uuidv4 (40x)', () => { + for (let i = 0; i < 40; i++) { + const uuid = uuidv4() + expect(uuidv4Schema.safeParse(uuid).success).toBeTruthy() + } + }) +}) diff --git a/shared/src/schema/base.schema.ts b/shared/src/schema/base.schema.ts new file mode 100644 index 000000000..ed341c48b --- /dev/null +++ b/shared/src/schema/base.schema.ts @@ -0,0 +1,6 @@ +import { string } from 'zod' +import { validate, version } from 'uuid' + +export const uuidv4Schema = string().refine((val: string) => validate(val) && version(val) === 4, 'Invalid uuid') +export const emailSchema = string().email() +export const urlSchema = string().url() \ No newline at end of file diff --git a/shared/src/schema/index.ts b/shared/src/schema/index.ts index b60401cfa..d8c9f9e4c 100644 --- a/shared/src/schema/index.ts +++ b/shared/src/schema/index.ts @@ -1 +1,2 @@ export * from './user.schema' +export * from './base.schema' \ No newline at end of file diff --git a/shared/turbo.json b/shared/turbo.json deleted file mode 100644 index 399303c0c..000000000 --- a/shared/turbo.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "extends": ["//"], - "tasks": { - "test": { - "dependsOn": ["config-schema#build"] - }, - "build": { - "dependsOn": ["^build"], - "outputs": ["build/**"], - "cache": true - } - } -} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 36e413b72..d68c092c1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5180,6 +5180,13 @@ cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6: shebang-command "^2.0.0" which "^2.0.1" +crypto-random-bigint@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/crypto-random-bigint/-/crypto-random-bigint-2.1.1.tgz#f80239ca9d69b53a4920fc5908949689d1b9db95" + integrity sha512-96+FDrenXybkpnLML/60S8NcG32KgJ5Y8yvNNCYPW02r+ssoXFR5XKenuIQcHLWumnGj8UPqUUHBzXNrDGkDmQ== + dependencies: + uint-rng "^1.2.1" + css-functions-list@^3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/css-functions-list/-/css-functions-list-3.2.3.tgz#95652b0c24f0f59b291a9fc386041a19d4f40dbe" @@ -11735,6 +11742,11 @@ tiny-case@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-case/-/tiny-case-1.0.3.tgz#d980d66bc72b5d5a9ca86fb7c9ffdb9c898ddd03" integrity sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q== +tiny-webcrypto@^1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tiny-webcrypto/-/tiny-webcrypto-1.0.3.tgz#a78e1c5707c546a7d086569368b13a0de56dc9f6" + integrity sha512-LQQdNMAgz9BXNT2SKbYh3eCb+fLV0p7JB7MwUjzY6IOlQLGIadfnFqRpshERsS5Dl2OM/hs0+4I/XmSrF+RBbw== + tinybench@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/tinybench/-/tinybench-2.9.0.tgz#103c9f8ba6d7237a47ab6dd1dcff77251863426b" @@ -12269,6 +12281,13 @@ uglify-js@^3.1.4: resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-3.19.3.tgz#82315e9bbc6f2b25888858acd1fff8441035b77f" integrity sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ== +uint-rng@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/uint-rng/-/uint-rng-1.2.1.tgz#4d1d22f75f52bc4baab739a0363fd054474be9c8" + integrity sha512-swhDg5H+3DX2sIvnYA7VMBMXV/t8mPxvh49CjCDkwFmj/3OZIDOQwJANBgM1MPSUBrUHNIlXmU7/GcL7m4907g== + dependencies: + tiny-webcrypto "^1.0.2" + uint8array-extras@^1.3.0: version "1.4.0" resolved "https://registry.yarnpkg.com/uint8array-extras/-/uint8array-extras-1.4.0.tgz#e42a678a6dd335ec2d21661333ed42f44ae7cc74"