832 lines
24 KiB
JavaScript
832 lines
24 KiB
JavaScript
import { describe, it, expect, vi } from 'vitest';
|
|
import { DateTime, Duration } from 'luxon';
|
|
import appConfig from '../config/app.js';
|
|
import Base from './base.js';
|
|
import AccessToken from './access-token.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 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 { 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';
|
|
|
|
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',
|
|
},
|
|
},
|
|
};
|
|
|
|
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);
|
|
});
|
|
|
|
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'
|
|
);
|
|
});
|
|
|
|
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('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
|
|
).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!'
|
|
);
|
|
});
|
|
});
|
|
|
|
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 = new Date(2024, 10, 14, 14, 30, 0, 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 = new Date(2024, 10, 14, 14, 30, 0, 0);
|
|
vi.setSystemTime(date);
|
|
|
|
const user = new User();
|
|
user.invitationTokenSentAt = '2024-11-11T14:20:00.000Z';
|
|
|
|
expect(user.isInvitationTokenValid()).toBe(false);
|
|
|
|
vi.useRealTimers();
|
|
});
|
|
});
|
|
});
|