From a7b02475752173f20c7198a3bbe53ee15e7fa881 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Feb 2022 14:09:58 +0100 Subject: [PATCH 1/8] helper function to call seeds --- database/src/helpers.ts | 16 +++++++++++++++- database/src/index.ts | 27 +++++++++++---------------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/database/src/helpers.ts b/database/src/helpers.ts index 710094548..acd0e6ccd 100644 --- a/database/src/helpers.ts +++ b/database/src/helpers.ts @@ -1,5 +1,6 @@ import CONFIG from './config' import { createPool, PoolConfig } from 'mysql' +import { useSeeding, runSeeder } from 'typeorm-seeding' import { Migration } from 'ts-mysql-migrate' import path from 'path' @@ -31,4 +32,17 @@ const resetDB = async (closePool = false): Promise => { if (closePool) pool.end() } -export { resetDB, pool, migration, initialize } +/* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +const runSeeds = async (seeds: any[]): Promise => { + if (seeds.length > 0) { + await useSeeding({ + root: process.cwd(), + configName: 'ormconfig.js', + }) + for (let i = 0; i < seeds.length; i++) { + await runSeeder(seeds[i]) + } + } +} + +export { resetDB, pool, migration, initialize, runSeeds } diff --git a/database/src/index.ts b/database/src/index.ts index 6e99ac582..40c5913e4 100644 --- a/database/src/index.ts +++ b/database/src/index.ts @@ -1,7 +1,6 @@ import 'reflect-metadata' import prepare from './prepare' import connection from './typeorm/connection' -import { useSeeding, runSeeder } from 'typeorm-seeding' import { CreatePeterLustigSeed } from './seeds/users/peter-lustig.admin.seed' import { CreateBibiBloxbergSeed } from './seeds/users/bibi-bloxberg.seed' import { CreateRaeuberHotzenplotzSeed } from './seeds/users/raeuber-hotzenplotz.seed' @@ -9,7 +8,7 @@ import { CreateBobBaumeisterSeed } from './seeds/users/bob-baumeister.seed' import { CreateStephenHawkingSeed } from './seeds/users/stephen-hawking.seed' import { CreateGarrickOllivanderSeed } from './seeds/users/garrick-ollivander.seed' import { CreateUserSeed } from './seeds/create-user.seed' -import { resetDB, pool, migration } from './helpers' +import { resetDB, pool, migration, runSeeds } from './helpers' const run = async (command: string) => { // Database actions not supported by our migration library @@ -37,20 +36,16 @@ const run = async (command: string) => { break case 'seed': // TODO protect from production - await useSeeding({ - root: process.cwd(), - configName: 'ormconfig.js', - }) - await runSeeder(CreatePeterLustigSeed) - await runSeeder(CreateBibiBloxbergSeed) - await runSeeder(CreateRaeuberHotzenplotzSeed) - await runSeeder(CreateBobBaumeisterSeed) - await runSeeder(CreateStephenHawkingSeed) - // eslint-disable-next-line prefer-spread - Array.apply(null, Array(96)).forEach(async () => { - await runSeeder(CreateUserSeed) - }) - await runSeeder(CreateGarrickOllivanderSeed) + // await runSeeder(CreatePeterLustigSeed) + await runSeeds([ + CreatePeterLustigSeed, + CreateBibiBloxbergSeed, + CreateRaeuberHotzenplotzSeed, + CreateBobBaumeisterSeed, + CreateStephenHawkingSeed, + CreateGarrickOllivanderSeed, + ...Array(96).fill(CreateUserSeed), + ]) break default: throw new Error(`Unsupported command ${command}`) From e60fc4e164da48fd6d99b8a97d5dafd09279ae3f Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Feb 2022 20:05:54 +0100 Subject: [PATCH 2/8] test login --- .../src/graphql/resolver/UserResolver.test.ts | 128 ++++++++++++++++-- backend/test/graphql.ts | 25 ++++ backend/test/helpers.ts | 46 +++++++ 3 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 backend/test/graphql.ts create mode 100644 backend/test/helpers.ts diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index b01c99552..d40648c83 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1,11 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { createTestClient } from 'apollo-server-testing' +import { testEnvironment, resetEntities, createUser } from '../../../test/helpers' import gql from 'graphql-tag' import { GraphQLError } from 'graphql' -import createServer from '../../server/createServer' -import { resetDB, initialize } from '@dbTools/helpers' +import { resetDB } from '@dbTools/helpers' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '../../config' @@ -30,15 +29,25 @@ jest.mock('../../apis/KlicktippController', () => { }) */ -let mutate: any -let con: any +let token: string + +// eslint-disable-next-line @typescript-eslint/no-unused-vars +const headerPushMock = jest.fn((t) => (token = t.value)) + +const context = { + setHeaders: { + push: headerPushMock, + forEach: jest.fn(), + }, +} + +let mutate: any, query: any, con: any beforeAll(async () => { - const server = await createServer({}) - con = server.con - mutate = createTestClient(server.apollo).mutate - await initialize() - await resetDB() + const testEnv = await testEnvironment(context) + mutate = testEnv.mutate + query = testEnv.query + con = testEnv.con }) afterAll(async () => { @@ -78,11 +87,12 @@ describe('UserResolver', () => { let emailOptIn: string beforeAll(async () => { + jest.clearAllMocks() result = await mutate({ mutation, variables }) }) afterAll(async () => { - await resetDB() + await resetEntities([User, LoginEmailOptIn]) }) it('returns success', () => { @@ -225,6 +235,7 @@ describe('UserResolver', () => { setPassword(code: $code, password: $password) } ` + let result: any let emailOptIn: string @@ -243,7 +254,7 @@ describe('UserResolver', () => { }) afterAll(async () => { - await resetDB() + await resetEntities([User, LoginEmailOptIn]) }) it('sets email checked to true', () => { @@ -286,7 +297,7 @@ describe('UserResolver', () => { }) afterAll(async () => { - await resetDB() + await resetEntities([User, LoginEmailOptIn]) }) it('throws an error', () => { @@ -312,7 +323,7 @@ describe('UserResolver', () => { }) afterAll(async () => { - await resetDB() + await resetEntities([User, LoginEmailOptIn]) }) it('throws an error', () => { @@ -324,4 +335,93 @@ describe('UserResolver', () => { }) }) }) + + describe('login', () => { + const loginQuery = gql` + query ($email: String!, $password: String!, $publisherId: Int) { + login(email: $email, password: $password, publisherId: $publisherId) { + email + firstName + lastName + language + coinanimation + klickTipp { + newsletterState + } + hasElopage + publisherId + isAdmin + } + } + ` + + const variables = { + email: 'peter@lustig.de', + password: 'Aa12345_', + publisherId: 1234, + } + + let result: User + + afterAll(async () => { + await resetEntities([User, LoginEmailOptIn]) + }) + + describe('no users in database', () => { + beforeAll(async () => { + result = await query({ query: loginQuery, variables }) + }) + + it('throws an error', () => { + expect(result).toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No user with this credentials')], + }), + ) + }) + }) + + describe('user is in database', () => { + beforeAll(async () => { + await createUser(mutate, { + email: 'peter@lustig.de', + firstName: 'Peter', + lastName: 'Lustig', + language: 'de', + publisherId: 1234, + }) + result = await query({ query: loginQuery, variables }) + }) + + afterAll(async () => { + await resetEntities([User, LoginEmailOptIn]) + }) + + it('returns the user object', () => { + expect(result).toEqual( + expect.objectContaining({ + data: { + login: { + coinanimation: true, + email: 'peter@lustig.de', + firstName: 'Peter', + hasElopage: false, + isAdmin: false, + klickTipp: { + newsletterState: false, + }, + language: 'de', + lastName: 'Lustig', + publisherId: 1234, + }, + }, + }), + ) + }) + + it('sets the token in the header', () => { + expect(headerPushMock).toBeCalledWith({ key: 'token', value: expect.any(String) }) + }) + }) + }) }) diff --git a/backend/test/graphql.ts b/backend/test/graphql.ts new file mode 100644 index 000000000..89393876e --- /dev/null +++ b/backend/test/graphql.ts @@ -0,0 +1,25 @@ +import gql from 'graphql-tag' + +export const createUserMutation = gql` + mutation ( + $email: String! + $firstName: String! + $lastName: String! + $language: String! + $publisherId: Int + ) { + createUser( + email: $email + firstName: $firstName + lastName: $lastName + language: $language + publisherId: $publisherId + ) + } +` + +export const setPasswordMutation = gql` + mutation ($code: String!, $password: String!) { + setPassword(code: $code, password: $password) + } +` diff --git a/backend/test/helpers.ts b/backend/test/helpers.ts new file mode 100644 index 000000000..f3588cd43 --- /dev/null +++ b/backend/test/helpers.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/explicit-module-boundary-types */ + +import { createTestClient } from 'apollo-server-testing' +import createServer from '../src/server/createServer' +import { resetDB, initialize } from '@dbTools/helpers' +import { createUserMutation, setPasswordMutation } from './graphql' +import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' +import { User } from '@entity/User' + +export const testEnvironment = async (context: any) => { + const server = await createServer(context) + const con = server.con + const testClient = createTestClient(server.apollo) + const mutate = testClient.mutate + const query = testClient.query + await initialize() + await resetDB() + return { mutate, query, con } +} + +export const resetEntity = async (entity: any) => { + const items = await entity.find() + if (items.length > 0) { + const ids = items.map((i: any) => i.id) + await entity.delete(ids) + } +} + +export const resetEntities = async (entities: any[]) => { + for (let i = 0; i < entities.length; i++) { + await resetEntity(entities[i]) + } +} + +export const createUser = async (mutate: any, user: any) => { + await mutate({ mutation: createUserMutation, variables: user }) + const dbUser = await User.findOne({ where: { email: user.email } }) + if (!dbUser) throw new Error('Ups, no user found') + const optin = await LoginEmailOptIn.findOne(dbUser.id) + if (!optin) throw new Error('Ups, no optin found') + await mutate({ + mutation: setPasswordMutation, + variables: { password: 'Aa12345_', code: optin.verificationCode }, + }) +} From 6202c336b7cdf29a38412a6b11ca21789df22338 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Thu, 24 Feb 2022 20:18:52 +0100 Subject: [PATCH 3/8] use graphql imports --- .../src/graphql/resolver/UserResolver.test.ts | 51 ++----------------- 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index d40648c83..83626577d 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ import { testEnvironment, resetEntities, createUser } from '../../../test/helpers' +import { createUserMutation, setPasswordMutation } from '../../../test/graphql' import gql from 'graphql-tag' import { GraphQLError } from 'graphql' import { resetDB } from '@dbTools/helpers' @@ -65,30 +66,12 @@ describe('UserResolver', () => { publisherId: 1234, } - const mutation = gql` - mutation ( - $email: String! - $firstName: String! - $lastName: String! - $language: String! - $publisherId: Int - ) { - createUser( - email: $email - firstName: $firstName - lastName: $lastName - language: $language - publisherId: $publisherId - ) - } - ` - let result: any let emailOptIn: string beforeAll(async () => { jest.clearAllMocks() - result = await mutate({ mutation, variables }) + result = await mutate({ mutation: createUserMutation, variables }) }) afterAll(async () => { @@ -160,7 +143,7 @@ describe('UserResolver', () => { describe('email already exists', () => { it('throws an error', async () => { - await expect(mutate({ mutation, variables })).resolves.toEqual( + await expect(mutate({ mutation: createUserMutation, variables })).resolves.toEqual( expect.objectContaining({ errors: [new GraphQLError('User already exists.')], }), @@ -171,7 +154,7 @@ describe('UserResolver', () => { describe('unknown language', () => { it('sets "de" as default language', async () => { await mutate({ - mutation, + mutation: createUserMutation, variables: { ...variables, email: 'bibi@bloxberg.de', language: 'es' }, }) await expect(User.find()).resolves.toEqual( @@ -188,7 +171,7 @@ describe('UserResolver', () => { describe('no publisher id', () => { it('sets publisher id to null', async () => { await mutate({ - mutation, + mutation: createUserMutation, variables: { ...variables, email: 'raeuber@hotzenplotz.de', publisherId: undefined }, }) await expect(User.find()).resolves.toEqual( @@ -204,24 +187,6 @@ describe('UserResolver', () => { }) describe('setPassword', () => { - const createUserMutation = gql` - mutation ( - $email: String! - $firstName: String! - $lastName: String! - $language: String! - $publisherId: Int - ) { - createUser( - email: $email - firstName: $firstName - lastName: $lastName - language: $language - publisherId: $publisherId - ) - } - ` - const createUserVariables = { email: 'peter@lustig.de', firstName: 'Peter', @@ -230,12 +195,6 @@ describe('UserResolver', () => { publisherId: 1234, } - const setPasswordMutation = gql` - mutation ($code: String!, $password: String!) { - setPassword(code: $code, password: $password) - } - ` - let result: any let emailOptIn: string From c5ff18f461518c0a0893c3c067ca424bb6694e8a Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 8 Mar 2022 18:04:55 +0100 Subject: [PATCH 4/8] use @paths --- backend/src/graphql/resolver/UserResolver.test.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 83626577d..9f3d7265f 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1,20 +1,20 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { testEnvironment, resetEntities, createUser } from '../../../test/helpers' -import { createUserMutation, setPasswordMutation } from '../../../test/graphql' +import { testEnvironment, resetEntities, createUser } from '@test/helpers' +import { createUserMutation, setPasswordMutation } from '@test/graphql' import gql from 'graphql-tag' import { GraphQLError } from 'graphql' import { resetDB } from '@dbTools/helpers' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' -import CONFIG from '../../config' -import { sendAccountActivationEmail } from '../../mailer/sendAccountActivationEmail' -// import { klicktippSignIn } from '../../apis/KlicktippController' +import CONFIG from '@/config' +import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' +// import { klicktippSignIn } from '@/apis/KlicktippController' jest.setTimeout(10000) -jest.mock('../../mailer/sendAccountActivationEmail', () => { +jest.mock('@/mailer/sendAccountActivationEmail', () => { return { __esModule: true, sendAccountActivationEmail: jest.fn(), @@ -22,7 +22,7 @@ jest.mock('../../mailer/sendAccountActivationEmail', () => { }) /* -jest.mock('../../apis/KlicktippController', () => { +jest.mock('@/apis/KlicktippController', () => { return { __esModule: true, klicktippSignIn: jest.fn(), From a385f1772c4b4004ce1642b5d63f31cff73ad396 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Tue, 8 Mar 2022 18:19:01 +0100 Subject: [PATCH 5/8] fix test --- backend/jest.config.js | 1 + backend/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/jest.config.js b/backend/jest.config.js index f61917b01..41e464b8e 100644 --- a/backend/jest.config.js +++ b/backend/jest.config.js @@ -11,6 +11,7 @@ module.exports = { '@arg/(.*)': '/src/graphql/arg/$1', '@enum/(.*)': '/src/graphql/enum/$1', '@repository/(.*)': '/src/typeorm/repository/$1', + '@test/(.*)': '/test/$1', '@entity/(.*)': process.env.NODE_ENV === 'development' ? '/../database/entity/$1' diff --git a/backend/tsconfig.json b/backend/tsconfig.json index 26e3706fe..3b2c6a1ef 100644 --- a/backend/tsconfig.json +++ b/backend/tsconfig.json @@ -53,7 +53,8 @@ "@entity/*": ["../database/entity/*"], "@enum/*": ["./src/graphql/enum/*"], "@model/*": ["./src/graphql/model/*"], - "@repository/*": ["./src/typeorm/repository/*"] + "@repository/*": ["./src/typeorm/repository/*"], + "@test/*": ["./test/*"] }, // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ From d4ce46b271bc2e859b629f93feea9cddc2069dde Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 9 Mar 2022 16:34:54 +0100 Subject: [PATCH 6/8] update db version --- backend/src/config/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 82fb9ff2b..4cd428153 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -10,7 +10,7 @@ Decimal.set({ }) const constants = { - DB_VERSION: '0029-clean_transaction_table', + DB_VERSION: '0030-transaction_link', DECAY_START_TIME: new Date('2021-05-13 17:46:31'), // GMT+0 } From 01648aaa0d5d84676bbede8cc9c8a7c4ac313ce9 Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 9 Mar 2022 16:51:17 +0100 Subject: [PATCH 7/8] add soft delete for transaction links --- database/entity/0030-transaction_link/TransactionLink.ts | 5 ++++- database/migrations/0030-transaction_link.ts | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/database/entity/0030-transaction_link/TransactionLink.ts b/database/entity/0030-transaction_link/TransactionLink.ts index bb12277d1..6ea708547 100644 --- a/database/entity/0030-transaction_link/TransactionLink.ts +++ b/database/entity/0030-transaction_link/TransactionLink.ts @@ -1,5 +1,5 @@ import Decimal from 'decimal.js-light' -import { BaseEntity, Entity, PrimaryGeneratedColumn, Column } from 'typeorm' +import { BaseEntity, Entity, PrimaryGeneratedColumn, Column, DeleteDateColumn } from 'typeorm' import { DecimalTransformer } from '../../src/typeorm/DecimalTransformer' @Entity('transaction_links') @@ -41,6 +41,9 @@ export class TransactionLink extends BaseEntity { }) createdAt: Date + @DeleteDateColumn() + deletedAt?: Date | null + @Column({ type: 'datetime', nullable: false, diff --git a/database/migrations/0030-transaction_link.ts b/database/migrations/0030-transaction_link.ts index 4d72cf43b..ee76c980d 100644 --- a/database/migrations/0030-transaction_link.ts +++ b/database/migrations/0030-transaction_link.ts @@ -13,6 +13,7 @@ export async function upgrade(queryFn: (query: string, values?: any[]) => Promis \`memo\` varchar(255) NOT NULL, \`code\` varchar(24) NOT NULL, \`createdAt\` datetime NOT NULL, + \`deletedAt\` datetime DEFAULT NULL, \`validUntil\` datetime NOT NULL, \`showEmail\` boolean NOT NULL DEFAULT false, \`redeemedAt\` datetime, From f96177285471bac61f8533cdc203873ea1ee1f0d Mon Sep 17 00:00:00 2001 From: Moriz Wahl Date: Wed, 9 Mar 2022 16:51:56 +0100 Subject: [PATCH 8/8] add soft delete to transaction link model --- backend/src/graphql/model/TransactionLink.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/src/graphql/model/TransactionLink.ts b/backend/src/graphql/model/TransactionLink.ts index f449a6f9e..0f19df466 100644 --- a/backend/src/graphql/model/TransactionLink.ts +++ b/backend/src/graphql/model/TransactionLink.ts @@ -25,6 +25,9 @@ export class TransactionLink { @Field(() => Date) createdAt: Date + @Field(() => Date, { nullable: true }) + deletedAt: Date | null + @Field(() => Date) validUntil: Date