mirror of
https://github.com/n8n-io/n8n.git
synced 2025-01-11 21:07:28 -08:00
feat(core): Email recipients on resource shared (#8408)
This commit is contained in:
parent
ae06fdeb62
commit
a0a1830696
|
@ -648,7 +648,12 @@ export class InternalHooks {
|
||||||
|
|
||||||
async onUserTransactionalEmail(userTransactionalEmailData: {
|
async onUserTransactionalEmail(userTransactionalEmailData: {
|
||||||
user_id: string;
|
user_id: string;
|
||||||
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
message_type:
|
||||||
|
| 'Reset password'
|
||||||
|
| 'New user invite'
|
||||||
|
| 'Resend invite'
|
||||||
|
| 'Workflow shared'
|
||||||
|
| 'Credentials shared';
|
||||||
public_api: boolean;
|
public_api: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
return await this.telemetry.track(
|
return await this.telemetry.track(
|
||||||
|
@ -737,7 +742,12 @@ export class InternalHooks {
|
||||||
|
|
||||||
async onEmailFailed(failedEmailData: {
|
async onEmailFailed(failedEmailData: {
|
||||||
user: User;
|
user: User;
|
||||||
message_type: 'Reset password' | 'New user invite' | 'Resend invite';
|
message_type:
|
||||||
|
| 'Reset password'
|
||||||
|
| 'New user invite'
|
||||||
|
| 'Resend invite'
|
||||||
|
| 'Workflow shared'
|
||||||
|
| 'Credentials shared';
|
||||||
public_api: boolean;
|
public_api: boolean;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
void Promise.all([
|
void Promise.all([
|
||||||
|
|
|
@ -9,7 +9,7 @@ import { NodeMailer } from './NodeMailer';
|
||||||
import { ApplicationError } from 'n8n-workflow';
|
import { ApplicationError } from 'n8n-workflow';
|
||||||
|
|
||||||
type Template = HandlebarsTemplateDelegate<unknown>;
|
type Template = HandlebarsTemplateDelegate<unknown>;
|
||||||
type TemplateName = 'invite' | 'passwordReset';
|
type TemplateName = 'invite' | 'passwordReset' | 'workflowShared' | 'credentialsShared';
|
||||||
|
|
||||||
const templates: Partial<Record<TemplateName, Template>> = {};
|
const templates: Partial<Record<TemplateName, Template>> = {};
|
||||||
|
|
||||||
|
@ -81,4 +81,50 @@ export class UserManagementMailer {
|
||||||
// No error, just say no email was sent.
|
// No error, just say no email was sent.
|
||||||
return result ?? { emailSent: false };
|
return result ?? { emailSent: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async notifyWorkflowShared({
|
||||||
|
recipientEmails,
|
||||||
|
workflowName,
|
||||||
|
baseUrl,
|
||||||
|
workflowId,
|
||||||
|
sharerFirstName,
|
||||||
|
}: {
|
||||||
|
recipientEmails: string[];
|
||||||
|
workflowName: string;
|
||||||
|
baseUrl: string;
|
||||||
|
workflowId: string;
|
||||||
|
sharerFirstName: string;
|
||||||
|
}) {
|
||||||
|
const populateTemplate = await getTemplate('workflowShared', 'workflowShared.html');
|
||||||
|
|
||||||
|
const result = await this.mailer?.sendMail({
|
||||||
|
emailRecipients: recipientEmails,
|
||||||
|
subject: `${sharerFirstName} has shared an n8n workflow with you`,
|
||||||
|
body: populateTemplate({ workflowName, workflowUrl: `${baseUrl}/workflow/${workflowId}` }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result ?? { emailSent: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async notifyCredentialsShared({
|
||||||
|
sharerFirstName,
|
||||||
|
credentialsName,
|
||||||
|
recipientEmails,
|
||||||
|
baseUrl,
|
||||||
|
}: {
|
||||||
|
sharerFirstName: string;
|
||||||
|
credentialsName: string;
|
||||||
|
recipientEmails: string[];
|
||||||
|
baseUrl: string;
|
||||||
|
}) {
|
||||||
|
const populateTemplate = await getTemplate('credentialsShared', 'credentialsShared.html');
|
||||||
|
|
||||||
|
const result = await this.mailer?.sendMail({
|
||||||
|
emailRecipients: recipientEmails,
|
||||||
|
subject: `${sharerFirstName} has shared an n8n credential with you`,
|
||||||
|
body: populateTemplate({ credentialsName, credentialsListUrl: `${baseUrl}/credentials` }),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result ?? { emailSent: false };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
<p>Hi there,</p>
|
||||||
|
<p><b>"{{ credentialsName }}" credential</b> has been shared with you.</p>
|
||||||
|
<p>To view all the credentials you have access to within n8n, click the following link:</p>
|
||||||
|
<p><a href="{{ credentialsListUrl }}" target="_blank">{{ credentialsListUrl }}</a></p>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<p>Hi there,</p>
|
||||||
|
<p><b>"{{ workflowName }}" workflow</b> has been shared with you.</p>
|
||||||
|
<p>To access the workflow, click the following link:</p>
|
||||||
|
<p><a href="{{ workflowUrl }}" target="_blank">{{ workflowUrl }}</a></p>
|
|
@ -852,6 +852,18 @@ export const schema = {
|
||||||
default: '',
|
default: '',
|
||||||
env: 'N8N_UM_EMAIL_TEMPLATES_PWRESET',
|
env: 'N8N_UM_EMAIL_TEMPLATES_PWRESET',
|
||||||
},
|
},
|
||||||
|
workflowShared: {
|
||||||
|
doc: 'Overrides default HTML template for notifying that a workflow was shared (use full path)',
|
||||||
|
format: String,
|
||||||
|
default: '',
|
||||||
|
env: 'N8N_UM_EMAIL_TEMPLATES_WORKFLOW_SHARED',
|
||||||
|
},
|
||||||
|
credentialsShared: {
|
||||||
|
doc: 'Overrides default HTML template for notifying that credentials were shared (use full path)',
|
||||||
|
format: String,
|
||||||
|
default: '',
|
||||||
|
env: 'N8N_UM_EMAIL_TEMPLATES_CREDENTIALS_SHARED',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authenticationMethod: {
|
authenticationMethod: {
|
||||||
|
|
|
@ -15,6 +15,11 @@ import { NotFoundError } from '@/errors/response-errors/not-found.error';
|
||||||
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
import { UnauthorizedError } from '@/errors/response-errors/unauthorized.error';
|
||||||
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
import { CredentialsRepository } from '@/databases/repositories/credentials.repository';
|
||||||
import * as utils from '@/utils';
|
import * as utils from '@/utils';
|
||||||
|
import { UserRepository } from '@/databases/repositories/user.repository';
|
||||||
|
import { UserManagementMailer } from '@/UserManagement/email';
|
||||||
|
import { UrlService } from '@/services/url.service';
|
||||||
|
import { Logger } from '@/Logger';
|
||||||
|
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
|
||||||
|
|
||||||
export const EECredentialsController = express.Router();
|
export const EECredentialsController = express.Router();
|
||||||
|
|
||||||
|
@ -185,5 +190,37 @@ EECredentialsController.put(
|
||||||
user_ids_sharees_added: newShareeIds,
|
user_ids_sharees_added: newShareeIds,
|
||||||
sharees_removed: amountRemoved,
|
sharees_removed: amountRemoved,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const recipients = await Container.get(UserRepository).getEmailsByIds(newShareeIds);
|
||||||
|
|
||||||
|
if (recipients.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Container.get(UserManagementMailer).notifyCredentialsShared({
|
||||||
|
sharerFirstName: req.user.firstName,
|
||||||
|
credentialsName: credential.name,
|
||||||
|
recipientEmails: recipients.map(({ email }) => email),
|
||||||
|
baseUrl: Container.get(UrlService).getInstanceBaseUrl(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
void Container.get(InternalHooks).onEmailFailed({
|
||||||
|
user: req.user,
|
||||||
|
message_type: 'Credentials shared',
|
||||||
|
public_api: false,
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Container.get(Logger).info('Sent credentials shared email successfully', {
|
||||||
|
sharerId: req.user.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
void Container.get(InternalHooks).onUserTransactionalEmail({
|
||||||
|
user_id: req.user.id,
|
||||||
|
message_type: 'Credentials shared',
|
||||||
|
public_api: false,
|
||||||
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -84,4 +84,14 @@ export class UserRepository extends Repository<User> {
|
||||||
|
|
||||||
return findManyOptions;
|
return findManyOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get emails of users who have completed setup, by user IDs.
|
||||||
|
*/
|
||||||
|
async getEmailsByIds(userIds: string[]) {
|
||||||
|
return await this.find({
|
||||||
|
select: ['email'],
|
||||||
|
where: { id: In(userIds), password: Not(IsNull()) },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,8 @@ import { WorkflowRequest } from './workflow.request';
|
||||||
import { EnterpriseWorkflowService } from './workflow.service.ee';
|
import { EnterpriseWorkflowService } from './workflow.service.ee';
|
||||||
import { WorkflowExecutionService } from './workflowExecution.service';
|
import { WorkflowExecutionService } from './workflowExecution.service';
|
||||||
import { WorkflowSharingService } from './workflowSharing.service';
|
import { WorkflowSharingService } from './workflowSharing.service';
|
||||||
|
import { UserManagementMailer } from '@/UserManagement/email';
|
||||||
|
import { UrlService } from '@/services/url.service';
|
||||||
|
|
||||||
@Service()
|
@Service()
|
||||||
@Authorized()
|
@Authorized()
|
||||||
|
@ -62,6 +64,8 @@ export class WorkflowsController {
|
||||||
private readonly workflowSharingService: WorkflowSharingService,
|
private readonly workflowSharingService: WorkflowSharingService,
|
||||||
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
private readonly sharedWorkflowRepository: SharedWorkflowRepository,
|
||||||
private readonly userRepository: UserRepository,
|
private readonly userRepository: UserRepository,
|
||||||
|
private readonly mailer: UserManagementMailer,
|
||||||
|
private readonly urlService: UrlService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Post('/')
|
@Post('/')
|
||||||
|
@ -401,5 +405,36 @@ export class WorkflowsController {
|
||||||
});
|
});
|
||||||
|
|
||||||
void this.internalHooks.onWorkflowSharingUpdate(workflowId, req.user.id, shareWithIds);
|
void this.internalHooks.onWorkflowSharingUpdate(workflowId, req.user.id, shareWithIds);
|
||||||
|
|
||||||
|
const recipients = await this.userRepository.getEmailsByIds(newShareeIds);
|
||||||
|
|
||||||
|
if (recipients.length === 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.mailer.notifyWorkflowShared({
|
||||||
|
recipientEmails: recipients.map(({ email }) => email),
|
||||||
|
workflowName: workflow.name,
|
||||||
|
workflowId,
|
||||||
|
sharerFirstName: req.user.firstName,
|
||||||
|
baseUrl: this.urlService.getInstanceBaseUrl(),
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
void this.internalHooks.onEmailFailed({
|
||||||
|
user: req.user,
|
||||||
|
message_type: 'Workflow shared',
|
||||||
|
public_api: false,
|
||||||
|
});
|
||||||
|
if (error instanceof Error) {
|
||||||
|
throw new InternalServerError(`Please contact your administrator: ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.info('Sent workflow shared email successfully', { sharerId: req.user.id });
|
||||||
|
|
||||||
|
void this.internalHooks.onUserTransactionalEmail({
|
||||||
|
user_id: req.user.id,
|
||||||
|
message_type: 'Workflow shared',
|
||||||
|
public_api: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,6 +16,8 @@ import { getCredentialOwnerRole, getGlobalMemberRole, getGlobalOwnerRole } from
|
||||||
import { createManyUsers, createUser, createUserShell } from './shared/db/users';
|
import { createManyUsers, createUser, createUserShell } from './shared/db/users';
|
||||||
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
import { SharedCredentialsRepository } from '@db/repositories/sharedCredentials.repository';
|
||||||
import Container from 'typedi';
|
import Container from 'typedi';
|
||||||
|
import { mockInstance } from '../shared/mocking';
|
||||||
|
import { UserManagementMailer } from '@/UserManagement/email';
|
||||||
|
|
||||||
const sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
|
const sharingSpy = jest.spyOn(UserManagementHelpers, 'isSharingEnabled').mockReturnValue(true);
|
||||||
const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] });
|
const testServer = utils.setupTestServer({ endpointGroups: ['credentials'] });
|
||||||
|
@ -27,6 +29,7 @@ let anotherMember: User;
|
||||||
let authOwnerAgent: SuperAgentTest;
|
let authOwnerAgent: SuperAgentTest;
|
||||||
let authAnotherMemberAgent: SuperAgentTest;
|
let authAnotherMemberAgent: SuperAgentTest;
|
||||||
let saveCredential: SaveCredentialFunction;
|
let saveCredential: SaveCredentialFunction;
|
||||||
|
const mailer = mockInstance(UserManagementMailer);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const globalOwnerRole = await getGlobalOwnerRole();
|
const globalOwnerRole = await getGlobalOwnerRole();
|
||||||
|
@ -47,6 +50,10 @@ beforeEach(async () => {
|
||||||
await testDb.truncate(['SharedCredentials', 'Credentials']);
|
await testDb.truncate(['SharedCredentials', 'Credentials']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
// dynamic router switching
|
// dynamic router switching
|
||||||
// ----------------------------------------
|
// ----------------------------------------
|
||||||
|
@ -363,6 +370,8 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
expect(sharedCredential.role.name).toBe('user');
|
expect(sharedCredential.role.name).toBe('user');
|
||||||
expect(sharedCredential.role.scope).toBe('credential');
|
expect(sharedCredential.role.scope).toBe('credential');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should share the credential with the provided userIds', async () => {
|
test('should share the credential with the provided userIds', async () => {
|
||||||
|
@ -400,6 +409,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
|
|
||||||
expect(ownerSharedCredential.role.name).toBe('owner');
|
expect(ownerSharedCredential.role.name).toBe('owner');
|
||||||
expect(ownerSharedCredential.role.scope).toBe('credential');
|
expect(ownerSharedCredential.role.scope).toBe('credential');
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respond 403 for non-existing credentials', async () => {
|
test('should respond 403 for non-existing credentials', async () => {
|
||||||
|
@ -408,6 +418,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
.send({ shareWithIds: [member.id] });
|
.send({ shareWithIds: [member.id] });
|
||||||
|
|
||||||
expect(response.statusCode).toBe(403);
|
expect(response.statusCode).toBe(403);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respond 403 for non-owned credentials for shared members', async () => {
|
test('should respond 403 for non-owned credentials for shared members', async () => {
|
||||||
|
@ -424,6 +435,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
where: { credentialsId: savedCredential.id },
|
where: { credentialsId: savedCredential.id },
|
||||||
});
|
});
|
||||||
expect(sharedCredentials).toHaveLength(2);
|
expect(sharedCredentials).toHaveLength(2);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respond 403 for non-owned credentials for non-shared members sharing with self', async () => {
|
test('should respond 403 for non-owned credentials for non-shared members sharing with self', async () => {
|
||||||
|
@ -439,6 +451,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
where: { credentialsId: savedCredential.id },
|
where: { credentialsId: savedCredential.id },
|
||||||
});
|
});
|
||||||
expect(sharedCredentials).toHaveLength(1);
|
expect(sharedCredentials).toHaveLength(1);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respond 403 for non-owned credentials for non-shared members sharing', async () => {
|
test('should respond 403 for non-owned credentials for non-shared members sharing', async () => {
|
||||||
|
@ -455,6 +468,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
where: { credentialsId: savedCredential.id },
|
where: { credentialsId: savedCredential.id },
|
||||||
});
|
});
|
||||||
expect(sharedCredentials).toHaveLength(1);
|
expect(sharedCredentials).toHaveLength(1);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respond 200 for non-owned credentials for owners', async () => {
|
test('should respond 200 for non-owned credentials for owners', async () => {
|
||||||
|
@ -469,6 +483,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
where: { credentialsId: savedCredential.id },
|
where: { credentialsId: savedCredential.id },
|
||||||
});
|
});
|
||||||
expect(sharedCredentials).toHaveLength(2);
|
expect(sharedCredentials).toHaveLength(2);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should ignore pending sharee', async () => {
|
test('should ignore pending sharee', async () => {
|
||||||
|
@ -487,6 +502,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
|
|
||||||
expect(sharedCredentials).toHaveLength(1);
|
expect(sharedCredentials).toHaveLength(1);
|
||||||
expect(sharedCredentials[0].userId).toBe(owner.id);
|
expect(sharedCredentials[0].userId).toBe(owner.id);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should ignore non-existing sharee', async () => {
|
test('should ignore non-existing sharee', async () => {
|
||||||
|
@ -504,6 +520,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
|
|
||||||
expect(sharedCredentials).toHaveLength(1);
|
expect(sharedCredentials).toHaveLength(1);
|
||||||
expect(sharedCredentials[0].userId).toBe(owner.id);
|
expect(sharedCredentials[0].userId).toBe(owner.id);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should respond 400 if invalid payload is provided', async () => {
|
test('should respond 400 if invalid payload is provided', async () => {
|
||||||
|
@ -515,6 +532,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
]);
|
]);
|
||||||
|
|
||||||
responses.forEach((response) => expect(response.statusCode).toBe(400));
|
responses.forEach((response) => expect(response.statusCode).toBe(400));
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
test('should unshare the credential', async () => {
|
test('should unshare the credential', async () => {
|
||||||
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
const savedCredential = await saveCredential(randomCredentialPayload(), { user: owner });
|
||||||
|
@ -537,6 +555,7 @@ describe('PUT /credentials/:id/share', () => {
|
||||||
|
|
||||||
expect(sharedCredentials).toHaveLength(1);
|
expect(sharedCredentials).toHaveLength(1);
|
||||||
expect(sharedCredentials[0].userId).toBe(owner.id);
|
expect(sharedCredentials[0].userId).toBe(owner.id);
|
||||||
|
expect(mailer.notifyCredentialsShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import {
|
||||||
} from '../shared/db/roles';
|
} from '../shared/db/roles';
|
||||||
import { createUser } from '../shared/db/users';
|
import { createUser } from '../shared/db/users';
|
||||||
import { createWorkflow, getWorkflowSharing, shareWorkflowWithUsers } from '../shared/db/workflows';
|
import { createWorkflow, getWorkflowSharing, shareWorkflowWithUsers } from '../shared/db/workflows';
|
||||||
|
import { UserManagementMailer } from '@/UserManagement/email';
|
||||||
|
|
||||||
let globalMemberRole: Role;
|
let globalMemberRole: Role;
|
||||||
let owner: User;
|
let owner: User;
|
||||||
|
@ -44,6 +45,7 @@ const testServer = utils.setupTestServer({
|
||||||
enabledFeatures: ['feat:sharing'],
|
enabledFeatures: ['feat:sharing'],
|
||||||
});
|
});
|
||||||
const license = testServer.license;
|
const license = testServer.license;
|
||||||
|
const mailer = mockInstance(UserManagementMailer);
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
const globalOwnerRole = await getGlobalOwnerRole();
|
const globalOwnerRole = await getGlobalOwnerRole();
|
||||||
|
@ -70,6 +72,10 @@ beforeEach(async () => {
|
||||||
await testDb.truncate(['Workflow', 'SharedWorkflow', 'WorkflowHistory']);
|
await testDb.truncate(['Workflow', 'SharedWorkflow', 'WorkflowHistory']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe('router should switch based on flag', () => {
|
describe('router should switch based on flag', () => {
|
||||||
let savedWorkflowId: string;
|
let savedWorkflowId: string;
|
||||||
|
|
||||||
|
@ -107,6 +113,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(2);
|
expect(sharedWorkflows).toHaveLength(2);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should succeed when sharing with invalid user-id', async () => {
|
test('PUT /workflows/:id/share should succeed when sharing with invalid user-id', async () => {
|
||||||
|
@ -133,6 +140,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(3);
|
expect(sharedWorkflows).toHaveLength(3);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should override sharing', async () => {
|
test('PUT /workflows/:id/share should override sharing', async () => {
|
||||||
|
@ -154,6 +162,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const secondSharedWorkflows = await getWorkflowSharing(workflow);
|
const secondSharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(secondSharedWorkflows).toHaveLength(2);
|
expect(secondSharedWorkflows).toHaveLength(2);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should allow sharing by the owner of the workflow', async () => {
|
test('PUT /workflows/:id/share should allow sharing by the owner of the workflow', async () => {
|
||||||
|
@ -167,6 +176,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(2);
|
expect(sharedWorkflows).toHaveLength(2);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should allow sharing by the instance owner', async () => {
|
test('PUT /workflows/:id/share should allow sharing by the instance owner', async () => {
|
||||||
|
@ -180,6 +190,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(2);
|
expect(sharedWorkflows).toHaveLength(2);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should not allow sharing by another shared member', async () => {
|
test('PUT /workflows/:id/share should not allow sharing by another shared member', async () => {
|
||||||
|
@ -195,6 +206,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(2);
|
expect(sharedWorkflows).toHaveLength(2);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should not allow sharing with self by another non-shared member', async () => {
|
test('PUT /workflows/:id/share should not allow sharing with self by another non-shared member', async () => {
|
||||||
|
@ -208,6 +220,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(1);
|
expect(sharedWorkflows).toHaveLength(1);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('PUT /workflows/:id/share should not allow sharing by another non-shared member', async () => {
|
test('PUT /workflows/:id/share should not allow sharing by another non-shared member', async () => {
|
||||||
|
@ -223,6 +236,7 @@ describe('PUT /workflows/:id', () => {
|
||||||
|
|
||||||
const sharedWorkflows = await getWorkflowSharing(workflow);
|
const sharedWorkflows = await getWorkflowSharing(workflow);
|
||||||
expect(sharedWorkflows).toHaveLength(1);
|
expect(sharedWorkflows).toHaveLength(1);
|
||||||
|
expect(mailer.notifyWorkflowShared).toHaveBeenCalledTimes(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue