diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ee602a343..3d046fcda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -528,7 +528,7 @@ jobs: report_name: Coverage Backend type: lcov result_path: ./backend/coverage/lcov.info - min_coverage: 55 + min_coverage: 58 token: ${{ github.token }} ########################################################################## diff --git a/backend/src/graphql/resolver/UserResolver.test.ts b/backend/src/graphql/resolver/UserResolver.test.ts index 8e5cb299d..c658476a4 100644 --- a/backend/src/graphql/resolver/UserResolver.test.ts +++ b/backend/src/graphql/resolver/UserResolver.test.ts @@ -1,17 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ -import { testEnvironment, headerPushMock, resetToken, cleanDB } from '@test/helpers' +import { testEnvironment, headerPushMock, resetToken, cleanDB, resetEntity } from '@test/helpers' import { userFactory } from '@/seeds/factory/user' import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' -import { createUser, setPassword } from '@/seeds/graphql/mutations' -import { login, logout } from '@/seeds/graphql/queries' +import { createUser, setPassword, forgotPassword, updateUserInfos } from '@/seeds/graphql/mutations' +import { login, logout, verifyLogin, queryOptIn } from '@/seeds/graphql/queries' import { GraphQLError } from 'graphql' import { LoginEmailOptIn } from '@entity/LoginEmailOptIn' import { User } from '@entity/User' import CONFIG from '@/config' import { sendAccountActivationEmail } from '@/mailer/sendAccountActivationEmail' -import { printTimeDuration } from './UserResolver' +import { sendResetPasswordEmail } from '@/mailer/sendResetPasswordEmail' +import { printTimeDuration, activationLink } from './UserResolver' // import { klicktippSignIn } from '@/apis/KlicktippController' @@ -22,6 +23,13 @@ jest.mock('@/mailer/sendAccountActivationEmail', () => { } }) +jest.mock('@/mailer/sendResetPasswordEmail', () => { + return { + __esModule: true, + sendResetPasswordEmail: jest.fn(), + } +}) + /* jest.mock('@/apis/KlicktippController', () => { return { @@ -85,7 +93,7 @@ describe('UserResolver', () => { }) describe('filling all tables', () => { - it('saves the user in login_user table', () => { + it('saves the user in users table', () => { expect(user).toEqual([ { id: expect.any(Number), @@ -413,6 +421,356 @@ describe('UserResolver', () => { }) }) }) + + describe('verifyLogin', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('user exists but is not logged in', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + }) + + afterAll(async () => { + await cleanDB() + }) + + it('throws an error', async () => { + resetToken() + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + + describe('authenticated', () => { + const variables = { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + } + + beforeAll(async () => { + await query({ query: login, variables }) + }) + + afterAll(() => { + resetToken() + }) + + it('returns user object', async () => { + await expect(query({ query: verifyLogin })).resolves.toEqual( + expect.objectContaining({ + data: { + verifyLogin: { + email: 'bibi@bloxberg.de', + firstName: 'Bibi', + lastName: 'Bloxberg', + language: 'de', + coinanimation: true, + klickTipp: { + newsletterState: false, + }, + hasElopage: false, + publisherId: 1234, + isAdmin: null, + }, + }, + }), + ) + }) + }) + }) + }) + + describe('forgotPassword', () => { + const variables = { email: 'bibi@bloxberg.de' } + describe('user is not in DB', () => { + it('returns true', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) + }) + + describe('user exists in DB', () => { + let result: any + let loginEmailOptIn: LoginEmailOptIn[] + + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await resetEntity(LoginEmailOptIn) + result = await mutate({ mutation: forgotPassword, variables }) + loginEmailOptIn = await LoginEmailOptIn.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + it('returns true', async () => { + await expect(result).toEqual( + expect.objectContaining({ + data: { + forgotPassword: true, + }, + }), + ) + }) + + it('sends reset password email', () => { + expect(sendResetPasswordEmail).toBeCalledWith({ + link: activationLink(loginEmailOptIn[0]), + firstName: 'Bibi', + lastName: 'Bloxberg', + email: 'bibi@bloxberg.de', + duration: expect.any(String), + }) + }) + + describe('request reset password again', () => { + it('thows an error', async () => { + await expect(mutate({ mutation: forgotPassword, variables })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('email already sent less than 10 minutes minutes ago')], + }), + ) + }) + }) + }) + }) + + describe('queryOptIn', () => { + let loginEmailOptIn: LoginEmailOptIn[] + + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + loginEmailOptIn = await LoginEmailOptIn.find() + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('wrong optin code', () => { + it('throws an error', async () => { + await expect( + query({ query: queryOptIn, variables: { optIn: 'not-valid' } }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + // keep Whitspace in error message! + new GraphQLError(`Could not find any entity of type "LoginEmailOptIn" matching: { + "verificationCode": "not-valid" +}`), + ], + }), + ) + }) + }) + + describe('correct optin code', () => { + it('returns true', async () => { + await expect( + query({ + query: queryOptIn, + variables: { optIn: loginEmailOptIn[0].verificationCode.toString() }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + queryOptIn: true, + }, + }), + ) + }) + }) + }) + + describe('updateUserInfos', () => { + describe('unauthenticated', () => { + it('throws an error', async () => { + resetToken() + await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('401 Unauthorized')], + }), + ) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + await query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }) + }) + + afterAll(async () => { + await cleanDB() + }) + + it('returns true', async () => { + await expect(mutate({ mutation: updateUserInfos })).resolves.toEqual( + expect.objectContaining({ + data: { + updateUserInfos: true, + }, + }), + ) + }) + + describe('first-name, last-name and language', () => { + it('updates the fields in DB', async () => { + await mutate({ + mutation: updateUserInfos, + variables: { + firstName: 'Benjamin', + lastName: 'Blümchen', + locale: 'en', + }, + }) + await expect(User.findOne()).resolves.toEqual( + expect.objectContaining({ + firstName: 'Benjamin', + lastName: 'Blümchen', + language: 'en', + }), + ) + }) + }) + + describe('language is not valid', () => { + it('thows an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + locale: 'not-valid', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError(`"not-valid" isn't a valid language`)], + }), + ) + }) + }) + + describe('password', () => { + describe('wrong old password', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'wrong password', + passwordNew: 'Aa12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('Old password is invalid')], + }), + ) + }) + }) + + describe('invalid new password', () => { + it('throws an error', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'Aa12345_', + passwordNew: 'Aa12345', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [ + new GraphQLError( + 'Please enter a valid password with at least 8 characters, upper and lower case letters, at least one number and one special character!', + ), + ], + }), + ) + }) + }) + + describe('correct old and new password', () => { + it('returns true', async () => { + await expect( + mutate({ + mutation: updateUserInfos, + variables: { + password: 'Aa12345_', + passwordNew: 'Bb12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { updateUserInfos: true }, + }), + ) + }) + + it('can login wtih new password', async () => { + await expect( + query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Bb12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + data: { + login: expect.objectContaining({ + email: 'bibi@bloxberg.de', + }), + }, + }), + ) + }) + + it('cannot login wtih old password', async () => { + await expect( + query({ + query: login, + variables: { + email: 'bibi@bloxberg.de', + password: 'Aa12345_', + }, + }), + ).resolves.toEqual( + expect.objectContaining({ + errors: [new GraphQLError('No user with this credentials')], + }), + ) + }) + }) + }) + }) + }) }) describe('printTimeDuration', () => { diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 7685268b4..4ab5a901b 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -245,8 +245,6 @@ export class UserResolver { user.hasElopage = await this.hasElopage({ ...context, user: dbUser }) if (!user.hasElopage && publisherId) { user.publisherId = publisherId - // TODO: Check if we can use updateUserInfos - // await this.updateUserInfos({ publisherId }, { pubKey: loginUser.pubKey }) dbUser.publisherId = publisherId DbUser.save(dbUser) } @@ -519,15 +517,7 @@ export class UserResolver { @Mutation(() => Boolean) async updateUserInfos( @Args() - { - firstName, - lastName, - language, - publisherId, - password, - passwordNew, - coinanimation, - }: UpdateUserInfosArgs, + { firstName, lastName, language, password, passwordNew, coinanimation }: UpdateUserInfosArgs, @Ctx() context: Context, ): Promise { const userEntity = getUser(context) @@ -571,11 +561,6 @@ export class UserResolver { userEntity.privKey = encryptedPrivkey } - // Save publisherId only if Elopage is not yet registered - if (publisherId && !(await this.hasElopage(context))) { - userEntity.publisherId = publisherId - } - const queryRunner = getConnection().createQueryRunner() await queryRunner.connect() await queryRunner.startTransaction('READ UNCOMMITTED') diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index 298d56bdb..fc662cf19 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -18,6 +18,12 @@ export const setPassword = gql` } ` +export const forgotPassword = gql` + mutation ($email: String!) { + forgotPassword(email: $email) + } +` + export const updateUserInfos = gql` mutation ( $firstName: String diff --git a/backend/src/seeds/graphql/queries.ts b/backend/src/seeds/graphql/queries.ts index 11a675eeb..76a386953 100644 --- a/backend/src/seeds/graphql/queries.ts +++ b/backend/src/seeds/graphql/queries.ts @@ -43,6 +43,12 @@ export const logout = gql` } ` +export const queryOptIn = gql` + query ($optIn: String!) { + queryOptIn(optIn: $optIn) + } +` + export const transactionsQuery = gql` query ( $currentPage: Int = 1