import { nanoid } from 'nanoid'; import { test, expect } from '../../../fixtures/base'; test.describe('Workflow Sharing', { annotation: [ { type: 'owner', description: 'Identity & Access' }, ], }, () => { test('should share workflow with another user via UI', async ({ n8n, api }) => { const member = await api.publicApi.createUser({ email: `member-${nanoid()}@test.com`, firstName: 'Test', lastName: 'Member', }); await n8n.navigate.toWorkflow('new'); const workflowName = `Test Workflow ${nanoid()}`; await n8n.canvas.setWorkflowName(workflowName); await n8n.canvas.addNode('Manual Trigger'); await n8n.canvas.openShareModal(); await n8n.workflowSharingModal.addUser(member.email); await n8n.workflowSharingModal.save(); const memberN8n = await n8n.start.withUser(member); await memberN8n.navigate.toWorkflows(); await expect(memberN8n.workflows.cards.getWorkflow(workflowName)).toBeVisible(); }); test('should share workflow with another user via API', async ({ api }) => { const member = await api.publicApi.createUser({ email: `member-${nanoid()}@test.com`, firstName: 'Test', lastName: 'Member', }); const workflow = await api.workflows.createWorkflow({ name: `Test Workflow ${nanoid()}`, nodes: [ { id: 'manual-trigger', name: 'Manual Trigger', type: 'n8n-nodes-base.manualTrigger', position: [100, 200], parameters: {}, typeVersion: 1, }, ], connections: {}, }); const memberApi = await api.createApiForUser(member); const memberProject = await memberApi.projects.getMyPersonalProject(); await api.workflows.shareWorkflow(workflow.id, [memberProject.id]); const memberWorkflows = await memberApi.workflows.getWorkflows(); const sharedWorkflow = memberWorkflows.find((w: { id: string }) => w.id === workflow.id); expect(sharedWorkflow).toBeTruthy(); expect(sharedWorkflow?.name).toBe(workflow.name); }); test('should allow shared user to edit shared workflow', async ({ n8n, api }) => { const member = await api.publicApi.createUser({ email: `member-${nanoid()}@test.com`, firstName: 'Test', lastName: 'Member', }); const workflow = await api.workflows.createWorkflow({ name: `Test Workflow ${nanoid()}`, nodes: [ { id: 'manual-trigger', name: 'Manual Trigger', type: 'n8n-nodes-base.manualTrigger', position: [100, 200], parameters: {}, typeVersion: 1, }, ], connections: {}, }); const memberApi = await api.createApiForUser(member); const memberProject = await memberApi.projects.getMyPersonalProject(); await api.workflows.shareWorkflow(workflow.id, [memberProject.id]); const memberN8n = await n8n.start.withUser(member); await memberN8n.navigate.toWorkflow(workflow.id); await expect(memberN8n.canvas.getCanvasNodes()).toHaveCount(1); await memberN8n.canvas.addNode('Code', { action: 'Code in JavaScript', closeNDV: true }); await expect(memberN8n.canvas.getCanvasNodes()).toHaveCount(2); }); test('should prevent access to workflow when user is not shared', async ({ api }) => { await api.enableProjectFeatures(); const member = await api.publicApi.createUser({ email: `member-${nanoid()}@test.com`, firstName: 'Test', lastName: 'Member', }); const workflow = await api.workflows.createWorkflow({ name: `Private Workflow ${nanoid()}`, nodes: [ { id: 'manual-trigger', name: 'Manual Trigger', type: 'n8n-nodes-base.manualTrigger', position: [100, 200], parameters: {}, typeVersion: 1, }, ], connections: {}, }); const memberApi = await api.createApiForUser(member); const response = await memberApi.request.get(`/rest/workflows/${workflow.id}`); // With project features enabled, unauthorized access returns 403 (Forbidden) expect(response.status()).toBe(403); }); });