Files
automatisch/packages/backend/src/models/user.test.js
2025-03-24 18:03:37 +01:00

1800 lines
51 KiB
JavaScript

import { beforeEach, describe, it, expect, vi } from 'vitest';
import { DateTime, Duration } from 'luxon';
import Crypto from 'crypto';
import appConfig from '../config/app.js';
import * as licenseModule from '../helpers/license.ee.js';
import Base from './base.js';
import AccessToken from './access-token.js';
import Config from './config.js';
import Connection from './connection.js';
import Execution from './execution.js';
import Flow from './flow.js';
import Identity from './identity.ee.js';
import Permission from './permission.js';
import Role from './role.js';
import Step from './step.js';
import Subscription from './subscription.ee.js';
import UsageData from './usage-data.ee.js';
import Folder from './folder.js';
import User from './user.js';
import deleteUserQueue from '../queues/delete-user.ee.js';
import emailQueue from '../queues/email.js';
import {
REMOVE_AFTER_30_DAYS_OR_150_JOBS,
REMOVE_AFTER_7_DAYS_OR_50_JOBS,
} from '../helpers/remove-job-configuration.js';
import * as userAbilityModule from '../helpers/user-ability.js';
import { createUser } from '../../test/factories/user.js';
import { createConnection } from '../../test/factories/connection.js';
import { createRole } from '../../test/factories/role.js';
import { createPermission } from '../../test/factories/permission.js';
import { createFlow } from '../../test/factories/flow.js';
import { createStep } from '../../test/factories/step.js';
import { createExecution } from '../../test/factories/execution.js';
import { createSubscription } from '../../test/factories/subscription.js';
import { createUsageData } from '../../test/factories/usage-data.js';
import { createFolder } from '../../test/factories/folder.js';
import { createTemplate } from '../../test/factories/template.js';
import Billing from '../helpers/billing/index.ee.js';
import Template from './template.ee.js';
describe('User model', () => {
it('tableName should return correct name', () => {
expect(User.tableName).toBe('users');
});
it('jsonSchema should have correct validations', () => {
expect(User.jsonSchema).toMatchSnapshot();
});
describe('relationMappings', () => {
it('should return correct associations', () => {
const relationMappings = User.relationMappings();
const expectedRelations = {
accessTokens: {
relation: Base.HasManyRelation,
modelClass: AccessToken,
join: {
from: 'users.id',
to: 'access_tokens.user_id',
},
},
connections: {
relation: Base.HasManyRelation,
modelClass: Connection,
join: {
from: 'users.id',
to: 'connections.user_id',
},
},
flows: {
relation: Base.HasManyRelation,
modelClass: Flow,
join: {
from: 'users.id',
to: 'flows.user_id',
},
},
steps: {
relation: Base.ManyToManyRelation,
modelClass: Step,
join: {
from: 'users.id',
through: {
from: 'flows.user_id',
to: 'flows.id',
},
to: 'steps.flow_id',
},
},
executions: {
relation: Base.ManyToManyRelation,
modelClass: Execution,
join: {
from: 'users.id',
through: {
from: 'flows.user_id',
to: 'flows.id',
},
to: 'executions.flow_id',
},
},
usageData: {
relation: Base.HasManyRelation,
modelClass: UsageData,
join: {
from: 'usage_data.user_id',
to: 'users.id',
},
},
currentUsageData: {
relation: Base.HasOneRelation,
modelClass: UsageData,
join: {
from: 'usage_data.user_id',
to: 'users.id',
},
filter: expect.any(Function),
},
subscriptions: {
relation: Base.HasManyRelation,
modelClass: Subscription,
join: {
from: 'subscriptions.user_id',
to: 'users.id',
},
},
currentSubscription: {
relation: Base.HasOneRelation,
modelClass: Subscription,
join: {
from: 'subscriptions.user_id',
to: 'users.id',
},
filter: expect.any(Function),
},
role: {
relation: Base.HasOneRelation,
modelClass: Role,
join: {
from: 'roles.id',
to: 'users.role_id',
},
},
permissions: {
relation: Base.HasManyRelation,
modelClass: Permission,
join: {
from: 'users.role_id',
to: 'permissions.role_id',
},
},
identities: {
relation: Base.HasManyRelation,
modelClass: Identity,
join: {
from: 'identities.user_id',
to: 'users.id',
},
},
folders: {
relation: Base.HasManyRelation,
modelClass: Folder,
join: {
from: 'users.id',
to: 'folders.user_id',
},
},
};
expect(relationMappings).toStrictEqual(expectedRelations);
});
it('currentUsageData should return the current usage data', () => {
const relations = User.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const orderBySpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.currentUsageData.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
it('currentSubscription should return the current subscription', () => {
const relations = User.relationMappings();
const firstSpy = vi.fn();
const limitSpy = vi.fn().mockImplementation(() => ({
first: firstSpy,
}));
const orderBySpy = vi.fn().mockImplementation(() => ({
limit: limitSpy,
}));
relations.currentSubscription.filter({ orderBy: orderBySpy });
expect(orderBySpy).toHaveBeenCalledWith('created_at', 'desc');
expect(limitSpy).toHaveBeenCalledWith(1);
expect(firstSpy).toHaveBeenCalledOnce();
});
});
it('virtualAttributes should return correct attributes', () => {
const virtualAttributes = User.virtualAttributes;
const expectedAttributes = ['acceptInvitationUrl'];
expect(virtualAttributes).toStrictEqual(expectedAttributes);
});
describe('authorizedFlows', () => {
it('should return user flows with isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userFlow = await createFlow({ userId: user.id });
await createFlow();
expect(await userWithRoleAndPermissions.authorizedFlows).toStrictEqual([
userFlow,
]);
});
it('should return all flows without isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userFlow = await createFlow({ userId: user.id });
const anotherUserFlow = await createFlow();
expect(await userWithRoleAndPermissions.authorizedFlows).toStrictEqual([
userFlow,
anotherUserFlow,
]);
});
it('should throw an authorization error without Flow read permission', async () => {
const user = new User();
expect(() => user.authorizedFlows).toThrowError(
'The user is not authorized!'
);
});
});
describe('authorizedSteps', () => {
it('should return user steps with isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userFlow = await createFlow({ userId: user.id });
const userFlowStep = await createStep({ flowId: userFlow.id });
const anotherUserFlow = await createFlow();
await createStep({ flowId: anotherUserFlow.id });
expect(await userWithRoleAndPermissions.authorizedSteps).toStrictEqual([
userFlowStep,
]);
});
it('should return all steps without isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userFlow = await createFlow({ userId: user.id });
const userFlowStep = await createStep({ flowId: userFlow.id });
const anotherUserFlow = await createFlow();
const anotherUserFlowStep = await createStep({
flowId: anotherUserFlow.id,
});
expect(await userWithRoleAndPermissions.authorizedSteps).toStrictEqual([
userFlowStep,
anotherUserFlowStep,
]);
});
it('should throw an authorization error without Flow read permission', async () => {
const user = new User();
expect(() => user.authorizedSteps).toThrowError(
'The user is not authorized!'
);
});
});
describe('authorizedConnections', () => {
it('should return user connections with isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Connection',
action: 'read',
conditions: ['isCreator'],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userConnection = await createConnection({ userId: user.id });
await createConnection();
expect(
await userWithRoleAndPermissions.authorizedConnections
).toStrictEqual([userConnection]);
});
it('should return all connections without isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Connection',
action: 'read',
conditions: [],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userConnection = await createConnection({ userId: user.id });
const anotherUserConnection = await createConnection();
expect(
await userWithRoleAndPermissions.authorizedConnections.orderBy(
'created_at',
'asc'
)
).toStrictEqual([userConnection, anotherUserConnection]);
});
it('should throw an authorization error without Connection read permission', async () => {
const user = new User();
expect(() => user.authorizedConnections).toThrowError(
'The user is not authorized!'
);
});
});
describe('authorizedExecutions', () => {
it('should return user executions with isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Execution',
action: 'read',
conditions: ['isCreator'],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userFlow = await createFlow({ userId: user.id });
const userFlowExecution = await createExecution({ flowId: userFlow.id });
await createExecution();
expect(
await userWithRoleAndPermissions.authorizedExecutions
).toStrictEqual([userFlowExecution]);
});
it('should return all executions without isCreator condition', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Execution',
action: 'read',
conditions: [],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
const userFlow = await createFlow({ userId: user.id });
const userFlowExecution = await createExecution({ flowId: userFlow.id });
const anotherUserFlowExecution = await createExecution();
expect(
await userWithRoleAndPermissions.authorizedExecutions
).toStrictEqual([userFlowExecution, anotherUserFlowExecution]);
});
it('should throw an authorization error without Execution read permission', async () => {
const user = new User();
expect(() => user.authorizedExecutions).toThrowError(
'The user is not authorized!'
);
});
});
it('acceptInvitationUrl should return accept invitation page URL with invitation token', async () => {
const user = new User();
user.invitationToken = 'invitation-token';
vi.spyOn(appConfig, 'webAppUrl', 'get').mockReturnValue(
'https://automatisch.io'
);
expect(user.acceptInvitationUrl).toBe(
'https://automatisch.io/accept-invitation?token=invitation-token'
);
});
it('ability should return userAbility for the user', () => {
const user = new User();
user.fullName = 'Sample user';
const userAbilitySpy = vi
.spyOn(userAbilityModule, 'default')
.mockReturnValue('user-ability');
expect(user.ability).toStrictEqual('user-ability');
expect(userAbilitySpy).toHaveBeenNthCalledWith(1, user);
});
describe('authenticate', () => {
it('should create and return the token for correct email and password', async () => {
const user = await createUser({
email: 'test-user@automatisch.io',
password: 'sample-password',
});
const token = await User.authenticate(
'test-user@automatisch.io',
'sample-password'
);
const persistedToken = await AccessToken.query().findOne({
userId: user.id,
});
expect(token).toBe(persistedToken.token);
});
it('should return undefined for existing email and incorrect password', async () => {
await createUser({
email: 'test-user@automatisch.io',
password: 'sample-password',
});
const token = await User.authenticate(
'test-user@automatisch.io',
'wrong-password'
);
expect(token).toBe(undefined);
});
it('should return undefined for non-existing email', async () => {
await createUser({
email: 'test-user@automatisch.io',
password: 'sample-password',
});
const token = await User.authenticate('non-existing-user@automatisch.io');
expect(token).toBe(undefined);
});
});
describe('login', () => {
it('should return true when the given password matches with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
expect(await user.login('sample-password')).toBe(true);
});
it('should return false when the given password does not match with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
expect(await user.login('wrong-password')).toBe(false);
});
});
it('generateResetPasswordToken should persist a random reset password token with the current date', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 11, 15, 17, 0, 0);
vi.setSystemTime(date);
const user = await createUser({
resetPasswordToken: null,
resetPasswordTokenSentAt: null,
});
await user.generateResetPasswordToken();
const refetchedUser = await user.$query();
expect(refetchedUser.resetPasswordToken.length).toBe(128);
expect(refetchedUser.resetPasswordTokenSentAt).toStrictEqual(date);
vi.useRealTimers();
});
it('generateInvitationToken should persist a random invitation token with the current date', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 11, 15, 26, 0, 0);
vi.setSystemTime(date);
const user = await createUser({
invitationToken: null,
invitationTokenSentAt: null,
});
await user.generateInvitationToken();
const refetchedUser = await user.$query();
expect(refetchedUser.invitationToken.length).toBe(128);
expect(refetchedUser.invitationTokenSentAt).toStrictEqual(date);
vi.useRealTimers();
});
it('resetPassword should persist given password and remove reset password token', async () => {
const user = await createUser({
resetPasswordToken: 'reset-password-token',
resetPasswordTokenSentAt: '2024-11-11T12:26:00.000Z',
});
await user.resetPassword('new-password');
const refetchedUser = await user.$query();
expect(refetchedUser.resetPasswordToken).toBe(null);
expect(refetchedUser.resetPasswordTokenSentAt).toBe(null);
expect(await refetchedUser.login('new-password')).toBe(true);
});
it('acceptInvitation should persist given password, set user active and remove invitation token', async () => {
const user = await createUser({
invitationToken: 'invitation-token',
invitationTokenSentAt: '2024-11-11T12:26:00.000Z',
status: 'invited',
});
await user.acceptInvitation('new-password');
const refetchedUser = await user.$query();
expect(refetchedUser.invitationToken).toBe(null);
expect(refetchedUser.invitationTokenSentAt).toBe(null);
expect(refetchedUser.status).toBe('active');
});
describe('updatePassword', () => {
it('should update password when the given current password matches with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
const updatedUser = await user.updatePassword({
currentPassword: 'sample-password',
password: 'new-password',
});
expect(await updatedUser.login('new-password')).toBe(true);
});
it('should throw validation error when the given current password does not match with the user password', async () => {
const user = await createUser({ password: 'sample-password' });
await expect(
user.updatePassword({
currentPassword: 'wrong-password',
password: 'new-password',
})
).rejects.toThrowError('currentPassword: is incorrect.');
});
});
it('softRemove should soft remove the user, its associations and queue it for hard deletion in 30 days', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 12, 12, 50, 0, 0);
vi.setSystemTime(date);
const user = await createUser();
const softRemoveAssociationsSpy = vi
.spyOn(user, 'softRemoveAssociations')
.mockReturnValue();
const deleteUserQueueAddSpy = vi
.spyOn(deleteUserQueue, 'add')
.mockResolvedValue();
await user.softRemove();
const refetchedSoftDeletedUser = await user.$query().withSoftDeleted();
const millisecondsFor30Days = Duration.fromObject({ days: 30 }).toMillis();
const jobName = `Delete user - ${user.id}`;
const jobPayload = { id: user.id };
const jobOptions = {
delay: millisecondsFor30Days,
};
expect(softRemoveAssociationsSpy).toHaveBeenCalledOnce();
expect(refetchedSoftDeletedUser.deletedAt).toStrictEqual(date);
expect(deleteUserQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
it.todo('softRemoveAssociations');
it('sendResetPasswordEmail should generate reset password token and queue to send reset password email', async () => {
vi.useFakeTimers();
const date = new Date(2024, 10, 12, 14, 33, 0, 0);
vi.setSystemTime(date);
const user = await createUser();
const generateResetPasswordTokenSpy = vi
.spyOn(user, 'generateResetPasswordToken')
.mockReturnValue();
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
await user.sendResetPasswordEmail();
const refetchedUser = await user.$query();
const jobName = `Reset Password Email - ${user.id}`;
const jobPayload = {
email: refetchedUser.email,
subject: 'Reset Password',
template: 'reset-password-instructions.ee',
params: {
token: refetchedUser.resetPasswordToken,
webAppUrl: appConfig.webAppUrl,
fullName: refetchedUser.fullName,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
expect(generateResetPasswordTokenSpy).toHaveBeenCalledOnce();
expect(emailQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
describe('isResetPasswordTokenValid', () => {
it('should return true when resetPasswordTokenSentAt is within the next four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.resetPasswordTokenSentAt = '2024-11-12T13:31:00.000Z';
expect(user.isResetPasswordTokenValid()).toBe(true);
vi.useRealTimers();
});
it('should return false when there is no resetPasswordTokenSentAt', async () => {
const user = new User();
expect(user.isResetPasswordTokenValid()).toBe(false);
});
it('should return false when resetPasswordTokenSentAt is older than four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 16, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.resetPasswordTokenSentAt = '2024-11-12T12:29:00.000Z';
expect(user.isResetPasswordTokenValid()).toBe(false);
vi.useRealTimers();
});
});
it('sendInvitationEmail should generate invitation token and queue to send invitation email', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 17, minute: 10 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = await createUser();
const generateInvitationTokenSpy = vi
.spyOn(user, 'generateInvitationToken')
.mockReturnValue();
const emailQueueAddSpy = vi.spyOn(emailQueue, 'add').mockResolvedValue();
await user.sendInvitationEmail();
const refetchedUser = await user.$query();
const jobName = `Invitation Email - ${refetchedUser.id}`;
const jobPayload = {
email: refetchedUser.email,
subject: 'You are invited!',
template: 'invitation-instructions',
params: {
fullName: refetchedUser.fullName,
acceptInvitationUrl: refetchedUser.acceptInvitationUrl,
},
};
const jobOptions = {
removeOnComplete: REMOVE_AFTER_7_DAYS_OR_50_JOBS,
removeOnFail: REMOVE_AFTER_30_DAYS_OR_150_JOBS,
};
expect(generateInvitationTokenSpy).toHaveBeenCalledOnce();
expect(emailQueueAddSpy).toHaveBeenCalledWith(
jobName,
jobPayload,
jobOptions
);
vi.useRealTimers();
});
describe('isInvitationTokenValid', () => {
it('should return truen when invitationTokenSentAt is within the next four hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.invitationTokenSentAt = '2024-11-14T13:31:00.000Z';
expect(user.isInvitationTokenValid()).toBe(true);
vi.useRealTimers();
});
it('should return false when there is no invitationTokenSentAt', async () => {
const user = new User();
expect(user.isInvitationTokenValid()).toBe(false);
});
it('should return false when invitationTokenSentAt is older than seventy two hours', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 14, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.invitationTokenSentAt = '2024-11-11T14:20:00.000Z';
expect(user.isInvitationTokenValid()).toBe(false);
vi.useRealTimers();
});
});
describe('generateHash', () => {
it('should hash password and re-assign it', async () => {
const user = new User();
user.password = 'sample-password';
await user.generateHash();
expect(user.password).not.toBe('sample-password');
expect(await user.login('sample-password')).toBe(true);
});
it('should do nothing when password does not exist', async () => {
const user = new User();
await user.generateHash();
expect(user.password).toBe(undefined);
});
});
it('startTrialPeriod should assign trialExpiryDate 30 days from now', () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 14, hour: 16 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = new User();
user.startTrialPeriod();
expect(user.trialExpiryDate).toBe('2024-12-14');
vi.useRealTimers();
});
describe('isAllowedToRunFlows', () => {
it('should return true when Automatisch is self hosted', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return true when the user is in trial', async () => {
const user = new User();
vi.spyOn(user, 'inTrial').mockResolvedValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return true when the user has active subscription and within quota limits', async () => {
const user = new User();
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
vi.spyOn(user, 'withinLimits').mockResolvedValue(true);
expect(await user.isAllowedToRunFlows()).toBe(true);
});
it('should return false when the user has active subscription over quota limits', async () => {
const user = new User();
vi.spyOn(user, 'hasActiveSubscription').mockResolvedValue(true);
vi.spyOn(user, 'withinLimits').mockResolvedValue(false);
expect(await user.isAllowedToRunFlows()).toBe(false);
});
it('should return false otherwise', async () => {
const user = new User();
expect(await user.isAllowedToRunFlows()).toBe(false);
});
});
describe('inTrial', () => {
it('should return false when Automatisch is self hosted', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(true);
expect(await user.inTrial()).toBe(false);
});
it('should return false when the user does not have trial expiry date', async () => {
const user = new User();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
expect(await user.inTrial()).toBe(false);
});
it('should return false when the user has an active subscription', async () => {
const user = new User();
user.trialExpiryDate = '2024-12-14';
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
const hasActiveSubscriptionSpy = vi
.spyOn(user, 'hasActiveSubscription')
.mockResolvedValue(true);
expect(await user.inTrial()).toBe(false);
expect(hasActiveSubscriptionSpy).toHaveBeenCalledOnce();
});
it('should return true when trial expiry date is in future', async () => {
vi.useFakeTimers();
const date = DateTime.fromObject(
{ year: 2024, month: 11, day: 12, hour: 17, minute: 30 },
{ zone: 'UTC+0' }
);
vi.setSystemTime(date);
const user = await createUser();
await user.startTrialPeriod();
const refetchedUser = await user.$query();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
expect(await refetchedUser.inTrial()).toBe(true);
vi.useRealTimers();
});
it('should return false when trial expiry date is in past', async () => {
vi.useFakeTimers();
const user = await createUser();
await user.startTrialPeriod();
vi.setSystemTime(DateTime.now().plus({ days: 31 }));
const refetchedUser = await user.$query();
vi.spyOn(appConfig, 'isSelfHosted', 'get').mockReturnValue(false);
vi.spyOn(refetchedUser, 'hasActiveSubscription').mockResolvedValue(false);
expect(await refetchedUser.inTrial()).toBe(false);
vi.useRealTimers();
});
});
describe('hasActiveSubscription', () => {
it('should return true if current subscription is valid', async () => {
const user = await createUser();
await createSubscription({ userId: user.id, status: 'active' });
expect(await user.hasActiveSubscription()).toBe(true);
});
it('should return false if current subscription is not valid', async () => {
const user = await createUser();
await createSubscription({
userId: user.id,
status: 'deleted',
cancellationEffectiveDate: DateTime.now().minus({ day: 1 }).toString(),
});
expect(await user.hasActiveSubscription()).toBe(false);
});
it('should return false if Automatisch is not a cloud installation', async () => {
const user = new User();
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
expect(await user.hasActiveSubscription()).toBe(false);
});
});
describe('withinLimits', () => {
it('should return true when the consumed task count is less than the quota', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
await createUsageData({
subscriptionId: subscription.id,
userId: user.id,
consumedTaskCount: 100,
});
expect(await user.withinLimits()).toBe(true);
});
it('should return true when the consumed task count is less than the quota', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
await createUsageData({
subscriptionId: subscription.id,
userId: user.id,
consumedTaskCount: 10000,
});
expect(await user.withinLimits()).toBe(false);
});
});
describe('getPlanAndUsage', () => {
it('should return plan and usage', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
expect(await user.getPlanAndUsage()).toStrictEqual({
usage: {
task: 0,
},
plan: {
id: subscription.paddlePlanId,
name: '10k - monthly',
limit: '10,000',
},
});
});
it('should return trial plan and usage if no subscription exists', async () => {
const user = await createUser();
expect(await user.getPlanAndUsage()).toStrictEqual({
usage: {
task: 0,
},
plan: {
id: null,
name: 'Free Trial',
limit: null,
},
});
});
it('should throw not found when the current usage data does not exist', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const user = await createUser();
await expect(() => user.getPlanAndUsage()).rejects.toThrow(
'NotFoundError'
);
});
});
describe('getInvoices', () => {
it('should return invoices for the current subscription', async () => {
const user = await createUser();
const subscription = await createSubscription({ userId: user.id });
const getInvoicesSpy = vi
.spyOn(Billing.paddleClient, 'getInvoices')
.mockResolvedValue('dummy-invoices');
expect(await user.getInvoices()).toBe('dummy-invoices');
expect(getInvoicesSpy).toHaveBeenCalledWith(
Number(subscription.paddleSubscriptionId)
);
});
it('should return empty array without any subscriptions', async () => {
const user = await createUser();
expect(await user.getInvoices()).toEqual([]);
});
});
describe('hasFolderAccess', () => {
let currentUser, currentUserFolder;
beforeEach(async () => {
currentUser = await createUser();
currentUserFolder = await createFolder({ userId: currentUser.id });
});
it('should return true if the user has access to the folder', async () => {
const hasAccess = await currentUser.hasFolderAccess(currentUserFolder.id);
expect(hasAccess).toBe(true);
});
it('should throw an error if the user does not have access to the folder', async () => {
const anotherUser = await createUser();
const anotherUserFolder = await createFolder({ userId: anotherUser.id });
await expect(
currentUser.hasFolderAccess(anotherUserFolder.id)
).rejects.toThrow();
});
it('should throw an error if the folder does not exist', async () => {
const nonExistingFolderUUID = Crypto.randomUUID();
await expect(
currentUser.hasFolderAccess(nonExistingFolderUUID)
).rejects.toThrow();
});
});
describe('getFlows', () => {
let currentUser,
currentUserRole,
anotherUser,
folder,
anotherUserFolder,
flowOne,
flowTwo,
flowThree;
beforeEach(async () => {
currentUser = await createUser();
currentUserRole = await currentUser.$relatedQuery('role');
anotherUser = await createUser();
folder = await createFolder({ userId: currentUser.id });
anotherUserFolder = await createFolder({ userId: anotherUser.id });
flowOne = await createFlow({
userId: currentUser.id,
folderId: folder.id,
active: true,
name: 'Flow One',
});
flowTwo = await createFlow({
userId: currentUser.id,
active: false,
name: 'Flow Two',
});
flowThree = await createFlow({
userId: anotherUser.id,
name: 'Flow Three',
folderId: anotherUserFolder.id,
});
await createPermission({
action: 'read',
subject: 'Flow',
roleId: currentUserRole.id,
conditions: [],
});
currentUser = await currentUser.$query().withGraphFetched({
role: true,
permissions: true,
});
});
it('should return flows filtered by name', async () => {
const flows = await currentUser.getFlows({ name: 'Flow Two' }, [
folder.id,
]);
expect(flows).toHaveLength(1);
expect(flows[0].id).toBe(flowTwo.id);
});
it('should return flows filtered by status', async () => {
const flows = await currentUser.getFlows({ status: 'published' }, [
folder.id,
]);
expect(flows).toHaveLength(1);
expect(flows[0].id).toBe(flowOne.id);
});
it('should return flows filtered by name and status', async () => {
const flows = await currentUser.getFlows(
{ name: 'Flow One', status: 'published' },
[folder.id]
);
expect(flows).toHaveLength(1);
expect(flows[0].id).toBe(flowOne.id);
});
it('should return flows filtered by onlyOwnedFlows', async () => {
const flows = await currentUser.getFlows({ onlyOwnedFlows: true }, [
folder.id,
]);
expect(flows).toHaveLength(2);
expect(flows[0].id).toBe(flowOne.id);
expect(flows[1].id).toBe(flowTwo.id);
});
it('should return flows filtered by onlyOwnedFlows with null folderId', async () => {
const flows = await currentUser.getFlows(
{ onlyOwnedFlows: true, folderId: 'null' },
[folder.id]
);
expect(flows).toHaveLength(1);
expect(flows[0].id).toBe(flowTwo.id);
});
it('should return flows with specific folder ID', async () => {
const flows = await currentUser.getFlows({ folderId: folder.id }, [
folder.id,
]);
expect(flows.length).toBe(1);
expect(flows[0].id).toBe(flowOne.id);
});
it('should return flows filtered by folderId and name', async () => {
const flows = await currentUser.getFlows(
{
folderId: folder.id,
name: 'Flow One',
},
[folder.id]
);
expect(flows).toHaveLength(1);
expect(flows[0].id).toBe(flowOne.id);
});
it('should return all flows if no filters are provided', async () => {
const flows = await currentUser.getFlows({}, [folder.id]);
expect(flows).toHaveLength(3);
expect(flows.map((flow) => flow.id)).toEqual(
expect.arrayContaining([flowOne.id, flowTwo.id, flowThree.id])
);
});
it('should return uncategorized flows if the folderId is null', async () => {
const flows = await currentUser.getFlows({ folderId: 'null' }, [
folder.id,
]);
expect(flows).toHaveLength(2);
expect(flows.map((flow) => flow.id)).toEqual(
expect.arrayContaining([flowTwo.id, flowThree.id])
);
});
it('should return other users flow as uncategorized flows if the folderId is null', async () => {
const flows = await currentUser.getFlows({ folderId: 'null' }, [
folder.id,
]);
expect(flows).toHaveLength(2);
expect(flows.map((flow) => flow.id)).toEqual(
expect.arrayContaining([flowTwo.id, flowThree.id])
);
});
it('should return specified flows with all filters together', async () => {
const flows = await currentUser.getFlows(
{
folderId: folder.id,
name: 'Flow One',
status: 'published',
onlyOwnedFlows: true,
},
[folder.id]
);
expect(flows).toHaveLength(1);
expect(flows[0].id).toBe(flowOne.id);
});
});
it.todo('getApps');
it('createAdmin should create admin with given data and mark the installation completed', async () => {
const adminRole = await createRole({ name: 'Admin' });
const markInstallationCompletedSpy = vi
.spyOn(Config, 'markInstallationCompleted')
.mockResolvedValue();
const adminUser = await User.createAdmin({
fullName: 'Sample admin',
email: 'admin@automatisch.io',
password: 'sample',
});
expect(adminUser).toMatchObject({
fullName: 'Sample admin',
email: 'admin@automatisch.io',
roleId: adminRole.id,
});
expect(markInstallationCompletedSpy).toHaveBeenCalledOnce();
expect(await adminUser.login('sample')).toBe(true);
});
describe('registerUser', () => {
it('should register user with user role and given data', async () => {
const userRole = await createRole({ name: 'User' });
const user = await User.registerUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
});
expect(user).toMatchObject({
fullName: 'Sample user',
email: 'user@automatisch.io',
roleId: userRole.id,
});
expect(await user.login('sample-password')).toBe(true);
});
it('should throw not found error when user role does not exist', async () => {
await expect(() =>
User.registerUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
})
).rejects.toThrowError('NotFoundError');
});
});
describe('can', () => {
it('should return conditions for the given action and subject of the user', async () => {
const userRole = await createRole({ name: 'User' });
await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: ['isCreator'],
});
await createPermission({
roleId: userRole.id,
subject: 'Connection',
action: 'read',
conditions: [],
});
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
expect(userWithRoleAndPermissions.can('read', 'Flow')).toStrictEqual({
isCreator: true,
});
expect(
userWithRoleAndPermissions.can('read', 'Connection')
).toStrictEqual({});
});
it('should return not authorized error when the user is not permitted for the given action and subject', async () => {
const userRole = await createRole({ name: 'User' });
const user = await createUser({ roleId: userRole.id });
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
expect(() => userWithRoleAndPermissions.can('read', 'Flow')).toThrowError(
'The user is not authorized!'
);
});
});
it('lowercaseEmail should lowercase the user email', () => {
const user = new User();
user.email = 'USER@AUTOMATISCH.IO';
user.lowercaseEmail();
expect(user.email).toBe('user@automatisch.io');
});
describe('createUsageData', () => {
it('should create usage data if Automatisch is a cloud installation', async () => {
vi.useFakeTimers();
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
vi.setSystemTime(DateTime.now().plus({ month: 1 }));
const usageData = await user.createUsageData();
const currentUsageData = await user.$relatedQuery('currentUsageData');
expect(usageData).toStrictEqual(currentUsageData);
vi.useRealTimers();
});
it('should not create usage data if Automatisch is not a cloud installation', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
const usageData = await user.createUsageData();
expect(usageData).toBe(undefined);
});
});
describe('omitEnterprisePermissionsWithoutValidLicense', () => {
it('should return user as-is with valid license', async () => {
const userRole = await createRole({ name: 'User' });
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
roleId: userRole.id,
});
const readFlowPermission = await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'App',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'Role',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'Config',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'SamlAuthProvider',
action: 'read',
conditions: [],
});
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
readFlowPermission,
]);
});
it('should omit enterprise permissions without valid license', async () => {
vi.spyOn(licenseModule, 'hasValidLicense').mockResolvedValue(false);
const userRole = await createRole({ name: 'User' });
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
roleId: userRole.id,
});
const readFlowPermission = await createPermission({
roleId: userRole.id,
subject: 'Flow',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'App',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'Role',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'Config',
action: 'read',
conditions: [],
});
await createPermission({
roleId: userRole.id,
subject: 'SamlAuthProvider',
action: 'read',
conditions: [],
});
const userWithRoleAndPermissions = await user
.$query()
.withGraphFetched({ role: true, permissions: true });
expect(userWithRoleAndPermissions.permissions).toStrictEqual([
readFlowPermission,
]);
});
});
describe('createEmptyFlow', () => {
it('should create a flow with default name', async () => {
const user = await createUser();
const flow = await user.createEmptyFlow();
expect(flow.name).toBe('Name your flow');
expect(flow.userId).toBe(user.id);
});
it('should call createInitialSteps on the created flow', async () => {
const user = await createUser();
const createInitialStepsSpy = vi.spyOn(
Flow.prototype,
'createInitialSteps'
);
await user.createEmptyFlow();
expect(createInitialStepsSpy).toHaveBeenCalledOnce();
});
});
describe('createFlowFromTemplate', () => {
let user, template;
beforeEach(async () => {
user = await createUser();
template = await createTemplate();
});
it('should throw an error if template is not found', async () => {
const nonExistentTemplateId = Crypto.randomUUID();
await expect(
user.createFlowFromTemplate(nonExistentTemplateId)
).rejects.toThrow('NotFoundError');
});
it('should call Flow.import with the correct parameters', async () => {
vi.spyOn(Template.query(), 'findById').mockImplementation(() => ({
throwIfNotFound: () => template,
}));
const importSpy = vi.spyOn(Flow, 'import').mockResolvedValue({
id: Crypto.randomUUID(),
name: template.flowData.name,
steps: [],
});
await user.createFlowFromTemplate(template.id);
expect(importSpy).toHaveBeenCalledWith(user, template.flowData);
});
});
describe('$beforeInsert', () => {
it('should call super.$beforeInsert', async () => {
const superBeforeInsertSpy = vi
.spyOn(User.prototype, '$beforeInsert')
.mockResolvedValue();
await createUser();
expect(superBeforeInsertSpy).toHaveBeenCalledOnce();
});
it('should lowercase the user email', async () => {
const user = await createUser({
fullName: 'Sample user',
email: 'USER@AUTOMATISCH.IO',
});
expect(user.email).toBe('user@automatisch.io');
});
it('should generate password hash', async () => {
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
});
expect(user.password).not.toBe('sample-password');
expect(await user.login('sample-password')).toBe(true);
});
it('should start trial period if Automatisch is a cloud installation', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(true);
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
expect(startTrialPeriodSpy).toHaveBeenCalledOnce();
});
it('should not start trial period if Automatisch is not a cloud installation', async () => {
vi.spyOn(appConfig, 'isCloud', 'get').mockReturnValue(false);
const startTrialPeriodSpy = vi.spyOn(User.prototype, 'startTrialPeriod');
await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
expect(startTrialPeriodSpy).not.toHaveBeenCalled();
});
});
describe('$beforeUpdate', () => {
it('should call super.$beforeUpdate', async () => {
const superBeforeUpdateSpy = vi
.spyOn(User.prototype, '$beforeUpdate')
.mockResolvedValue();
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
await user.$query().patch({ fullName: 'Updated user name' });
expect(superBeforeUpdateSpy).toHaveBeenCalledOnce();
});
it('should lowercase the user email if given', async () => {
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
await user.$query().patchAndFetch({ email: 'NEW_EMAIL@AUTOMATISCH.IO' });
expect(user.email).toBe('new_email@automatisch.io');
});
it('should generate password hash', async () => {
const user = await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
password: 'sample-password',
});
await user.$query().patchAndFetch({ password: 'new-password' });
expect(user.password).not.toBe('new-password');
expect(await user.login('new-password')).toBe(true);
});
});
describe('$afterInsert', () => {
it('should call super.$afterInsert', async () => {
const superAfterInsertSpy = vi.spyOn(User.prototype, '$afterInsert');
await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
expect(superAfterInsertSpy).toHaveBeenCalledOnce();
});
it('should call createUsageData', async () => {
const createUsageDataSpy = vi.spyOn(User.prototype, 'createUsageData');
await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
expect(createUsageDataSpy).toHaveBeenCalledOnce();
});
});
it('$afterFind should invoke omitEnterprisePermissionsWithoutValidLicense method', async () => {
const omitEnterprisePermissionsWithoutValidLicenseSpy = vi.spyOn(
User.prototype,
'omitEnterprisePermissionsWithoutValidLicense'
);
await createUser({
fullName: 'Sample user',
email: 'user@automatisch.io',
});
expect(
omitEnterprisePermissionsWithoutValidLicenseSpy
).toHaveBeenCalledOnce();
});
});