From 12f1e0095dc42cf0a9a2e94fa89abd117b122748 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch Date: Thu, 11 Jul 2024 17:56:10 +0200 Subject: [PATCH 1/4] add tests for the user service --- src/services/users.spec.ts | 187 ++++++++++++++++++++++++++++++++----- src/services/users.ts | 4 +- 2 files changed, 167 insertions(+), 24 deletions(-) diff --git a/src/services/users.spec.ts b/src/services/users.spec.ts index f85fe930..8107596b 100644 --- a/src/services/users.spec.ts +++ b/src/services/users.spec.ts @@ -1,26 +1,169 @@ +import * as db from '../db'; +import { createDbClient } from '../utils/db/create-db-connection'; +import { runMigrations } from '../utils/db/run-migrations'; +import { environmentVariables, insertUserSchema } from '../types'; +import { cleanup, seed } from '../utils/db/seed'; +import { updateUser, upsertUserData, validateUserData } from './users'; +import { eq } from 'drizzle-orm'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; import { z } from 'zod'; -import { insertUserSchema } from '../types/users'; - -describe('service: users', function () { - describe('schema: insertUserSchema', function () { - it('should remove empty strings from user data', function () { - const user: z.infer = { - email: '', - username: '', - firstName: '', - lastName: '', - telegram: '', - }; - - const transformedUser: { [key: string]: string | null | string[] | object } = - insertUserSchema.parse(user); - - // loop through all keys and check if they are not empty strings - - for (const key of Object.keys(transformedUser)) { - console.log(key); - expect(transformedUser[key]).not.toBe(''); - } + +describe('service: users', () => { + let dbPool: NodePgDatabase; + let dbConnection: Client; + let userData: { + email: string | null; + username: string | null; + firstName: string | null; + lastName: string | null; + telegram: string | null; + }; + let user: db.User | undefined; + let secondUser: db.User | undefined; + let thirdUser: db.User | undefined; + beforeAll(async () => { + const envVariables = environmentVariables.parse(process.env); + const initDb = await createDbClient({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + await runMigrations({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, }); + + dbPool = initDb.db; + dbConnection = initDb.client; + // seed + const { users } = await seed(dbPool); + user = users[0]; + secondUser = users[1]; + thirdUser = users[2]; + }); + + test('should remove empty strings from user data', function () { + const user: z.infer = { + email: '', + username: '', + firstName: '', + lastName: '', + telegram: '', + }; + + const transformedUser: { [key: string]: string | null | string[] | object } = + insertUserSchema.parse(user); + + // Loop through all keys and check if they are not empty strings + for (const key of Object.keys(transformedUser)) { + expect(transformedUser[key]).not.toBe(''); + } + }); + + test('validateUserData returns an error if email already exists', async () => { + userData = { + email: secondUser?.email ?? null, + username: user?.username ?? null, + firstName: user?.firstName ?? null, + lastName: user?.lastName ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await validateUserData(dbPool, user?.id!, userData); + expect(response).toBeDefined(); + expect(response![0]).toEqual(expect.any(String)); + }); + + test('validateUserData returns an error if username already exists', async () => { + userData = { + email: user?.email ?? null, + username: secondUser?.username ?? null, + firstName: user?.firstName ?? null, + lastName: user?.lastName ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await validateUserData(dbPool, user?.id!, userData); + expect(response).toBeDefined(); + expect(response![0]).toEqual(expect.any(String)); + }); + + test('validateUserData returns null if validation is successful', async () => { + userData = { + email: user?.email ?? null, + username: user?.username ?? null, + firstName: 'Some Name' ?? null, + lastName: 'Some Other Name' ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await validateUserData(dbPool, user?.id!, userData); + expect(response).toBeNull(); + }); + + test('upsertUserData returns updated user data if insertion is successful', async () => { + userData = { + email: user?.email ?? null, + username: user?.username ?? null, + firstName: 'Some Name' ?? null, + lastName: 'Some Other Name' ?? null, + telegram: user?.telegram ?? null, + }; + + const response = await upsertUserData(dbPool, user?.id!, userData); + expect(response).toBeDefined(); + expect(Array.isArray(response)).toBe(true); + const updatedUser = response![0]; + expect(updatedUser!.firstName).toBe('Some Name'); + expect(updatedUser!.lastName).toBe('Some Other Name'); + }); + + test('updateUser returns the respective error if validation fails', async () => { + userData = { + email: user?.email ?? null, + username: secondUser?.username ?? null, + firstName: user?.firstName ?? null, + lastName: user?.lastName ?? null, + telegram: user?.telegram ?? null, + }; + const mockData = { + userId: user?.id!, + userData: userData, + }; + + const response = await updateUser(dbPool, mockData); + expect(response.errors).toBeDefined(); + expect(response.errors![0]).toEqual(expect.any(String)); + }); + + test('updateUser returns user data if validation and insertion succeeds', async () => { + userData = { + email: user?.email ?? null, + username: user?.username ?? null, + firstName: 'Some Name' ?? null, + lastName: 'Some Other Name' ?? null, + telegram: user?.telegram ?? null, + }; + const mockData = { + userId: user?.id!, + userData: userData, + }; + + const response = await updateUser(dbPool, mockData); + expect(response.data).toBeDefined(); + expect(response.data![0]!.firstName).toBe('Some Name'); + expect(response.data![0]!.lastName).toBe('Some Other Name'); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); }); }); diff --git a/src/services/users.ts b/src/services/users.ts index 41058eef..8bd0cfc7 100644 --- a/src/services/users.ts +++ b/src/services/users.ts @@ -11,7 +11,7 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; * @param {UserData} userData - The user data to check. * @returns {Promise | null>} - An array of errors if user data conflicts, otherwise null. */ -async function validateUserData( +export async function validateUserData( dbPool: NodePgDatabase, userId: string, userData: UserData, @@ -51,7 +51,7 @@ async function validateUserData( * @param {string} userId - The ID of the user to update. * @param {UserData} userData - The updated user data. */ -async function upsertUserData( +export async function upsertUserData( dbPool: NodePgDatabase, userId: string, userData: UserData, From 553a9488334815d3f7c4f95c0de95bd5febf3dd9 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch Date: Thu, 11 Jul 2024 18:09:33 +0200 Subject: [PATCH 2/4] fix linting --- src/services/users.spec.ts | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/src/services/users.spec.ts b/src/services/users.spec.ts index 8107596b..4873942e 100644 --- a/src/services/users.spec.ts +++ b/src/services/users.spec.ts @@ -4,7 +4,6 @@ import { runMigrations } from '../utils/db/run-migrations'; import { environmentVariables, insertUserSchema } from '../types'; import { cleanup, seed } from '../utils/db/seed'; import { updateUser, upsertUserData, validateUserData } from './users'; -import { eq } from 'drizzle-orm'; import { NodePgDatabase } from 'drizzle-orm/node-postgres'; import { Client } from 'pg'; import { z } from 'zod'; @@ -19,9 +18,8 @@ describe('service: users', () => { lastName: string | null; telegram: string | null; }; - let user: db.User | undefined; - let secondUser: db.User | undefined; - let thirdUser: db.User | undefined; + let user: db.User; + let secondUser: db.User; beforeAll(async () => { const envVariables = environmentVariables.parse(process.env); const initDb = await createDbClient({ @@ -44,9 +42,8 @@ describe('service: users', () => { dbConnection = initDb.client; // seed const { users } = await seed(dbPool); - user = users[0]; - secondUser = users[1]; - thirdUser = users[2]; + user = users[0]!; + secondUser = users[1]!; }); test('should remove empty strings from user data', function () { @@ -76,9 +73,9 @@ describe('service: users', () => { telegram: user?.telegram ?? null, }; - const response = await validateUserData(dbPool, user?.id!, userData); + const response = await validateUserData(dbPool, user?.id, userData); expect(response).toBeDefined(); - expect(response![0]).toEqual(expect.any(String)); + expect(response).toEqual(expect.arrayContaining([expect.any(String)])); }); test('validateUserData returns an error if username already exists', async () => { @@ -90,9 +87,9 @@ describe('service: users', () => { telegram: user?.telegram ?? null, }; - const response = await validateUserData(dbPool, user?.id!, userData); + const response = await validateUserData(dbPool, user?.id, userData); expect(response).toBeDefined(); - expect(response![0]).toEqual(expect.any(String)); + expect(response).toEqual(expect.arrayContaining([expect.any(String)])); }); test('validateUserData returns null if validation is successful', async () => { @@ -104,7 +101,7 @@ describe('service: users', () => { telegram: user?.telegram ?? null, }; - const response = await validateUserData(dbPool, user?.id!, userData); + const response = await validateUserData(dbPool, user?.id, userData); expect(response).toBeNull(); }); @@ -117,7 +114,7 @@ describe('service: users', () => { telegram: user?.telegram ?? null, }; - const response = await upsertUserData(dbPool, user?.id!, userData); + const response = await upsertUserData(dbPool, user?.id, userData); expect(response).toBeDefined(); expect(Array.isArray(response)).toBe(true); const updatedUser = response![0]; @@ -134,7 +131,7 @@ describe('service: users', () => { telegram: user?.telegram ?? null, }; const mockData = { - userId: user?.id!, + userId: user?.id, userData: userData, }; @@ -152,7 +149,7 @@ describe('service: users', () => { telegram: user?.telegram ?? null, }; const mockData = { - userId: user?.id!, + userId: user?.id, userData: userData, }; From b0b47abd0acd98b278141721624671b3222c435f Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch Date: Thu, 11 Jul 2024 18:56:48 +0200 Subject: [PATCH 3/4] test funding mechanism and refactor service --- src/services/funding-mechanism.spec.ts | 61 ++++++++++++++++++++++++++ src/services/funding-mechanism.ts | 22 +++++++--- 2 files changed, 78 insertions(+), 5 deletions(-) create mode 100644 src/services/funding-mechanism.spec.ts diff --git a/src/services/funding-mechanism.spec.ts b/src/services/funding-mechanism.spec.ts new file mode 100644 index 00000000..a80b2820 --- /dev/null +++ b/src/services/funding-mechanism.spec.ts @@ -0,0 +1,61 @@ +import * as db from '../db'; +import { createDbClient } from '../utils/db/create-db-connection'; +import { runMigrations } from '../utils/db/run-migrations'; +import { environmentVariables } from '../types'; +import { cleanup, seed } from '../utils/db/seed'; +import { calculateFunding } from './funding-mechanism'; +import { NodePgDatabase } from 'drizzle-orm/node-postgres'; +import { Client } from 'pg'; + +describe('service: funding-mechanism', () => { + let dbPool: NodePgDatabase; + let dbConnection: Client; + let cycle: db.Cycle | undefined; + let option: db.Option; + let question: db.Question; + + beforeAll(async () => { + const envVariables = environmentVariables.parse(process.env); + const initDb = await createDbClient({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + await runMigrations({ + database: envVariables.DATABASE_NAME, + host: envVariables.DATABASE_HOST, + password: envVariables.DATABASE_PASSWORD, + user: envVariables.DATABASE_USER, + port: envVariables.DATABASE_PORT, + }); + + dbPool = initDb.db; + dbConnection = initDb.client; + const { cycles, questionOptions, forumQuestions } = await seed(dbPool); + cycle = cycles[0]; + option = questionOptions[0]!; + question = forumQuestions[0]!; + }); + + test('calculateFunding returns and error if the query returns no optionData', async () => { + const response = await calculateFunding(dbPool, '00000000-0000-0000-0000-000000000000'); + expect(response.allocatedFunding).toBeNull(); + expect(response.remainingFunding).toBeNull(); + expect(response.error).toEqual(expect.any(String)); + }); + + test('calculateFunding returns the correct funding amount', async () => { + const response = await calculateFunding(dbPool, question?.id); + expect(response.allocatedFunding).toBeDefined(); + expect(response.remainingFunding).toEqual(100000); + expect(response.error).toBeNull(); + }); + + afterAll(async () => { + await cleanup(dbPool); + await dbConnection.end(); + }); +}); diff --git a/src/services/funding-mechanism.ts b/src/services/funding-mechanism.ts index 405801b0..603e25fb 100644 --- a/src/services/funding-mechanism.ts +++ b/src/services/funding-mechanism.ts @@ -14,7 +14,11 @@ import { NodePgDatabase } from 'drizzle-orm/node-postgres'; export async function calculateFunding( dbPool: NodePgDatabase, forumQuestionId: string, -): Promise<{ allocated_funding: { [key: string]: number }; remaining_funding: number }> { +): Promise<{ + allocatedFunding: { [key: string]: number } | null; + remainingFunding: number | null; + error: string | null; +}> { const getOptionData = await dbPool .select({ id: db.options.id, @@ -24,15 +28,23 @@ export async function calculateFunding( .from(db.options) .where(eq(db.options.questionId, forumQuestionId)); - if (!getOptionData) { - throw new Error('Error in query getOptionData'); + if (getOptionData.length === 0) { + return { + allocatedFunding: null, + remainingFunding: null, + error: 'Error in query getOptionData', + }; } const funding = allocateFunding(100000, 10000, getOptionData); if (!funding) { - throw new Error('Error in allocating funding'); + return { allocatedFunding: null, remainingFunding: null, error: 'Error in allocating funding' }; } - return funding; + return { + allocatedFunding: funding.allocated_funding, + remainingFunding: funding.remaining_funding, + error: null, + }; } From 53beac6b00d14c76542d5e0337d54ca0160bba73 Mon Sep 17 00:00:00 2001 From: Martin Benedikt Busch Date: Fri, 12 Jul 2024 10:12:41 +0200 Subject: [PATCH 4/4] resolve lint warnings --- src/services/funding-mechanism.spec.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/services/funding-mechanism.spec.ts b/src/services/funding-mechanism.spec.ts index a80b2820..ee8ddf32 100644 --- a/src/services/funding-mechanism.spec.ts +++ b/src/services/funding-mechanism.spec.ts @@ -10,8 +10,6 @@ import { Client } from 'pg'; describe('service: funding-mechanism', () => { let dbPool: NodePgDatabase; let dbConnection: Client; - let cycle: db.Cycle | undefined; - let option: db.Option; let question: db.Question; beforeAll(async () => { @@ -34,9 +32,7 @@ describe('service: funding-mechanism', () => { dbPool = initDb.db; dbConnection = initDb.client; - const { cycles, questionOptions, forumQuestions } = await seed(dbPool); - cycle = cycles[0]; - option = questionOptions[0]!; + const { forumQuestions } = await seed(dbPool); question = forumQuestions[0]!; });