diff --git a/backend/src/apis/humhub/ExportUsers.ts b/backend/src/apis/humhub/ExportUsers.ts index de33ba779..8a40d480d 100644 --- a/backend/src/apis/humhub/ExportUsers.ts +++ b/backend/src/apis/humhub/ExportUsers.ts @@ -24,7 +24,7 @@ function getUsersPage(page: number, limit: number): Promise<[User[], number]> { /** * @param client - * @returns user map indiced with email + * @returns user map indices with email */ async function loadUsersFromHumHub(client: HumHubClient): Promise> { const start = new Date().getTime() diff --git a/backend/src/apis/humhub/__mocks__/HumHubClient.ts b/backend/src/apis/humhub/__mocks__/HumHubClient.ts new file mode 100644 index 000000000..a11d8f407 --- /dev/null +++ b/backend/src/apis/humhub/__mocks__/HumHubClient.ts @@ -0,0 +1,50 @@ +import { User } from '@entity/User' +import { UserContact } from '@entity/UserContact' + +import { GetUser } from '@/apis/humhub/model/GetUser' +import { PostUser } from '@/apis/humhub/model/PostUser' +import { UsersResponse } from '@/apis/humhub/model/UsersResponse' + +/** + * HumHubClient as singleton class + */ +export class HumHubClient { + // eslint-disable-next-line no-use-before-define + private static instance: HumHubClient + + // eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function + private constructor() {} + + public static getInstance(): HumHubClient { + if (!HumHubClient.instance) { + HumHubClient.instance = new HumHubClient() + } + return HumHubClient.instance + } + + public async users(): Promise { + return Promise.resolve(new UsersResponse()) + } + + public async userByEmail(email: string): Promise { + const user = new User() + user.emailContact = new UserContact() + user.emailContact.email = email + return Promise.resolve(new GetUser(user, 1)) + } + + public async createUser(): Promise { + return Promise.resolve() + } + + public async updateUser(inputUser: PostUser, humhubUserId: number): Promise { + const user = new User() + user.emailContact = new UserContact() + user.emailContact.email = inputUser.account.email + return Promise.resolve(new GetUser(user, humhubUserId)) + } + + public async deleteUser(): Promise { + return Promise.resolve() + } +} diff --git a/backend/src/apis/humhub/__mocks__/syncUser.ts b/backend/src/apis/humhub/__mocks__/syncUser.ts new file mode 100644 index 000000000..7e0660da4 --- /dev/null +++ b/backend/src/apis/humhub/__mocks__/syncUser.ts @@ -0,0 +1,44 @@ +import { User } from '@entity/User' + +import { isHumhubUserIdenticalToDbUser } from '@/apis/humhub/compareHumhubUserDbUser' +import { GetUser } from '@/apis/humhub/model/GetUser' + +export enum ExecutedHumhubAction { + UPDATE, + CREATE, + SKIP, + DELETE, +} +/** + * Trigger action according to conditions + * | User exist on humhub | export to humhub allowed | changes in user data | ACTION + * | true | false | ignored | DELETE + * | true | true | true | UPDATE + * | true | true | false | SKIP + * | false | false | ignored | SKIP + * | false | true | ignored | CREATE + * @param user + * @param humHubClient + * @param humhubUsers + * @returns + */ +export async function syncUser( + user: User, + humhubUsers: Map, +): Promise { + const humhubUser = humhubUsers.get(user.emailContact.email.trim()) + if (humhubUser) { + if (!user.humhubAllowed) { + return Promise.resolve(ExecutedHumhubAction.DELETE) + } + if (!isHumhubUserIdenticalToDbUser(humhubUser, user)) { + // if humhub allowed + return Promise.resolve(ExecutedHumhubAction.UPDATE) + } + } else { + if (user.humhubAllowed) { + return Promise.resolve(ExecutedHumhubAction.CREATE) + } + } + return Promise.resolve(ExecutedHumhubAction.SKIP) +} diff --git a/backend/src/apis/humhub/syncUser.test.ts b/backend/src/apis/humhub/syncUser.test.ts index d99d15156..20a6b2c33 100644 --- a/backend/src/apis/humhub/syncUser.test.ts +++ b/backend/src/apis/humhub/syncUser.test.ts @@ -2,47 +2,16 @@ import { User } from '@entity/User' import { UserContact } from '@entity/UserContact' -import { CONFIG } from '@/config' - -import { HumHubClient } from './HumHubClient' import { GetUser } from './model/GetUser' import { syncUser, ExecutedHumhubAction } from './syncUser' +jest.mock('@/apis/humhub/HumHubClient') + const defaultUser = new User() defaultUser.emailContact = new UserContact() defaultUser.emailContact.email = 'email@gmail.com' -CONFIG.HUMHUB_ACTIVE = true -CONFIG.HUMHUB_API_URL = 'http://localhost' - -let humhubClient: HumHubClient | undefined -let humhubClientSpy: { - createUser: jest.SpyInstance - updateUser: jest.SpyInstance - deleteUser: jest.SpyInstance -} - describe('syncUser function', () => { - beforeAll(() => { - humhubClient = HumHubClient.getInstance() - if (!humhubClient) { - throw new Error('error creating humhub client') - } - humhubClientSpy = { - createUser: jest.spyOn(humhubClient, 'createUser'), - updateUser: jest.spyOn(humhubClient, 'updateUser'), - deleteUser: jest.spyOn(humhubClient, 'deleteUser'), - } - humhubClientSpy.createUser.mockImplementation(() => Promise.resolve()) - humhubClientSpy.updateUser.mockImplementation(() => Promise.resolve()) - humhubClientSpy.deleteUser.mockImplementation(() => Promise.resolve()) - }) - - afterEach(() => { - humhubClientSpy.createUser.mockClear() - humhubClientSpy.updateUser.mockClear() - humhubClientSpy.deleteUser.mockClear() - }) afterAll(() => { jest.resetAllMocks() }) diff --git a/backend/src/graphql/resolver/UserResolver.ts b/backend/src/graphql/resolver/UserResolver.ts index 436c0aa83..33a351730 100644 --- a/backend/src/graphql/resolver/UserResolver.ts +++ b/backend/src/graphql/resolver/UserResolver.ts @@ -79,6 +79,7 @@ import { getKlicktippState } from './util/getKlicktippState' import { Location2Point } from './util/Location2Point' import { setUserRole, deleteUserRole } from './util/modifyUserRole' import { sendUserToGms } from './util/sendUserToGms' +import { syncHumhub } from './util/syncHumhub' import { validateAlias } from './util/validateAlias' const LANGUAGES = ['de', 'en', 'es', 'fr', 'nl'] @@ -670,6 +671,9 @@ export class UserResolver { logger.debug(`gms-user update successfully.`) } } + if (CONFIG.HUMHUB_ACTIVE) { + await syncHumhub(updateUserInfosArgs, user) + } return true } diff --git a/backend/src/graphql/resolver/util/syncHumhub.test.ts b/backend/src/graphql/resolver/util/syncHumhub.test.ts new file mode 100644 index 000000000..c7b187f23 --- /dev/null +++ b/backend/src/graphql/resolver/util/syncHumhub.test.ts @@ -0,0 +1,63 @@ +import { User } from '@entity/User' +import { UserContact } from '@entity/UserContact' + +import { HumHubClient } from '@/apis/humhub/HumHubClient' +import { GetUser } from '@/apis/humhub/model/GetUser' +import { ExecutedHumhubAction } from '@/apis/humhub/syncUser' +import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs' +import { PublishNameType } from '@/graphql/enum/PublishNameType' +import { backendLogger as logger } from '@/server/logger' + +import { syncHumhub } from './syncHumhub' + +jest.mock('@/apis/humhub/HumHubClient') +jest.mock('@/apis/humhub/syncUser') + +const mockUser = new User() +mockUser.humhubAllowed = true +mockUser.emailContact = new UserContact() +mockUser.emailContact.email = 'email@gmail.com' +mockUser.humhubPublishName = PublishNameType.PUBLISH_NAME_FULL +const mockUpdateUserInfosArg = new UpdateUserInfosArgs() +const mockHumHubUser = new GetUser(mockUser, 1) + +describe('syncHumhub', () => { + beforeAll(() => { + // humhubClientMockbBeforeAll() + }) + + beforeEach(() => { + jest.spyOn(logger, 'debug').mockImplementation() + jest.spyOn(logger, 'info').mockImplementation() + jest.spyOn(HumHubClient, 'getInstance') + }) + + afterEach(() => { + // humhubClientMockbAfterEach() + }) + afterAll(() => { + jest.resetAllMocks() + }) + + it('Should not sync if no relevant changes', async () => { + await syncHumhub(mockUpdateUserInfosArg, new User()) + expect(HumHubClient.getInstance).not.toBeCalled() + // language logging from some other place + expect(logger.debug).toBeCalledTimes(4) + expect(logger.info).toBeCalledTimes(0) + }) + + it('Should retrieve user from humhub and sync if relevant changes', async () => { + mockUpdateUserInfosArg.firstName = 'New' // Relevant changes + mockUser.firstName = 'New' + await syncHumhub(mockUpdateUserInfosArg, mockUser) + expect(logger.debug).toHaveBeenCalledTimes(7) // Four language logging calls, two debug calls in function, one for not syncing + expect(logger.info).toHaveBeenLastCalledWith('finished sync user with humhub', { + localId: mockUser.id, + externId: mockHumHubUser.id, + result: ExecutedHumhubAction.UPDATE, + }) + }) + + // Add more test cases as needed... +}) diff --git a/backend/src/graphql/resolver/util/syncHumhub.ts b/backend/src/graphql/resolver/util/syncHumhub.ts new file mode 100644 index 000000000..1ed1af7ab --- /dev/null +++ b/backend/src/graphql/resolver/util/syncHumhub.ts @@ -0,0 +1,42 @@ +import { User } from '@entity/User' + +import { HumHubClient } from '@/apis/humhub/HumHubClient' +import { GetUser } from '@/apis/humhub/model/GetUser' +import { syncUser } from '@/apis/humhub/syncUser' +import { UpdateUserInfosArgs } from '@/graphql/arg/UpdateUserInfosArgs' +import { backendLogger as logger } from '@/server/logger' + +export async function syncHumhub( + updateUserInfosArg: UpdateUserInfosArgs, + user: User, +): Promise { + // check for humhub relevant changes + if ( + !updateUserInfosArg.alias && + !updateUserInfosArg.firstName && + !updateUserInfosArg.lastName && + !updateUserInfosArg.humhubAllowed && + !updateUserInfosArg.humhubPublishName && + !updateUserInfosArg.language + ) { + return + } + logger.debug('changed user-settings relevant for humhub-user update...') + const humhubClient = HumHubClient.getInstance() + if (!humhubClient) { + return + } + logger.debug('retrieve user from humhub') + const humhubUser = await humhubClient.userByEmail(user.emailContact.email) + const humhubUsers = new Map() + if (humhubUser) { + humhubUsers.set(user.emailContact.email, humhubUser) + } + logger.debug('update user at humhub') + const result = await syncUser(user, humhubUsers) + logger.info('finished sync user with humhub', { + localId: user.id, + externId: humhubUser?.id, + result, + }) +}