From d7b201c7c0d50b95a7df3a47d73a3f6c99e85b2e Mon Sep 17 00:00:00 2001 From: Ricardo Espinoza Date: Thu, 6 Jun 2024 16:23:07 -0400 Subject: [PATCH] Add DB access in BE hooks to hooks service --- packages/cli/src/services/hooks.service.ts | 64 ++++++++++--- .../test/unit/services/hooks.service.test.ts | 90 ++++++++++++++++--- 2 files changed, 130 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/services/hooks.service.ts b/packages/cli/src/services/hooks.service.ts index be7629f7335c5..afe688d97b3cc 100644 --- a/packages/cli/src/services/hooks.service.ts +++ b/packages/cli/src/services/hooks.service.ts @@ -5,8 +5,13 @@ import { AuthService } from '@/auth/auth.service'; import type { Response } from 'express'; import { UserRepository } from '@/databases/repositories/user.repository'; import { SettingsRepository } from '@/databases/repositories/settings.repository'; -import type { Settings } from '@oclif/core'; import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; +import type { FindManyOptions, FindOneOptions, FindOptionsWhere } from '@n8n/typeorm'; +import type { WorkflowEntity } from '@/databases/entities/WorkflowEntity'; +import type { CredentialsEntity } from '@/databases/entities/CredentialsEntity'; +import type { Settings } from '@/databases/entities/Settings'; /** * Exposes functionality to be used by the cloud BE hooks. @@ -19,33 +24,70 @@ export class HooksService { private authService: AuthService, private userRepository: UserRepository, private settingsRepository: SettingsRepository, + private workflowRepository: WorkflowRepository, + private credentialsRepository: CredentialsRepository, ) {} /** - * Invites users to the instance. - * This method is used in the cloud BE hooks, to invite members during sign-up + * Invite users to instance during signup */ async inviteUsers(owner: User, attributes: Array<{ email: string; role: AssignableRole }>) { return await this.userService.inviteUsers(owner, attributes); } + /** + * Set the n8n-auth cookie in the response to auto-login + * the user after instance is provisioned + */ issueCookie(res: Response, user: User) { return this.authService.issueCookie(res, user); } - async findInstanceOwner() { - return await this.userRepository.findOne({ where: { role: 'global:owner' } }); - } - - async findUserById(id: string) { - return await this.userRepository.findOne({ where: { id } }); + /** + * Find user in the instance + * 1. To know whether the instance owner is already setup + * 2. To know when to update the user's profile also in cloud + */ + async findOneUser(filter: FindOneOptions) { + return await this.userRepository.findOne(filter); } + /** + * Save instance owner with the cloud signup data + */ async saveUser(user: User) { return await this.userRepository.save(user); } - async updateSettings(key: keyof Settings, value: QueryDeepPartialEntity) { - return await this.settingsRepository.update({ key }, { value: JSON.stringify(value) }); + /** + * Update instance's settings + * 1. To keep the state when users are invited to the instance + */ + async updateSettings(filter: FindOptionsWhere, set: QueryDeepPartialEntity) { + return await this.settingsRepository.update(filter, set); + } + + /** + * Count the number of workflows + * 1. To enforce the active workflow limits in cloud + */ + async workflowCount(filter: FindManyOptions) { + return await this.workflowRepository.count(filter); + } + + /** + * Count the number of credentials + * 1. To enforce the max credential limits in cloud + */ + async credentialCount(filter: FindManyOptions) { + return await this.credentialsRepository.count(filter); + } + + /** + * Count the number of occurrences of a specific key + * 1. To know when to stop attempting to invite users + */ + async settingsCount(filter: FindManyOptions) { + return await this.settingsRepository.count(filter); } } diff --git a/packages/cli/test/unit/services/hooks.service.test.ts b/packages/cli/test/unit/services/hooks.service.test.ts index 5f3debe25ebcc..6b43771072097 100644 --- a/packages/cli/test/unit/services/hooks.service.test.ts +++ b/packages/cli/test/unit/services/hooks.service.test.ts @@ -3,31 +3,95 @@ import { HooksService } from '@/services/hooks.service'; import { User } from '@/databases/entities/User'; import { mockInstance } from '../../shared/mocking'; import { v4 as uuid } from 'uuid'; +import { AuthService } from '@/auth/auth.service'; +import { CredentialsRepository } from '@/databases/repositories/credentials.repository'; +import { SettingsRepository } from '@/databases/repositories/settings.repository'; +import { UserRepository } from '@/databases/repositories/user.repository'; +import { WorkflowRepository } from '@/databases/repositories/workflow.repository'; +import type { Response } from 'express'; + +let hooksService: HooksService; + +const mockedUser = Object.assign(new User(), { + id: uuid(), + password: 'passwordHash', + mfaEnabled: false, + mfaSecret: 'test', + mfaRecoveryCodes: ['test'], + updatedAt: new Date(), +}); describe('HooksService', () => { const userService = mockInstance(UserService); + const authService = mockInstance(AuthService); + const userRepository = mockInstance(UserRepository); + const settingsRepository = mockInstance(SettingsRepository); + const workflowRepository = mockInstance(WorkflowRepository); + const credentialsRepository = mockInstance(CredentialsRepository); + hooksService = new HooksService( + userService, + authService, + userRepository, + settingsRepository, + workflowRepository, + credentialsRepository, + ); beforeEach(() => { jest.clearAllMocks(); }); it('hooksService.inviteUsers should call userService.inviteUsers', async () => { - const hooksService = new HooksService(userService); const usersToInvite: Parameters[1] = [ { email: 'test@n8n.io', role: 'global:member' }, ]; - const mockUser = Object.assign(new User(), { - id: uuid(), - password: 'passwordHash', - mfaEnabled: false, - mfaSecret: 'test', - mfaRecoveryCodes: ['test'], - updatedAt: new Date(), - authIdentities: [], - }); - - await hooksService.inviteUsers(mockUser, usersToInvite); - expect(userService.inviteUsers).toHaveBeenCalledWith(mockUser, usersToInvite); + await hooksService.inviteUsers(mockedUser, usersToInvite); + expect(userService.inviteUsers).toHaveBeenCalledWith(mockedUser, usersToInvite); + }); + + it('hooksService.issueCookie should call authService.issueCookie', async () => { + const res = { + cookie: jest.fn(), + } as unknown as Response; + + hooksService.issueCookie(res, mockedUser); + expect(authService.issueCookie).toHaveBeenCalledWith(res, mockedUser); + }); + + it('hooksService.findOneUser should call userRepository.findOne', async () => { + const filter = { where: { id: '1' } }; + await hooksService.findOneUser(filter); + expect(userRepository.findOne).toHaveBeenCalledWith(filter); + }); + + it('hooksService.saveUser should call userRepository.save', async () => { + await hooksService.saveUser(mockedUser); + expect(userRepository.save).toHaveBeenCalledWith(mockedUser); + }); + + it('hooksService.updateSettings should call settingRepository.update', async () => { + const filter = { key: 'test' }; + const set = { value: 'true' }; + await hooksService.updateSettings(filter, set); + expect(settingsRepository.update).toHaveBeenCalledWith(filter, set); + }); + + it('hooksService.workflowCount should call workflowRepository.count', async () => { + const filter = { where: { active: true } }; + await hooksService.workflowCount(filter); + expect(workflowRepository.count).toHaveBeenCalledWith(filter); + }); + + it('hooksService.credentialCount should call credentialRepository.count', async () => { + const filter = { where: {} }; + await hooksService.credentialCount(filter); + expect(credentialsRepository.count).toHaveBeenCalledWith(filter); + }); + + it('hooksService.settingsCount should call settingsRepository.count', async () => { + const filter = { where: { key: 'test' } }; + await hooksService.settingsCount(filter); + expect(settingsRepository.count).toHaveBeenCalledWith(filter); }); });