fix: account deletion event (#904)

* fix: account deletion event

* fix: feature service binding
This commit is contained in:
Karol Sójko 2023-11-07 11:34:10 +01:00 committed by GitHub
parent b01d1c659d
commit d66ae62cf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 261 additions and 157 deletions

View file

@ -791,7 +791,16 @@ export class ContainerConfigLoader {
container
.bind<SubscriptionSettingsAssociationServiceInterface>(TYPES.Auth_SubscriptionSettingsAssociationService)
.to(SubscriptionSettingsAssociationService)
container.bind<FeatureServiceInterface>(TYPES.Auth_FeatureService).to(FeatureService)
container
.bind<FeatureServiceInterface>(TYPES.Auth_FeatureService)
.toConstantValue(
new FeatureService(
container.get<RoleToSubscriptionMapInterface>(TYPES.Auth_RoleToSubscriptionMap),
container.get<OfflineUserSubscriptionRepositoryInterface>(TYPES.Auth_OfflineUserSubscriptionRepository),
container.get<TimerInterface>(TYPES.Auth_Timer),
container.get<UserSubscriptionRepositoryInterface>(TYPES.Auth_UserSubscriptionRepository),
),
)
container
.bind<SelectorInterface<boolean>>(TYPES.Auth_BooleanSelector)
.toConstantValue(new DeterministicSelector<boolean>())
@ -1104,6 +1113,7 @@ export class ContainerConfigLoader {
new DeleteAccount(
container.get<UserRepositoryInterface>(TYPES.Auth_UserRepository),
container.get<GetRegularSubscriptionForUser>(TYPES.Auth_GetRegularSubscriptionForUser),
container.get<GetSharedSubscriptionForUser>(TYPES.Auth_GetSharedSubscriptionForUser),
container.get<DomainEventPublisherInterface>(TYPES.Auth_DomainEventPublisher),
container.get<DomainEventFactoryInterface>(TYPES.Auth_DomainEventFactory),
container.get<TimerInterface>(TYPES.Auth_Timer),

View file

@ -281,9 +281,16 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
createAccountDeletionRequestedEvent(dto: {
userUuid: string
email: string
userCreatedAtTimestamp: number
regularSubscriptionUuid: string | undefined
roleNames: string[]
regularSubscription?: {
uuid: string
ownerUuid: string
}
sharedSubscription?: {
uuid: string
ownerUuid: string
}
}): AccountDeletionRequestedEvent {
return {
type: 'ACCOUNT_DELETION_REQUESTED',

View file

@ -45,9 +45,16 @@ export interface DomainEventFactoryInterface {
): EmailBackupRequestedEvent
createAccountDeletionRequestedEvent(dto: {
userUuid: string
email: string
userCreatedAtTimestamp: number
regularSubscriptionUuid: string | undefined
roleNames: string[]
regularSubscription?: {
uuid: string
ownerUuid: string
}
sharedSubscription?: {
uuid: string
ownerUuid: string
}
}): AccountDeletionRequestedEvent
createUserRolesChangedEvent(userUuid: string, email: string, currentRoles: string[]): UserRolesChangedEvent
createUserEmailChangedEvent(userUuid: string, fromEmail: string, toEmail: string): UserEmailChangedEvent

View file

@ -35,6 +35,7 @@ import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/Offl
import { TimerInterface } from '@standardnotes/time'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { UserSubscriptionType } from '../Subscription/UserSubscriptionType'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
describe('FeatureService', () => {
let roleToSubscriptionMap: RoleToSubscriptionMapInterface
@ -52,8 +53,10 @@ describe('FeatureService', () => {
let offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface
let timer: TimerInterface
let offlineUserSubscription: OfflineUserSubscription
let userSubscriptionRepository: UserSubscriptionRepositoryInterface
const createService = () => new FeatureService(roleToSubscriptionMap, offlineUserSubscriptionRepository, timer)
const createService = () =>
new FeatureService(roleToSubscriptionMap, offlineUserSubscriptionRepository, timer, userSubscriptionRepository)
beforeEach(() => {
roleToSubscriptionMap = {} as jest.Mocked<RoleToSubscriptionMapInterface>
@ -107,7 +110,7 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 555,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: false,
subscriptionId: 1,
subscriptionType: UserSubscriptionType.Regular,
@ -120,7 +123,7 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.ProPlan,
endsAt: 777,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: false,
subscriptionId: 2,
subscriptionType: UserSubscriptionType.Regular,
@ -133,7 +136,7 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: true,
subscriptionId: 3,
subscriptionType: UserSubscriptionType.Regular,
@ -146,7 +149,7 @@ describe('FeatureService', () => {
renewedAt: null,
planName: SubscriptionName.PlusPlan,
endsAt: 333,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: true,
subscriptionId: 4,
subscriptionType: UserSubscriptionType.Regular,
@ -155,9 +158,11 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription1]),
} as jest.Mocked<User>
userSubscriptionRepository = {} as jest.Mocked<UserSubscriptionRepositoryInterface>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1])
offlineUserSubscription = {
roles: Promise.resolve([role1]),
uuid: 'subscription-1-1-1',
@ -247,9 +252,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2, nonSubscriptionRole]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
expect(await createService().userIsEntitledToFeature(user, 'files-beta')).toBe(true)
})
@ -269,9 +275,12 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription3, subscription1, subscription4]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest
.fn()
.mockReturnValue([subscription3, subscription1, subscription4])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([
@ -284,14 +293,13 @@ describe('FeatureService', () => {
})
it('should not return user features if a subscription could not be found', async () => {
const subscriptions: Array<UserSubscription> = []
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve(subscriptions),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([])
expect(await createService().getFeaturesForUser(user)).toEqual([])
})
@ -307,9 +315,12 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription3, subscription1, subscription4]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest
.fn()
.mockReturnValue([subscription3, subscription1, subscription4])
expect(await createService().getFeaturesForUser(user)).toEqual([])
})
@ -321,7 +332,7 @@ describe('FeatureService', () => {
renewedAt: null,
planName: 'non existing plan name' as SubscriptionName,
endsAt: 555,
user: Promise.resolve(user),
userUuid: 'user-1-1-1',
cancelled: false,
subscriptionId: 1,
subscriptionType: UserSubscriptionType.Regular,
@ -330,9 +341,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1]),
subscriptions: Promise.resolve([subscription1]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1])
expect(await createService().getFeaturesForUser(user)).toEqual([])
})
@ -351,9 +363,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([
@ -409,9 +422,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2, nonSubscriptionRole]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([
@ -445,9 +459,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const longestExpireAt = 777
const features = await createService().getFeaturesForUser(user)
@ -482,9 +497,10 @@ describe('FeatureService', () => {
user = {
uuid: 'user-1-1-1',
roles: Promise.resolve([role1, role2]),
subscriptions: Promise.resolve([subscription1, subscription2]),
} as jest.Mocked<User>
userSubscriptionRepository.findByUserUuid = jest.fn().mockReturnValue([subscription1, subscription2])
const features = await createService().getFeaturesForUser(user)
expect(features).toEqual(
expect.arrayContaining([

View file

@ -1,24 +1,22 @@
import { SubscriptionName } from '@standardnotes/common'
import { FeatureDescription, GetFeatures } from '@standardnotes/features'
import { inject, injectable } from 'inversify'
import TYPES from '../../Bootstrap/Types'
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
import { TimerInterface } from '@standardnotes/time'
import { RoleToSubscriptionMapInterface } from '../Role/RoleToSubscriptionMapInterface'
import { User } from '../User/User'
import { UserSubscription } from '../Subscription/UserSubscription'
import { FeatureServiceInterface } from './FeatureServiceInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Role } from '../Role/Role'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { TimerInterface } from '@standardnotes/time'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
@injectable()
export class FeatureService implements FeatureServiceInterface {
constructor(
@inject(TYPES.Auth_RoleToSubscriptionMap) private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
@inject(TYPES.Auth_OfflineUserSubscriptionRepository)
private roleToSubscriptionMap: RoleToSubscriptionMapInterface,
private offlineUserSubscriptionRepository: OfflineUserSubscriptionRepositoryInterface,
@inject(TYPES.Auth_Timer) private timer: TimerInterface,
private timer: TimerInterface,
private userSubscriptionRepository: UserSubscriptionRepositoryInterface,
) {}
async userIsEntitledToFeature(user: User, featureIdentifier: string): Promise<boolean> {
@ -61,7 +59,7 @@ export class FeatureService implements FeatureServiceInterface {
}
async getFeaturesForUser(user: User): Promise<Array<FeatureDescription>> {
const userSubscriptions = await user.subscriptions
const userSubscriptions = await this.userSubscriptionRepository.findByUserUuid(user.uuid)
return this.getFeaturesForSubscriptions(userSubscriptions, await user.roles)
}

View file

@ -7,7 +7,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterface {
@ -48,7 +48,22 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
const userUuidOrError = Uuid.create(userSubscription.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not remove role from user with uuid: ${userUuidOrError.getError()}`)
continue
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
continue
}
await this.roleService.removeUserRoleBasedOnSubscription(user, subscriptionName)
}
}

View file

@ -84,7 +84,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
): Promise<UserSubscription> {
const subscription = new UserSubscription()
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = subscriptionExpiresAt

View file

@ -80,7 +80,7 @@ export class SubscriptionReassignedEventHandler implements DomainEventHandlerInt
): Promise<UserSubscription> {
const subscription = new UserSubscription()
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = subscriptionExpiresAt

View file

@ -7,7 +7,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { UserSubscriptionRepositoryInterface } from '../Subscription/UserSubscriptionRepositoryInterface'
import { OfflineUserSubscriptionRepositoryInterface } from '../Subscription/OfflineUserSubscriptionRepositoryInterface'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class SubscriptionRefundedEventHandler implements DomainEventHandlerInterface {
@ -48,7 +48,22 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
private async removeRoleFromSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
await this.roleService.removeUserRoleBasedOnSubscription(await userSubscription.user, subscriptionName)
const userUuidOrError = Uuid.create(userSubscription.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not remove role from user with uuid: ${userUuidOrError.getError()}`)
continue
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
continue
}
await this.roleService.removeUserRoleBasedOnSubscription(user, subscriptionName)
}
}

View file

@ -8,7 +8,7 @@ import { RoleServiceInterface } from '../Role/RoleServiceInterface'
import { UserRepositoryInterface } from '../User/UserRepositoryInterface'
import { Logger } from 'winston'
import { OfflineUserSubscription } from '../Subscription/OfflineUserSubscription'
import { Username } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
@injectable()
export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterface {
@ -71,7 +71,20 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
private async addRoleToSubscriptionUsers(subscriptionId: number, subscriptionName: string): Promise<void> {
const userSubscriptions = await this.userSubscriptionRepository.findBySubscriptionId(subscriptionId)
for (const userSubscription of userSubscriptions) {
const user = await userSubscription.user
const userUuidOrError = Uuid.create(userSubscription.userUuid)
if (userUuidOrError.isFailed()) {
this.logger.warn(`Could not add role to user with uuid: ${userUuidOrError.getError()}`)
continue
}
const userUuid = userUuidOrError.getValue()
const user = await this.userRepository.findOneByUuid(userUuid)
if (user === null) {
this.logger.warn(`Could not find user with uuid: ${userUuid.value}`)
continue
}
await this.roleService.addUserRoleBasedOnSubscription(user, subscriptionName)
}

View file

@ -128,7 +128,7 @@ export class SubscriptionSyncRequestedEventHandler implements DomainEventHandler
}
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = subscriptionExpiresAt

View file

@ -1,5 +1,4 @@
import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'
import { User } from '../User/User'
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'
import { UserSubscriptionType } from './UserSubscriptionType'
@Entity({ name: 'user_subscriptions' })
@ -63,14 +62,9 @@ export class UserSubscription {
})
declare subscriptionType: UserSubscriptionType
@ManyToOne(
/* istanbul ignore next */
() => User,
/* istanbul ignore next */
(user) => user.subscriptions,
/* istanbul ignore next */
{ onDelete: 'CASCADE', nullable: false, lazy: true, eager: false },
)
@JoinColumn({ name: 'user_uuid', referencedColumnName: 'uuid' })
declare user: Promise<User>
@Column({
name: 'user_uuid',
length: 36,
})
declare userUuid: string
}

View file

@ -107,7 +107,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
subscriptionId: 3,
subscriptionType: 'shared',
updatedAt: 1,
user: Promise.resolve(invitee),
userUuid: '123',
})
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled()
@ -145,7 +145,7 @@ describe('AcceptSharedSubscriptionInvitation', () => {
subscriptionId: 3,
subscriptionType: 'shared',
updatedAt: 3,
user: Promise.resolve(invitee),
userUuid: '123',
})
expect(roleService.addUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(applyDefaultSubscriptionSettings.execute).toHaveBeenCalled()

View file

@ -115,7 +115,7 @@ export class AcceptSharedSubscriptionInvitation implements UseCaseInterface {
): Promise<UserSubscription> {
const subscription = new UserSubscription()
subscription.planName = subscriptionName
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
const timestamp = this.timer.getTimestampInMicroseconds()
subscription.createdAt = timestamp
subscription.updatedAt = timestamp

View file

@ -53,7 +53,7 @@ export class ActivatePremiumFeatures implements UseCaseInterface<string> {
const subscription = new UserSubscription()
subscription.planName = subscriptionPlanName.value
subscription.user = Promise.resolve(user)
subscription.userUuid = user.uuid
subscription.createdAt = timestamp
subscription.updatedAt = timestamp
subscription.endsAt = this.timer.convertDateToMicroseconds(endsAt)

View file

@ -80,7 +80,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
userSubscriptionRepository.findBySubscriptionIdAndType = jest.fn().mockReturnValue([inviterSubscription])
userSubscriptionRepository.findOneByUserUuidAndSubscriptionId = jest
.fn()
.mockReturnValue({ user: Promise.resolve(invitee) } as jest.Mocked<UserSubscription>)
.mockReturnValue({ userUuid: '123' } as jest.Mocked<UserSubscription>)
userSubscriptionRepository.save = jest.fn()
roleService = {} as jest.Mocked<RoleServiceInterface>
@ -120,7 +120,7 @@ describe('CancelSharedSubscriptionInvitation', () => {
})
expect(userSubscriptionRepository.save).toHaveBeenCalledWith({
endsAt: 1,
user: Promise.resolve(invitee),
userUuid: '123',
})
expect(roleService.removeUserRoleBasedOnSubscription).toHaveBeenCalledWith(invitee, 'PLUS_PLAN')
expect(domainEventPublisher.publish).toHaveBeenCalled()

View file

@ -5,7 +5,6 @@ import { TokenEncoderInterface, ValetTokenData, ValetTokenOperation } from '@sta
import { CreateValetToken } from './CreateValetToken'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { User } from '../../User/User'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
import { SubscriptionSettingsAssociationServiceInterface } from '../../Setting/SubscriptionSettingsAssociationServiceInterface'
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
@ -25,7 +24,6 @@ describe('CreateValetToken', () => {
const valetTokenTTL = 123
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
let user: User
const createUseCase = () =>
new CreateValetToken(
@ -59,20 +57,16 @@ describe('CreateValetToken', () => {
subscriptionSettingsAssociationService = {} as jest.Mocked<SubscriptionSettingsAssociationServiceInterface>
subscriptionSettingsAssociationService.getFileUploadLimit = jest.fn().mockReturnValue(5_368_709_120)
user = {
uuid: '123',
} as jest.Mocked<User>
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
userUuid: '123',
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(user),
userUuid: '123',
} as jest.Mocked<UserSubscription>
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>

View file

@ -11,29 +11,46 @@ import { TimerInterface } from '@standardnotes/time'
import { Result, RoleName } from '@standardnotes/domain-core'
import { Role } from '../../Role/Role'
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
describe('DeleteAccount', () => {
let userRepository: UserRepositoryInterface
let domainEventPublisher: DomainEventPublisherInterface
let domainEventFactory: DomainEventFactoryInterface
let getRegularSubscription: GetRegularSubscriptionForUser
let getSharedSubscription: GetSharedSubscriptionForUser
let user: User
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
let timer: TimerInterface
const createUseCase = () =>
new DeleteAccount(userRepository, getRegularSubscription, domainEventPublisher, domainEventFactory, timer)
new DeleteAccount(
userRepository,
getRegularSubscription,
getSharedSubscription,
domainEventPublisher,
domainEventFactory,
timer,
)
beforeEach(() => {
user = {
uuid: '1-2-3',
email: 'test@test.te',
} as jest.Mocked<User>
user.roles = Promise.resolve([{ name: RoleName.NAMES.CoreUser } as jest.Mocked<Role>])
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
userUuid: 'u-1-2-3',
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Shared,
userUuid: 'u-1-2-3',
} as jest.Mocked<UserSubscription>
userRepository = {} as jest.Mocked<UserRepositoryInterface>
@ -43,6 +60,9 @@ describe('DeleteAccount', () => {
getRegularSubscription = {} as jest.Mocked<GetRegularSubscriptionForUser>
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.ok(regularSubscription))
getSharedSubscription = {} as jest.Mocked<GetSharedSubscriptionForUser>
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.ok(sharedSubscription))
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
@ -58,6 +78,7 @@ describe('DeleteAccount', () => {
describe('when user uuid is provided', () => {
it('should trigger account deletion - no subscription', async () => {
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
@ -66,12 +87,11 @@ describe('DeleteAccount', () => {
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: undefined,
roleNames: ['CORE_USER'],
email: 'test@test.te',
})
})
it('should trigger account deletion - subscription present', async () => {
it('should trigger account deletion - shared subscription present', async () => {
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(result.isFailed()).toBeFalsy()
@ -79,9 +99,35 @@ describe('DeleteAccount', () => {
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
email: 'test@test.te',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '1-2-3',
roleNames: ['CORE_USER'],
regularSubscription: {
ownerUuid: 'u-1-2-3',
uuid: '1-2-3',
},
sharedSubscription: {
ownerUuid: 'u-1-2-3',
uuid: '1-2-3',
},
})
})
it('should trigger account deletion - regular subscription present', async () => {
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
const result = await createUseCase().execute({ userUuid: '00000000-0000-0000-0000-000000000000' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
email: 'test@test.te',
userCreatedAtTimestamp: 1,
regularSubscription: {
ownerUuid: 'u-1-2-3',
uuid: '1-2-3',
},
})
})
@ -109,6 +155,7 @@ describe('DeleteAccount', () => {
describe('when username is provided', () => {
it('should trigger account deletion - no subscription', async () => {
getRegularSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
const result = await createUseCase().execute({ username: 'test@test.te' })
@ -116,13 +163,12 @@ describe('DeleteAccount', () => {
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
email: 'test@test.te',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: undefined,
roleNames: ['CORE_USER'],
})
})
it('should trigger account deletion - subscription present', async () => {
it('should trigger account deletion - shared subscription present', async () => {
const result = await createUseCase().execute({ username: 'test@test.te' })
expect(result.isFailed()).toBeFalsy()
@ -130,9 +176,35 @@ describe('DeleteAccount', () => {
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
email: 'test@test.te',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '1-2-3',
roleNames: ['CORE_USER'],
regularSubscription: {
ownerUuid: 'u-1-2-3',
uuid: '1-2-3',
},
sharedSubscription: {
ownerUuid: 'u-1-2-3',
uuid: '1-2-3',
},
})
})
it('should trigger account deletion - regular subscription present', async () => {
getSharedSubscription.execute = jest.fn().mockReturnValue(Result.fail('not found'))
const result = await createUseCase().execute({ username: 'test@test.te' })
expect(result.isFailed()).toBeFalsy()
expect(domainEventPublisher.publish).toHaveBeenCalledTimes(1)
expect(domainEventFactory.createAccountDeletionRequestedEvent).toHaveBeenLastCalledWith({
userUuid: '1-2-3',
email: 'test@test.te',
userCreatedAtTimestamp: 1,
regularSubscription: {
ownerUuid: 'u-1-2-3',
uuid: '1-2-3',
},
})
})

View file

@ -8,11 +8,14 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { DeleteAccountDTO } from './DeleteAccountDTO'
import { User } from '../../User/User'
import { GetRegularSubscriptionForUser } from '../GetRegularSubscriptionForUser/GetRegularSubscriptionForUser'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { GetSharedSubscriptionForUser } from '../GetSharedSubscriptionForUser/GetSharedSubscriptionForUser'
export class DeleteAccount implements UseCaseInterface<string> {
constructor(
private userRepository: UserRepositoryInterface,
private getRegularSubscription: GetRegularSubscriptionForUser,
private getSharedSubscription: GetSharedSubscriptionForUser,
private domainEventPublisher: DomainEventPublisherInterface,
private domainEventFactory: DomainEventFactoryInterface,
private timer: TimerInterface,
@ -44,23 +47,39 @@ export class DeleteAccount implements UseCaseInterface<string> {
return Result.ok('User already deleted.')
}
const roles = await user.roles
let regularSubscriptionUuid: string | undefined
const result = await this.getRegularSubscription.execute({
let sharedSubscription: UserSubscription | undefined
const sharedSubscriptionOrError = await this.getSharedSubscription.execute({
userUuid: user.uuid,
})
if (!result.isFailed()) {
const regularSubscription = result.getValue()
regularSubscriptionUuid = regularSubscription.uuid
if (!sharedSubscriptionOrError.isFailed()) {
sharedSubscription = sharedSubscriptionOrError.getValue()
}
let regularSubscription: UserSubscription | undefined
const regularSubscriptionOrError = await this.getRegularSubscription.execute({
userUuid: user.uuid,
})
if (!regularSubscriptionOrError.isFailed()) {
regularSubscription = regularSubscriptionOrError.getValue()
}
await this.domainEventPublisher.publish(
this.domainEventFactory.createAccountDeletionRequestedEvent({
userUuid: user.uuid,
email: user.email,
userCreatedAtTimestamp: this.timer.convertDateToMicroseconds(user.createdAt),
regularSubscriptionUuid,
roleNames: roles.map((role) => role.name),
regularSubscription: regularSubscription
? {
ownerUuid: regularSubscription.userUuid,
uuid: regularSubscription.uuid,
}
: undefined,
sharedSubscription: sharedSubscription
? {
ownerUuid: sharedSubscription.userUuid,
uuid: sharedSubscription.uuid,
}
: undefined,
}),
)

View file

@ -45,13 +45,13 @@ describe('UpdateStorageQuotaUsedForUser', () => {
regularSubscription = {
uuid: '00000000-0000-0000-0000-000000000000',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
userUuid: '123',
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(user),
userUuid: '123',
} as jest.Mocked<UserSubscription>
getSharedSubscription = {} as jest.Mocked<GetSharedSubscriptionForUser>

View file

@ -56,7 +56,6 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
private async updateUploadBytesUsedSetting(subscription: UserSubscription, bytesUsed: number): Promise<void> {
let bytesAlreadyUsed = '0'
const subscriptionUser = await subscription.user
const bytesUsedSettingExists = await this.getSubscriptionSetting.execute({
userSubscriptionUuid: subscription.uuid,
@ -77,7 +76,7 @@ export class UpdateStorageQuotaUsedForUser implements UseCaseInterface<void> {
/* istanbul ignore next */
if (result.isFailed()) {
this.logger.error(`Could not set file upload bytes used for user ${subscriptionUser.uuid}`)
this.logger.error(`Could not set file upload bytes used for subscription ${subscription.uuid}`)
}
}
}

View file

@ -1,7 +1,6 @@
import { Column, Entity, Index, JoinTable, ManyToMany, OneToMany, PrimaryGeneratedColumn } from 'typeorm'
import { RevokedSession } from '../Session/RevokedSession'
import { Role } from '../Role/Role'
import { UserSubscription } from '../Subscription/UserSubscription'
import { ProtocolVersion } from '@standardnotes/common'
import { TypeORMEmergencyAccessInvitation } from '../../Infra/TypeORM/TypeORMEmergencyAccessInvitation'
@ -161,16 +160,6 @@ export class User {
})
declare roles: Promise<Array<Role>>
@OneToMany(
/* istanbul ignore next */
() => UserSubscription,
/* istanbul ignore next */
(subscription) => subscription.user,
/* istanbul ignore next */
{ lazy: true, eager: false },
)
declare subscriptions: Promise<UserSubscription[]>
@OneToMany(
/* istanbul ignore next */
() => TypeORMEmergencyAccessInvitation,

View file

@ -1,5 +1,13 @@
export interface AccountDeletionRequestedEventPayload {
userUuid: string
email: string
userCreatedAtTimestamp: number
regularSubscriptionUuid: string | undefined
regularSubscription?: {
uuid: string
ownerUuid: string
}
sharedSubscription?: {
uuid: string
ownerUuid: string
}
}

View file

@ -1,6 +1,5 @@
export interface FileRemovedEventPayload {
userUuid: string
regularSubscriptionUuid: string
fileByteSize: number
filePath: string
fileName: string

View file

@ -18,7 +18,6 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
filePath: string
fileName: string
fileByteSize: number
regularSubscriptionUuid: string
}): FileRemovedEvent {
return {
type: 'FILE_REMOVED',

View file

@ -18,7 +18,6 @@ export interface DomainEventFactoryInterface {
filePath: string
fileName: string
fileByteSize: number
regularSubscriptionUuid: string
}): FileRemovedEvent
createSharedVaultFileMovedEvent(payload: {
fileByteSize: number

View file

@ -17,7 +17,7 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
) {}
async handle(event: AccountDeletionRequestedEvent): Promise<void> {
if (event.payload.regularSubscriptionUuid === undefined) {
if (event.payload.regularSubscription === undefined) {
return
}
@ -38,7 +38,6 @@ export class AccountDeletionRequestedEventHandler implements DomainEventHandlerI
for (const fileRemoved of filesRemoved) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileRemovedEvent({
regularSubscriptionUuid: event.payload.regularSubscriptionUuid,
userUuid: fileRemoved.userOrSharedVaultUuid,
filePath: fileRemoved.filePath,
fileName: fileRemoved.fileName,

View file

@ -40,7 +40,6 @@ export class SharedSubscriptionInvitationCanceledEventHandler implements DomainE
for (const fileRemoved of filesRemoved) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createFileRemovedEvent({
regularSubscriptionUuid: event.payload.inviterSubscriptionUuid,
userUuid: fileRemoved.userOrSharedVaultUuid,
filePath: fileRemoved.filePath,
fileName: fileRemoved.fileName,

View file

@ -36,7 +36,6 @@ export class RemoveFile implements UseCaseInterface<boolean> {
filePath: `${dto.userInput.userUuid}/${dto.userInput.resourceRemoteIdentifier}`,
fileName: dto.userInput.resourceRemoteIdentifier,
fileByteSize: removedFileSize,
regularSubscriptionUuid: dto.userInput.regularSubscriptionUuid,
}),
)
} else if (dto.vaultInput !== undefined) {

View file

@ -1,46 +0,0 @@
import 'reflect-metadata'
import { AccountDeletionRequestedEvent } from '@standardnotes/domain-events'
import { Logger } from 'winston'
import { AccountDeletionRequestedEventHandler } from './AccountDeletionRequestedEventHandler'
import { RevisionRepositoryInterface } from '../Revision/RevisionRepositoryInterface'
describe('AccountDeletionRequestedEventHandler', () => {
let revisionRepository: RevisionRepositoryInterface
let logger: Logger
let event: AccountDeletionRequestedEvent
const createHandler = () => new AccountDeletionRequestedEventHandler(revisionRepository, logger)
beforeEach(() => {
revisionRepository = {} as jest.Mocked<RevisionRepositoryInterface>
revisionRepository.removeByUserUuid = jest.fn()
logger = {} as jest.Mocked<Logger>
logger.info = jest.fn()
logger.warn = jest.fn()
logger.error = jest.fn()
event = {} as jest.Mocked<AccountDeletionRequestedEvent>
event.createdAt = new Date(1)
event.payload = {
userUuid: '2-3-4',
userCreatedAtTimestamp: 1,
regularSubscriptionUuid: '1-2-3',
}
})
it('should remove all revisions for a user', async () => {
event.payload.userUuid = '84c0f8e8-544a-4c7e-9adf-26209303bc1d'
await createHandler().handle(event)
expect(revisionRepository.removeByUserUuid).toHaveBeenCalled()
})
it('should not remove all revisions for an invalid user uuid', async () => {
await createHandler().handle(event)
expect(revisionRepository.removeByUserUuid).not.toHaveBeenCalled()
})
})