-
Notifications
You must be signed in to change notification settings - Fork 11.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4918ac8
commit dda7901
Showing
5 changed files
with
285 additions
and
17 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,120 @@ | ||
import { Service } from 'typedi'; | ||
import type { NextFunction, Response } from 'express'; | ||
import type { QueryDeepPartialEntity } from '@n8n/typeorm/query-builder/QueryPartialEntity'; | ||
import type { FindManyOptions, FindOneOptions, FindOptionsWhere } from '@n8n/typeorm'; | ||
|
||
import { AuthService } from '@/auth/auth.service'; | ||
import type { AuthUser } from '@db/entities/AuthUser'; | ||
import type { User } from '@db/entities/User'; | ||
import { UserRepository } from '@db/repositories/user.repository'; | ||
import { SettingsRepository } from '@db/repositories/settings.repository'; | ||
import { WorkflowRepository } from '@db/repositories/workflow.repository'; | ||
import { CredentialsRepository } from '@db/repositories/credentials.repository'; | ||
import type { WorkflowEntity } from '@db/entities/WorkflowEntity'; | ||
import type { CredentialsEntity } from '@db/entities/CredentialsEntity'; | ||
import { AuthUserRepository } from '@db/repositories/authUser.repository'; | ||
import type { Settings } from '@db/entities/Settings'; | ||
import { UserService } from '@/services/user.service'; | ||
import type { AuthenticatedRequest } from '@/requests'; | ||
import type { Invitation } from '@/Interfaces'; | ||
|
||
/** | ||
* Exposes functionality to be used by the cloud BE hooks. | ||
* DO NOT DELETE or RENAME any of the methods without making sure this is not used in cloud BE hooks. | ||
*/ | ||
@Service() | ||
export class HooksService { | ||
constructor( | ||
private readonly userService: UserService, | ||
private readonly authService: AuthService, | ||
private readonly userRepository: UserRepository, | ||
private readonly settingsRepository: SettingsRepository, | ||
private readonly workflowRepository: WorkflowRepository, | ||
private readonly credentialsRepository: CredentialsRepository, | ||
private readonly authUserRepository: AuthUserRepository, | ||
) {} | ||
|
||
/** | ||
* Invite users to instance during signup | ||
*/ | ||
async inviteUsers(owner: User, attributes: Invitation[]) { | ||
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: AuthUser) { | ||
return this.authService.issueCookie(res, user); | ||
} | ||
|
||
/** | ||
* 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<AuthUser>) { | ||
return await this.authUserRepository.findOne(filter); | ||
} | ||
|
||
/** | ||
* Save instance owner with the cloud signup data | ||
*/ | ||
async saveUser(user: User) { | ||
return await this.userRepository.save(user); | ||
} | ||
|
||
/** | ||
* Update instance's settings | ||
* 1. To keep the state when users are invited to the instance | ||
*/ | ||
async updateSettings(filter: FindOptionsWhere<Settings>, set: QueryDeepPartialEntity<Settings>) { | ||
return await this.settingsRepository.update(filter, set); | ||
} | ||
|
||
/** | ||
* Count the number of workflows | ||
* 1. To enforce the active workflow limits in cloud | ||
*/ | ||
async workflowsCount(filter: FindManyOptions<WorkflowEntity>) { | ||
return await this.workflowRepository.count(filter); | ||
} | ||
|
||
/** | ||
* Count the number of credentials | ||
* 1. To enforce the max credential limits in cloud | ||
*/ | ||
async credentialsCount(filter: FindManyOptions<CredentialsEntity>) { | ||
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<Settings>) { | ||
return await this.settingsRepository.count(filter); | ||
} | ||
|
||
/** | ||
* Add auth middleware to routes injected via the hooks | ||
* 1. To authenticate the /proxy routes in the hooks | ||
*/ | ||
async authMiddleware(req: AuthenticatedRequest, res: Response, next: NextFunction) { | ||
return await this.authService.authMiddleware(req, res, next); | ||
} | ||
|
||
/** | ||
* Return repositories to be used in the hooks | ||
* 1. Some self-hosted users rely in the repositories to interact with the DB directly | ||
*/ | ||
dbCollections() { | ||
return { | ||
User: this.userRepository, | ||
Settings: this.settingsRepository, | ||
Credentials: this.credentialsRepository, | ||
Workflow: this.workflowRepository, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
import type { Response } from 'express'; | ||
import { mock } from 'jest-mock-extended'; | ||
|
||
import type { AuthUser } from '@db/entities/AuthUser'; | ||
import type { CredentialsRepository } from '@db/repositories/credentials.repository'; | ||
import type { SettingsRepository } from '@db/repositories/settings.repository'; | ||
import type { UserRepository } from '@db/repositories/user.repository'; | ||
import type { WorkflowRepository } from '@db/repositories/workflow.repository'; | ||
import type { AuthService } from '@/auth/auth.service'; | ||
import type { UserService } from '@/services/user.service'; | ||
import { HooksService } from '@/services/hooks.service'; | ||
import type { Invitation } from '@/Interfaces'; | ||
import type { AuthenticatedRequest } from '@/requests'; | ||
import type { AuthUserRepository } from '@/databases/repositories/authUser.repository'; | ||
|
||
describe('HooksService', () => { | ||
const mockedUser = mock<AuthUser>(); | ||
const userService = mock<UserService>(); | ||
const authService = mock<AuthService>(); | ||
const userRepository = mock<UserRepository>(); | ||
const settingsRepository = mock<SettingsRepository>(); | ||
const workflowRepository = mock<WorkflowRepository>(); | ||
const credentialsRepository = mock<CredentialsRepository>(); | ||
const authUserRepository = mock<AuthUserRepository>(); | ||
const hooksService = new HooksService( | ||
userService, | ||
authService, | ||
userRepository, | ||
settingsRepository, | ||
workflowRepository, | ||
credentialsRepository, | ||
authUserRepository, | ||
); | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
}); | ||
|
||
it('hooksService.inviteUsers should call userService.inviteUsers', async () => { | ||
// ARRANGE | ||
const usersToInvite: Invitation[] = [{ email: '[email protected]', role: 'global:member' }]; | ||
|
||
// ACT | ||
await hooksService.inviteUsers(mockedUser, usersToInvite); | ||
|
||
// ASSERT | ||
expect(userService.inviteUsers).toHaveBeenCalledWith(mockedUser, usersToInvite); | ||
}); | ||
|
||
it('hooksService.issueCookie should call authService.issueCookie', async () => { | ||
// ARRANGE | ||
const res = mock<Response>(); | ||
|
||
// ACT | ||
hooksService.issueCookie(res, mockedUser); | ||
|
||
// ASSERT | ||
expect(authService.issueCookie).toHaveBeenCalledWith(res, mockedUser); | ||
}); | ||
|
||
it('hooksService.findOneUser should call authUserRepository.findOne', async () => { | ||
// ARRANGE | ||
const filter = { where: { id: '1' } }; | ||
|
||
// ACT | ||
await hooksService.findOneUser(filter); | ||
|
||
// ASSERT | ||
expect(authUserRepository.findOne).toHaveBeenCalledWith(filter); | ||
}); | ||
|
||
it('hooksService.saveUser should call userRepository.save', async () => { | ||
// ACT | ||
await hooksService.saveUser(mockedUser); | ||
|
||
// ASSERT | ||
|
||
expect(userRepository.save).toHaveBeenCalledWith(mockedUser); | ||
}); | ||
|
||
it('hooksService.updateSettings should call settingRepository.update', async () => { | ||
// ARRANGE | ||
const filter = { key: 'test' }; | ||
const set = { value: 'true' }; | ||
|
||
// ACT | ||
await hooksService.updateSettings(filter, set); | ||
|
||
// ASSERT | ||
expect(settingsRepository.update).toHaveBeenCalledWith(filter, set); | ||
}); | ||
|
||
it('hooksService.workflowsCount should call workflowRepository.count', async () => { | ||
// ARRANGE | ||
const filter = { where: { active: true } }; | ||
|
||
// ACT | ||
await hooksService.workflowsCount(filter); | ||
|
||
// ASSERT | ||
expect(workflowRepository.count).toHaveBeenCalledWith(filter); | ||
}); | ||
|
||
it('hooksService.credentialsCount should call credentialRepository.count', async () => { | ||
// ARRANGE | ||
const filter = { where: {} }; | ||
|
||
// ACT | ||
await hooksService.credentialsCount(filter); | ||
|
||
// ASSERT | ||
expect(credentialsRepository.count).toHaveBeenCalledWith(filter); | ||
}); | ||
|
||
it('hooksService.settingsCount should call settingsRepository.count', async () => { | ||
// ARRANGE | ||
const filter = { where: { key: 'test' } }; | ||
|
||
// ACT | ||
await hooksService.settingsCount(filter); | ||
|
||
// ASSERT | ||
expect(settingsRepository.count).toHaveBeenCalledWith(filter); | ||
}); | ||
|
||
it('hooksService.authMiddleware should call authService.authMiddleware', async () => { | ||
// ARRANGE | ||
const res = mock<Response>(); | ||
|
||
const req = mock<AuthenticatedRequest>(); | ||
|
||
const next = jest.fn(); | ||
|
||
// ACT | ||
await hooksService.authMiddleware(req, res, next); | ||
|
||
// ASSERT | ||
expect(authService.authMiddleware).toHaveBeenCalledWith(req, res, next); | ||
}); | ||
|
||
it('hooksService.dbCollections should return valid repositories', async () => { | ||
// ACT | ||
const collections = hooksService.dbCollections(); | ||
|
||
// ASSERT | ||
expect(collections).toHaveProperty('User'); | ||
expect(collections).toHaveProperty('Settings'); | ||
expect(collections).toHaveProperty('Credentials'); | ||
expect(collections).toHaveProperty('Workflow'); | ||
}); | ||
}); |