first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
import { createWorkflow, testDb } from '@n8n/backend-test-utils';
|
||||
import { ExecutionMetadataRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
|
||||
import { ExecutionMetadataService } from '@/services/execution-metadata.service';
|
||||
import { createExecution } from '@test-integration/db/executions';
|
||||
|
||||
let executionMetadataRepository: ExecutionMetadataRepository;
|
||||
let executionMetadataService: ExecutionMetadataService;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
|
||||
executionMetadataRepository = Container.get(ExecutionMetadataRepository);
|
||||
executionMetadataService = Container.get(ExecutionMetadataService);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
describe('ProjectService', () => {
|
||||
describe('save', () => {
|
||||
it('should deduplicate entries by exeuctionId and key, keeping the latest one', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const workflow = await createWorkflow();
|
||||
const execution = await createExecution({}, workflow);
|
||||
const key = 'key';
|
||||
const value1 = 'value1';
|
||||
const value2 = 'value2';
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await executionMetadataService.save(execution.id, { [key]: value1 });
|
||||
await executionMetadataService.save(execution.id, { [key]: value2 });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const rows = await executionMetadataRepository.find({
|
||||
where: { executionId: execution.id, key },
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(1);
|
||||
expect(rows[0]).toHaveProperty('value', value2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,441 @@
|
||||
import { LicenseState } from '@n8n/backend-common';
|
||||
import { testDb } from '@n8n/backend-test-utils';
|
||||
import { ProjectRelationRepository, ProjectRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { PROJECT_OWNER_ROLE_SLUG, type ProjectRole, type Scope } from '@n8n/permissions';
|
||||
|
||||
import { License } from '@/license';
|
||||
import { ProjectService } from '@/services/project.service.ee';
|
||||
import { createRole } from '@test-integration/db/roles';
|
||||
|
||||
import { createMember } from '../shared/db/users';
|
||||
import { LicenseMocker } from '@test-integration/license';
|
||||
|
||||
let projectRepository: ProjectRepository;
|
||||
let projectService: ProjectService;
|
||||
let projectRelationRepository: ProjectRelationRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
|
||||
projectRepository = Container.get(ProjectRepository);
|
||||
projectService = Container.get(ProjectService);
|
||||
projectRelationRepository = Container.get(ProjectRelationRepository);
|
||||
const license: LicenseMocker = new LicenseMocker();
|
||||
license.mock(Container.get(License));
|
||||
license.mockLicenseState(Container.get(LicenseState));
|
||||
license.enable('feat:projectRole:editor');
|
||||
license.enable('feat:projectRole:viewer');
|
||||
license.enable('feat:customRoles');
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await testDb.truncate(['User']);
|
||||
});
|
||||
|
||||
describe('ProjectService', () => {
|
||||
describe('addUser', () => {
|
||||
it.each([
|
||||
'project:viewer',
|
||||
'project:admin',
|
||||
'project:editor',
|
||||
PROJECT_OWNER_ROLE_SLUG,
|
||||
] as ProjectRole[])(
|
||||
'creates a relation between the user and the project using the role %s',
|
||||
async (role) => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, { userId: member.id, role });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
await projectRelationRepository.findOneOrFail({
|
||||
where: { userId: member.id, projectId: project.id, role: { slug: role } },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('changes the role the user has in the project if the user is already part of the project, instead of creating a new relationship', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, { userId: member.id, role: 'project:viewer' });
|
||||
|
||||
await projectRelationRepository.findOneOrFail({
|
||||
where: { userId: member.id, projectId: project.id, role: { slug: 'project:viewer' } },
|
||||
});
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, { userId: member.id, role: 'project:admin' });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const relationships = await projectRelationRepository.find({
|
||||
where: { userId: member.id, projectId: project.id },
|
||||
relations: { role: true },
|
||||
});
|
||||
|
||||
expect(relationships).toHaveLength(1);
|
||||
expect(relationships[0]).toHaveProperty('role.slug', 'project:admin');
|
||||
});
|
||||
|
||||
it('adds a user to a project with a custom role', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
const role = await createRole({ slug: 'project:custom', displayName: 'Custom Role' });
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, { userId: member.id, role: role.slug });
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const relationships = await projectRelationRepository.find({
|
||||
where: { userId: member.id, projectId: project.id },
|
||||
relations: { role: true },
|
||||
});
|
||||
|
||||
expect(relationships).toHaveLength(1);
|
||||
expect(relationships[0].role.slug).toBe('project:custom');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getProjectWithScope', () => {
|
||||
it.each([
|
||||
{ role: 'project:admin', scope: 'workflow:list' },
|
||||
{ role: 'project:admin', scope: 'workflow:create' },
|
||||
] as Array<{
|
||||
role: ProjectRole;
|
||||
scope: Scope;
|
||||
}>)(
|
||||
'should return the project if the user has the role $role and wants the scope $scope',
|
||||
async ({ role, scope }) => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const projectOwner = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, { userId: projectOwner.id, role });
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const projectFromService = await projectService.getProjectWithScope(
|
||||
projectOwner,
|
||||
project.id,
|
||||
[scope],
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
if (projectFromService === null) {
|
||||
fail('Expected projectFromService not to be null');
|
||||
}
|
||||
expect(project.id).toBe(projectFromService.id);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ role: 'project:viewer', scope: 'workflow:create' },
|
||||
{ role: 'project:viewer', scope: 'credential:create' },
|
||||
] as Array<{
|
||||
role: ProjectRole;
|
||||
scope: Scope;
|
||||
}>)(
|
||||
'should return the project if the user has the role $role and wants the scope $scope',
|
||||
async ({ role, scope }) => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const projectViewer = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, { userId: projectViewer.id, role });
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const projectFromService = await projectService.getProjectWithScope(
|
||||
projectViewer,
|
||||
project.id,
|
||||
[scope],
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(projectFromService).toBeNull();
|
||||
},
|
||||
);
|
||||
|
||||
it('should not return the project if the user is not part of it', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
const projectFromService = await projectService.getProjectWithScope(member, project.id, [
|
||||
'workflow:list',
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
expect(projectFromService).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteUserFromProject', () => {
|
||||
it('should not allow project owner to be removed from the project', async () => {
|
||||
const role = PROJECT_OWNER_ROLE_SLUG;
|
||||
|
||||
const user = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, { userId: user.id, role });
|
||||
|
||||
await expect(projectService.deleteUserFromProject(project.id, user.id)).rejects.toThrowError(
|
||||
/^Project owner cannot be removed from the project$/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should remove user from project if not owner', async () => {
|
||||
const role = 'project:editor';
|
||||
|
||||
const user = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
await projectService.addUser(project.id, { userId: user.id, role });
|
||||
|
||||
await projectService.deleteUserFromProject(project.id, user.id);
|
||||
|
||||
const relations = await projectRelationRepository.findOne({
|
||||
where: { userId: user.id, projectId: project.id, role: { slug: role } },
|
||||
});
|
||||
|
||||
expect(relations).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('addUsersToProject', () => {
|
||||
it('should add multiple users to a project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const members = await Promise.all([createMember(), createMember()]);
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUsersToProject(
|
||||
project.id,
|
||||
members.map((member) => ({ userId: member.id, role: 'project:editor' })),
|
||||
);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const relations = await projectRelationRepository.find({
|
||||
where: { projectId: project.id },
|
||||
});
|
||||
|
||||
expect(relations).toHaveLength(members.length);
|
||||
});
|
||||
|
||||
it('fails to add a user to a project with a non-existing role', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const member = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await expect(
|
||||
projectService.addUsersToProject(project.id, [
|
||||
{ userId: member.id, role: 'custom:non-existing' },
|
||||
]),
|
||||
).rejects.toThrowError('Role custom:non-existing does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('syncProjectRelations', () => {
|
||||
it('should synchronize project relations for a user', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const user = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.syncProjectRelations(project.id, [
|
||||
{ userId: user.id, role: 'project:editor' },
|
||||
]);
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const relations = await projectRelationRepository.find({
|
||||
where: { userId: user.id, projectId: project.id },
|
||||
});
|
||||
expect(relations).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should fail to synchronize users with non-existing roles', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const user = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await expect(
|
||||
projectService.syncProjectRelations(project.id, [
|
||||
{ userId: user.id, role: 'project:non-existing' },
|
||||
]),
|
||||
).rejects.toThrowError('Role project:non-existing does not exist');
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeUserRoleInProject', () => {
|
||||
it('should change user role in project', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const user = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, { userId: user.id, role: 'project:viewer' });
|
||||
await projectService.changeUserRoleInProject(project.id, user.id, 'project:editor');
|
||||
|
||||
//
|
||||
// ASSERT
|
||||
//
|
||||
const relations = await projectRelationRepository.find({
|
||||
where: { userId: user.id, projectId: project.id },
|
||||
relations: { role: true },
|
||||
});
|
||||
expect(relations).toHaveLength(1);
|
||||
expect(relations[0].role.slug).toBe('project:editor');
|
||||
});
|
||||
|
||||
it('should fail to change user role in project with non-existing role', async () => {
|
||||
//
|
||||
// ARRANGE
|
||||
//
|
||||
const user = await createMember();
|
||||
const project = await projectRepository.save(
|
||||
projectRepository.create({
|
||||
name: 'Team Project',
|
||||
type: 'team',
|
||||
}),
|
||||
);
|
||||
|
||||
//
|
||||
// ACT
|
||||
//
|
||||
await projectService.addUser(project.id, { userId: user.id, role: 'project:viewer' });
|
||||
await expect(
|
||||
projectService.changeUserRoleInProject(project.id, user.id, 'project:non-existing'),
|
||||
).rejects.toThrowError('Role project:non-existing does not exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,61 @@
|
||||
import { createWorkflow, testDb, mockInstance } from '@n8n/backend-test-utils';
|
||||
import { WorkflowRepository } from '@n8n/db';
|
||||
import { Container } from '@n8n/di';
|
||||
import { Workflow } from 'n8n-workflow';
|
||||
|
||||
import { NodeTypes } from '@/node-types';
|
||||
import { WorkflowStaticDataService } from '@/workflows/workflow-static-data.service';
|
||||
|
||||
const nodeTypes = mockInstance(NodeTypes);
|
||||
let workflowStaticDataService: WorkflowStaticDataService;
|
||||
let workflowRepository: WorkflowRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
await testDb.init();
|
||||
|
||||
workflowStaticDataService = Container.get(WorkflowStaticDataService);
|
||||
workflowRepository = Container.get(WorkflowRepository);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testDb.terminate();
|
||||
});
|
||||
|
||||
describe('WorkflowStaticDataService', () => {
|
||||
it('should not change workflow updatedAt when calling saveStaticData', async () => {
|
||||
const workflowEntityOriginal = await createWorkflow();
|
||||
|
||||
const workflow = new Workflow({
|
||||
id: workflowEntityOriginal.id,
|
||||
active: false,
|
||||
connections: {},
|
||||
nodeTypes,
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
workflow.staticData.testValue = 1;
|
||||
|
||||
await workflowStaticDataService.saveStaticData(workflow);
|
||||
|
||||
const workflowEntityNew = await workflowRepository.get({
|
||||
id: workflowEntityOriginal.id,
|
||||
});
|
||||
|
||||
expect(workflowEntityNew?.staticData).toEqual(workflow.staticData);
|
||||
expect(workflowEntityNew?.updatedAt).toEqual(workflowEntityOriginal.updatedAt);
|
||||
});
|
||||
|
||||
it('should not change workflow updatedAt when calling saveStaticDataById', async () => {
|
||||
const workflowEntityOriginal = await createWorkflow();
|
||||
const staticData = { testValue: 1 };
|
||||
|
||||
await workflowStaticDataService.saveStaticDataById(workflowEntityOriginal.id, staticData);
|
||||
|
||||
const workflowEntityNew = await workflowRepository.get({
|
||||
id: workflowEntityOriginal.id,
|
||||
});
|
||||
|
||||
expect(workflowEntityNew?.staticData).toEqual(staticData);
|
||||
expect(workflowEntityNew?.updatedAt).toEqual(workflowEntityOriginal.updatedAt);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user