diff --git a/backend/src/apis/KlicktippController.ts b/backend/src/apis/KlicktippController.ts index 3f7136de2..ebc3d6cef 100644 --- a/backend/src/apis/KlicktippController.ts +++ b/backend/src/apis/KlicktippController.ts @@ -18,6 +18,7 @@ export const klicktippSignIn = async ( firstName?: string, lastName?: string, ): Promise => { + if (!CONFIG.KLICKTIPP) return true const fields = { fieldFirstName: firstName, fieldLastName: lastName, @@ -28,12 +29,14 @@ export const klicktippSignIn = async ( } export const signout = async (email: string, language: string): Promise => { + if (!CONFIG.KLICKTIPP) return true const apiKey = language === 'de' ? CONFIG.KLICKTIPP_APIKEY_DE : CONFIG.KLICKTIPP_APIKEY_EN const result = await klicktippConnector.signoff(apiKey, email) return result } export const unsubscribe = async (email: string): Promise => { + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.unsubscribe(email) @@ -42,6 +45,7 @@ export const unsubscribe = async (email: string): Promise => { } export const getKlickTippUser = async (email: string): Promise => { + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { const subscriberId = await klicktippConnector.subscriberSearch(email) @@ -52,14 +56,17 @@ export const getKlickTippUser = async (email: string): Promise => { } export const loginKlicktippUser = async (): Promise => { + if (!CONFIG.KLICKTIPP) return true return await klicktippConnector.login(CONFIG.KLICKTIPP_USER, CONFIG.KLICKTIPP_PASSWORD) } export const logoutKlicktippUser = async (): Promise => { + if (!CONFIG.KLICKTIPP) return true return await klicktippConnector.logout() } export const untagUser = async (email: string, tagId: string): Promise => { + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.untag(email, tagId) @@ -68,6 +75,7 @@ export const untagUser = async (email: string, tagId: string): Promise } export const tagUser = async (email: string, tagIds: string): Promise => { + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.tag(email, tagIds) @@ -76,6 +84,7 @@ export const tagUser = async (email: string, tagIds: string): Promise = } export const getKlicktippTagMap = async () => { + if (!CONFIG.KLICKTIPP) return true const isLogin = await loginKlicktippUser() if (isLogin) { return await klicktippConnector.tagIndex() diff --git a/backend/src/auth/RIGHTS.ts b/backend/src/auth/RIGHTS.ts index bcdb2f7ec..df4eed8a1 100644 --- a/backend/src/auth/RIGHTS.ts +++ b/backend/src/auth/RIGHTS.ts @@ -5,8 +5,6 @@ export enum RIGHTS { COMMUNITIES = 'COMMUNITIES', LIST_GDT_ENTRIES = 'LIST_GDT_ENTRIES', EXIST_PID = 'EXIST_PID', - GET_KLICKTIPP_USER = 'GET_KLICKTIPP_USER', - GET_KLICKTIPP_TAG_MAP = 'GET_KLICKTIPP_TAG_MAP', UNSUBSCRIBE_NEWSLETTER = 'UNSUBSCRIBE_NEWSLETTER', SUBSCRIBE_NEWSLETTER = 'SUBSCRIBE_NEWSLETTER', TRANSACTION_LIST = 'TRANSACTION_LIST', diff --git a/backend/src/auth/ROLES.ts b/backend/src/auth/ROLES.ts index df1ee0271..bc868a199 100644 --- a/backend/src/auth/ROLES.ts +++ b/backend/src/auth/ROLES.ts @@ -9,8 +9,6 @@ export const ROLE_USER = new Role('user', [ RIGHTS.BALANCE, RIGHTS.LIST_GDT_ENTRIES, RIGHTS.EXIST_PID, - RIGHTS.GET_KLICKTIPP_USER, - RIGHTS.GET_KLICKTIPP_TAG_MAP, RIGHTS.UNSUBSCRIBE_NEWSLETTER, RIGHTS.SUBSCRIBE_NEWSLETTER, RIGHTS.TRANSACTION_LIST, diff --git a/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts b/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts new file mode 100644 index 000000000..717bb8296 --- /dev/null +++ b/backend/src/event/EVENT_NEWSLETTER_SUBSCRIBE.ts @@ -0,0 +1,8 @@ +import { Event as DbEvent } from '@entity/Event' +import { User as DbUser } from '@entity/User' + +import { Event } from './Event' +import { EventType } from './EventType' + +export const EVENT_NEWSLETTER_SUBSCRIBE = async (user: DbUser): Promise => + Event(EventType.NEWSLETTER_SUBSCRIBE, user, user).save() diff --git a/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts b/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts new file mode 100644 index 000000000..f8adc69d1 --- /dev/null +++ b/backend/src/event/EVENT_NEWSLETTER_UNSUBSCRIBE.ts @@ -0,0 +1,8 @@ +import { Event as DbEvent } from '@entity/Event' +import { User as DbUser } from '@entity/User' + +import { Event } from './Event' +import { EventType } from './EventType' + +export const EVENT_NEWSLETTER_UNSUBSCRIBE = async (user: DbUser): Promise => + Event(EventType.NEWSLETTER_UNSUBSCRIBE, user, user).save() diff --git a/backend/src/event/EventType.ts b/backend/src/event/EventType.ts index 959a848f5..b2b6f9322 100644 --- a/backend/src/event/EventType.ts +++ b/backend/src/event/EventType.ts @@ -21,6 +21,8 @@ export enum EventType { EMAIL_ADMIN_CONFIRMATION = 'EMAIL_ADMIN_CONFIRMATION', EMAIL_CONFIRMATION = 'EMAIL_CONFIRMATION', EMAIL_FORGOT_PASSWORD = 'EMAIL_FORGOT_PASSWORD', + NEWSLETTER_SUBSCRIBE = 'NEWSLETTER_SUBSCRIBE', + NEWSLETTER_UNSUBSCRIBE = 'NEWSLETTER_UNSUBSCRIBE', TRANSACTION_SEND = 'TRANSACTION_SEND', TRANSACTION_RECEIVE = 'TRANSACTION_RECEIVE', TRANSACTION_LINK_CREATE = 'TRANSACTION_LINK_CREATE', diff --git a/backend/src/event/Events.ts b/backend/src/event/Events.ts index d217cde28..4f10afbce 100644 --- a/backend/src/event/Events.ts +++ b/backend/src/event/Events.ts @@ -21,6 +21,8 @@ export { EVENT_EMAIL_ACCOUNT_MULTIREGISTRATION } from './EVENT_EMAIL_ACCOUNT_MUL export { EVENT_EMAIL_ADMIN_CONFIRMATION } from './EVENT_EMAIL_ADMIN_CONFIRMATION' export { EVENT_EMAIL_CONFIRMATION } from './EVENT_EMAIL_CONFIRMATION' export { EVENT_EMAIL_FORGOT_PASSWORD } from './EVENT_EMAIL_FORGOT_PASSWORD' +export { EVENT_NEWSLETTER_SUBSCRIBE } from './EVENT_NEWSLETTER_SUBSCRIBE' +export { EVENT_NEWSLETTER_UNSUBSCRIBE } from './EVENT_NEWSLETTER_UNSUBSCRIBE' export { EVENT_TRANSACTION_SEND } from './EVENT_TRANSACTION_SEND' export { EVENT_TRANSACTION_RECEIVE } from './EVENT_TRANSACTION_RECEIVE' export { EVENT_TRANSACTION_LINK_CREATE } from './EVENT_TRANSACTION_LINK_CREATE' diff --git a/backend/src/graphql/resolver/KlicktippResolver.test.ts b/backend/src/graphql/resolver/KlicktippResolver.test.ts new file mode 100644 index 000000000..6a2250bc9 --- /dev/null +++ b/backend/src/graphql/resolver/KlicktippResolver.test.ts @@ -0,0 +1,138 @@ +/* eslint-disable @typescript-eslint/no-unsafe-argument */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-call */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { Event as DbEvent } from '@entity/Event' +import { UserContact } from '@entity/UserContact' +import { GraphQLError } from 'graphql' + +import { cleanDB, resetToken, testEnvironment } from '@test/helpers' +import { logger, i18n as localization } from '@test/testSetup' + +import { EventType } from '@/event/Events' +import { userFactory } from '@/seeds/factory/user' +import { login, subscribeNewsletter, unsubscribeNewsletter } from '@/seeds/graphql/mutations' +import { bibiBloxberg } from '@/seeds/users/bibi-bloxberg' + +let testEnv: any, mutate: any, con: any + +beforeAll(async () => { + testEnv = await testEnvironment(logger, localization) + mutate = testEnv.mutate + con = testEnv.con + await cleanDB() +}) + +afterAll(async () => { + await cleanDB() + await con.close() +}) + +describe('KlicktippResolver', () => { + beforeAll(async () => { + await userFactory(testEnv, bibiBloxberg) + }) + + afterAll(async () => { + await cleanDB() + }) + + describe('subscribeNewsletter', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: subscribeNewsletter, + }) + + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + it('calls API', async () => { + const { + data: { subscribeNewsletter: isSubscribed }, + }: { data: { subscribeNewsletter: boolean } } = await mutate({ + mutation: subscribeNewsletter, + }) + + expect(isSubscribed).toEqual(true) + }) + + it('stores the NEWSLETTER_SUBSCRIBE event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + await expect(DbEvent.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventType.NEWSLETTER_SUBSCRIBE, + affectedUserId: userConatct.user.id, + actingUserId: userConatct.user.id, + }), + ) + }) + }) + }) + + describe('unsubscribeNewsletter', () => { + describe('unauthenticated', () => { + it('returns an error', async () => { + const { errors: errorObjects }: { errors: [GraphQLError] } = await mutate({ + mutation: unsubscribeNewsletter, + }) + + expect(errorObjects).toEqual([new GraphQLError('401 Unauthorized')]) + }) + }) + + describe('authenticated', () => { + beforeAll(async () => { + await mutate({ + mutation: login, + variables: { email: 'bibi@bloxberg.de', password: 'Aa12345_' }, + }) + }) + + afterAll(() => { + resetToken() + }) + + it('calls API', async () => { + const { + data: { unsubscribeNewsletter: isUnsubscribed }, + }: { data: { unsubscribeNewsletter: boolean } } = await mutate({ + mutation: unsubscribeNewsletter, + }) + + expect(isUnsubscribed).toEqual(true) + }) + + it('stores the NEWSLETTER_UNSUBSCRIBE event in the database', async () => { + const userConatct = await UserContact.findOneOrFail( + { email: 'bibi@bloxberg.de' }, + { relations: ['user'] }, + ) + await expect(DbEvent.find()).resolves.toContainEqual( + expect.objectContaining({ + type: EventType.NEWSLETTER_UNSUBSCRIBE, + affectedUserId: userConatct.user.id, + actingUserId: userConatct.user.id, + }), + ) + }) + }) + }) +}) diff --git a/backend/src/graphql/resolver/KlicktippResolver.ts b/backend/src/graphql/resolver/KlicktippResolver.ts index 31bde0581..6875abcc5 100644 --- a/backend/src/graphql/resolver/KlicktippResolver.ts +++ b/backend/src/graphql/resolver/KlicktippResolver.ts @@ -1,33 +1,17 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -import { Resolver, Query, Authorized, Arg, Mutation, Ctx } from 'type-graphql' +import { Resolver, Authorized, Mutation, Ctx } from 'type-graphql' -import { - getKlickTippUser, - getKlicktippTagMap, - unsubscribe, - klicktippSignIn, -} from '@/apis/KlicktippController' +import { unsubscribe, klicktippSignIn } from '@/apis/KlicktippController' import { RIGHTS } from '@/auth/RIGHTS' +import { EVENT_NEWSLETTER_SUBSCRIBE, EVENT_NEWSLETTER_UNSUBSCRIBE } from '@/event/Events' import { Context, getUser } from '@/server/context' @Resolver() export class KlicktippResolver { - @Authorized([RIGHTS.GET_KLICKTIPP_USER]) - @Query(() => String) - async getKlicktippUser(@Arg('email') email: string): Promise { - return await getKlickTippUser(email) - } - - @Authorized([RIGHTS.GET_KLICKTIPP_TAG_MAP]) - @Query(() => String) - async getKlicktippTagMap(): Promise { - return await getKlicktippTagMap() - } - @Authorized([RIGHTS.UNSUBSCRIBE_NEWSLETTER]) @Mutation(() => Boolean) async unsubscribeNewsletter(@Ctx() context: Context): Promise { const user = getUser(context) + await EVENT_NEWSLETTER_UNSUBSCRIBE(user) return unsubscribe(user.emailContact.email) } @@ -35,6 +19,7 @@ export class KlicktippResolver { @Mutation(() => Boolean) async subscribeNewsletter(@Ctx() context: Context): Promise { const user = getUser(context) + await EVENT_NEWSLETTER_SUBSCRIBE(user) return klicktippSignIn(user.emailContact.email, user.language) } } diff --git a/backend/src/middleware/klicktippMiddleware.ts b/backend/src/middleware/klicktippMiddleware.ts index c5d6dd0ff..4c5f8db4f 100644 --- a/backend/src/middleware/klicktippMiddleware.ts +++ b/backend/src/middleware/klicktippMiddleware.ts @@ -7,8 +7,7 @@ import { MiddlewareFn } from 'type-graphql' import { KlickTipp } from '@model/KlickTipp' -import { /* klicktippSignIn, */ getKlickTippUser } from '@/apis/KlicktippController' -import { CONFIG } from '@/config' +import { getKlickTippUser } from '@/apis/KlicktippController' import { klickTippLogger as logger } from '@/server/logger' // export const klicktippRegistrationMiddleware: MiddlewareFn = async ( @@ -32,15 +31,13 @@ export const klicktippNewsletterStateMiddleware: MiddlewareFn = async ( // eslint-disable-next-line n/callback-return const result = await next() let klickTipp = new KlickTipp({ status: 'Unsubscribed' }) - if (CONFIG.KLICKTIPP) { - try { - const klickTippUser = await getKlickTippUser(result.email) - if (klickTippUser) { - klickTipp = new KlickTipp(klickTippUser) - } - } catch (err) { - logger.error(`There is no user for (email='${result.email}') ${err}`) + try { + const klickTippUser = await getKlickTippUser(result.email) + if (klickTippUser) { + klickTipp = new KlickTipp(klickTippUser) } + } catch (err) { + logger.error(`There is no user for (email='${result.email}') ${err}`) } result.klickTipp = klickTipp return result diff --git a/backend/src/seeds/graphql/mutations.ts b/backend/src/seeds/graphql/mutations.ts index b0716ff74..67a01977f 100644 --- a/backend/src/seeds/graphql/mutations.ts +++ b/backend/src/seeds/graphql/mutations.ts @@ -1,14 +1,14 @@ import { gql } from 'graphql-tag' export const subscribeNewsletter = gql` - mutation ($email: String!, $language: String!) { - subscribeNewsletter(email: $email, language: $language) + mutation { + subscribeNewsletter } ` export const unsubscribeNewsletter = gql` - mutation ($email: String!) { - unsubscribeNewsletter(email: $email) + mutation { + unsubscribeNewsletter } `