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 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 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'; import { createSubscription } from '../../test/factories/subscription.js'; import { createUsageData } from '../../test/factories/usage-data.js'; import Billing from '../helpers/billing/index.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', }, }, }; 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 = 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(); const presentDate = DateTime.fromObject( { year: 2024, month: 11, day: 17, hour: 11, minute: 30 }, { zone: 'UTC+0' } ); vi.setSystemTime(presentDate); await user.startTrialPeriod(); const futureDate = DateTime.fromObject( { year: 2025, month: 1, day: 1 }, { zone: 'UTC+0' } ); vi.setSystemTime(futureDate); 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(); 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()).toStrictEqual([]); }); }); 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); }); });