move findUserByIdentifier and validateAlias into database inclusive refactored tests for this

This commit is contained in:
einhornimmond 2025-06-22 15:14:23 +02:00
parent bd6e804a56
commit aab6dcd98b
31 changed files with 410 additions and 557 deletions

View File

@ -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,
)
})
})
})

View File

@ -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 {

View File

@ -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', () => {

View File

@ -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<User> {
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

View File

@ -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<DbUser> => {
let user: DbUser | null
const communityWhere: FindOptionsWhere<Community> = 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
}

View File

@ -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)
})
})
})

View File

@ -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=="],

View File

@ -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<string, string>, 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<string, string>(),
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]
}

View File

@ -1 +1 @@
export * from './validation'
export * from './validation/user'

View File

@ -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'
export const LOG4JS_CATEGORY_SCHEMA_ALIAS = `${LOG4JS_BASE_CATEGORY_NAME}.schema`

View File

@ -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,

View File

@ -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<true> {
try {
@ -12,9 +12,10 @@ export async function validateAlias(alias: string): Promise<true> {
} 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
}

View File

@ -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",

View File

@ -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 () => {

View File

@ -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`

View File

@ -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()
})
})
})
})
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()
})
})
})
})

View File

@ -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<boolean> {
const user = await DbUser.findOne({
where: { alias: Raw((a) => `LOWER(${a}) = LOWER(:alias)`, { alias }) },
})
return user !== null
}
}
/**
*
* @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<DbUser | null> => {
const communityWhere: FindOptionsWhere<Community> = 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
}

View File

@ -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<User> => {
let dbUserContact = new UserContact()
@ -21,12 +22,17 @@ export const userFactory = async (user: UserInterface): Promise<User> => {
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

View File

@ -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)
}

View File

@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
setupFiles: '../config-schema/test/testSetup.vitest.ts',
},
})

View File

@ -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,

View File

@ -1,4 +1,4 @@
import { contributionDateFormatter } from '@test/helpers'
import { contributionDateFormatter } from './helpers'
describe('contributionDateFormatter', () => {
it('formats the date correctly', () => {

View File

@ -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,

View File

@ -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<DbUser> => {
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
}

View File

@ -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<boolean> => {
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
}

View File

@ -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",

View File

@ -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()
}
})
})

View File

@ -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()

View File

@ -1 +1,2 @@
export * from './user.schema'
export * from './base.schema'

View File

@ -1,13 +0,0 @@
{
"extends": ["//"],
"tasks": {
"test": {
"dependsOn": ["config-schema#build"]
},
"build": {
"dependsOn": ["^build"],
"outputs": ["build/**"],
"cache": true
}
}
}

View File

@ -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"