fix(core): Use cached value in retrieval of personal project owner (#11533)

Co-authored-by: Danny Martini <danny@n8n.io>
This commit is contained in:
Iván Ovejero
2024-11-07 13:30:10 +01:00
committed by GitHub
parent 5e6ec3bf23
commit 04029d82a1
3 changed files with 41 additions and 21 deletions

View File

@@ -1,4 +1,5 @@
import { mock } from 'jest-mock-extended'; import { mock } from 'jest-mock-extended';
import { v4 as uuid } from 'uuid';
import { Project } from '@/databases/entities/project'; import { Project } from '@/databases/entities/project';
import { ProjectRelation } from '@/databases/entities/project-relation'; import { ProjectRelation } from '@/databases/entities/project-relation';
@@ -13,12 +14,15 @@ import { OwnershipService } from '@/services/ownership.service';
import { mockCredential, mockProject } from '@test/mock-objects'; import { mockCredential, mockProject } from '@test/mock-objects';
import { mockInstance } from '@test/mocking'; import { mockInstance } from '@test/mocking';
import { CacheService } from '../cache/cache.service';
describe('OwnershipService', () => { describe('OwnershipService', () => {
const userRepository = mockInstance(UserRepository); const userRepository = mockInstance(UserRepository);
const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository); const sharedWorkflowRepository = mockInstance(SharedWorkflowRepository);
const projectRelationRepository = mockInstance(ProjectRelationRepository); const projectRelationRepository = mockInstance(ProjectRelationRepository);
const cacheService = mockInstance(CacheService);
const ownershipService = new OwnershipService( const ownershipService = new OwnershipService(
mock(), cacheService,
userRepository, userRepository,
mock(), mock(),
projectRelationRepository, projectRelationRepository,
@@ -52,22 +56,22 @@ describe('OwnershipService', () => {
}); });
}); });
describe('getProjectOwnerCached()', () => { describe('getPersonalProjectOwnerCached()', () => {
test('should retrieve a project owner', async () => { test('should retrieve a project owner', async () => {
const mockProject = new Project(); // ARRANGE
const mockOwner = new User(); const project = new Project();
const owner = new User();
const projectRelation = Object.assign(new ProjectRelation(), { const projectRelation = new ProjectRelation();
role: 'project:personalOwner', projectRelation.role = 'project:personalOwner';
project: mockProject, (projectRelation.project = project), (projectRelation.user = owner);
user: mockOwner,
});
projectRelationRepository.getPersonalProjectOwners.mockResolvedValueOnce([projectRelation]); projectRelationRepository.getPersonalProjectOwners.mockResolvedValueOnce([projectRelation]);
// ACT
const returnedOwner = await ownershipService.getPersonalProjectOwnerCached('some-project-id'); const returnedOwner = await ownershipService.getPersonalProjectOwnerCached('some-project-id');
expect(returnedOwner).toBe(mockOwner); // ASSERT
expect(returnedOwner).toBe(owner);
}); });
test('should not throw if no project owner found, should return null instead', async () => { test('should not throw if no project owner found, should return null instead', async () => {
@@ -77,6 +81,29 @@ describe('OwnershipService', () => {
expect(owner).toBeNull(); expect(owner).toBeNull();
}); });
test('should not use the repository if the owner was found in the cache', async () => {
// ARRANGE
const project = new Project();
project.id = uuid();
const owner = new User();
owner.id = uuid();
const projectRelation = new ProjectRelation();
projectRelation.role = 'project:personalOwner';
(projectRelation.project = project), (projectRelation.user = owner);
cacheService.getHashValue.mockResolvedValueOnce(owner);
userRepository.create.mockReturnValueOnce(owner);
// ACT
const foundOwner = await ownershipService.getPersonalProjectOwnerCached(project.id);
// ASSERT
expect(cacheService.getHashValue).toHaveBeenCalledTimes(1);
expect(cacheService.getHashValue).toHaveBeenCalledWith('project-owner', project.id);
expect(projectRelationRepository.getPersonalProjectOwners).not.toHaveBeenCalled();
expect(foundOwner).toEqual(owner);
});
}); });
describe('getProjectOwnerCached()', () => { describe('getProjectOwnerCached()', () => {

View File

@@ -45,13 +45,9 @@ export class OwnershipService {
* Personal project ownership is **immutable**. * Personal project ownership is **immutable**.
*/ */
async getPersonalProjectOwnerCached(projectId: string): Promise<User | null> { async getPersonalProjectOwnerCached(projectId: string): Promise<User | null> {
const cachedValue = await this.cacheService.getHashValue<User | null>( const cachedValue = await this.cacheService.getHashValue<User>('project-owner', projectId);
'project-owner',
projectId,
);
if (cachedValue) this.userRepository.create(cachedValue); if (cachedValue) return this.userRepository.create(cachedValue);
if (cachedValue === null) return null;
const ownerRel = await this.projectRelationRepository.getPersonalProjectOwners([projectId]); const ownerRel = await this.projectRelationRepository.getPersonalProjectOwners([projectId]);
const owner = ownerRel[0]?.user ?? null; const owner = ownerRel[0]?.user ?? null;

View File

@@ -1,6 +1,5 @@
import { GlobalConfig } from '@n8n/config'; import { GlobalConfig } from '@n8n/config';
import { type Workflow, type INode, type WorkflowSettings } from 'n8n-workflow'; import { type Workflow, type INode, type WorkflowSettings } from 'n8n-workflow';
import { strict as assert } from 'node:assert';
import { Service } from 'typedi'; import { Service } from 'typedi';
import type { Project } from '@/databases/entities/project'; import type { Project } from '@/databases/entities/project';
@@ -68,11 +67,9 @@ export class SubworkflowPolicyChecker {
const owner = await this.ownershipService.getPersonalProjectOwnerCached(subworkflowProject.id); const owner = await this.ownershipService.getPersonalProjectOwnerCached(subworkflowProject.id);
assert(owner !== null); // only `null` if not personal
return { return {
hasReadAccess, hasReadAccess,
ownerName: owner.firstName + ' ' + owner.lastName, ownerName: owner ? owner.firstName + ' ' + owner.lastName : 'No owner (team project)',
}; };
} }