feat: sign in setting refactor (#472)

* fix(auth): refactor setting names into domain core value objects

* fix(auth): refactor specs with setting name value objects

* feat(auth): move mute sign in emails to a subscription kind of setting

* feat(auth): add migration script to change sign in email settings to subscription settings

* chore: fix setting name usage

* fix(auth): upper casing setting names

---------

Co-authored-by: Karol Sójko <[email protected]>
This commit is contained in:
Karol Sójko
2023-03-08 10:22:27 +01:00
committed by GitHub
co-authored by Karol Sójko
parent ec0fb7e0b9
commit 27cf093f85
67 changed files with 1450 additions and 809 deletions
@@ -1,75 +1,239 @@
import { SettingName } from '@standardnotes/settings'
import 'reflect-metadata'
import { SettingName } from '@standardnotes/settings'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { Setting } from '../../Setting/Setting'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { GetSetting } from './GetSetting'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
describe('GetSetting', () => {
let settingProjector: SettingProjector
let setting: Setting
let subscriptionSetting: SubscriptionSetting
let settingService: SettingServiceInterface
let userSubscriptionService: UserSubscriptionServiceInterface
let subscriptionSettingProjector: SubscriptionSettingProjector
let subscriptionSettingService: SubscriptionSettingServiceInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createUseCase = () => new GetSetting(settingProjector, settingService)
const createUseCase = () =>
new GetSetting(
settingProjector,
subscriptionSettingProjector,
settingService,
subscriptionSettingService,
userSubscriptionService,
)
beforeEach(() => {
setting = {} as jest.Mocked<Setting>
subscriptionSetting = {
sensitive: false,
} as jest.Mocked<SubscriptionSetting>
settingService = {} as jest.Mocked<SettingServiceInterface>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
settingProjector = {} as jest.Mocked<SettingProjector>
settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
subscriptionSettingService = {} as jest.Mocked<SubscriptionSettingServiceInterface>
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
})
it('should find a setting for user', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test' })).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
describe('no subscription', () => {
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
it('should not find a setting if the setting name is invalid', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'invalid' })).toEqual({
success: false,
error: {
message: 'Invalid setting name: invalid',
},
})
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.DropboxBackupFrequency }),
).toEqual({
success: false,
error: {
message: 'Setting DROPBOX_BACKUP_FREQUENCY for user 1-2-3 not found!',
},
})
})
it('should not retrieve a sensitive setting for user', async () => {
setting = {
sensitive: true,
name: SettingName.NAMES.MfaSecret,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MfaSecret })).toEqual({
success: true,
sensitive: true,
})
})
it('should not retrieve a subscription setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'No subscription found.',
},
})
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
setting = {
sensitive: true,
name: SettingName.NAMES.MfaSecret,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(
await createUseCase().execute({
userUuid: '1-2-3',
settingName: SettingName.NAMES.MfaSecret,
allowSensitiveRetrieval: true,
}),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
})
})
})
it('should not get a setting for user if it does not exist', async () => {
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
describe('regular subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
})
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test' })).toEqual({
success: false,
error: {
message: 'Setting test for user 1-2-3 not found!',
},
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
})
it('should not get a suscription setting for user if it does not exist', async () => {
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest.fn().mockReturnValue(null)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: false,
error: {
message: 'Subscription setting MUTE_SIGN_IN_EMAILS for user 1-2-3 not found!',
},
})
})
it('should not retrieve a sensitive subscription setting for user', async () => {
subscriptionSetting.sensitive = true
subscriptionSettingService.findSubscriptionSettingWithDecryptedValue = jest
.fn()
.mockReturnValue(subscriptionSetting)
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
sensitive: true,
})
})
})
it('should not retrieve a sensitive setting for user', async () => {
setting = {
sensitive: true,
name: SettingName.MfaSecret,
} as jest.Mocked<Setting>
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.MfaSecret })).toEqual({
success: true,
sensitive: true,
describe('shared subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
})
})
it('should retrieve a sensitive setting for user if explicitly told to', async () => {
setting = {
sensitive: true,
name: SettingName.MfaSecret,
} as jest.Mocked<Setting>
it('should find a setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.MuteSignInEmails }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
settingService.findSettingWithDecryptedValue = jest.fn().mockReturnValue(setting)
expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({
subscriptionSettingName: SettingName.create(SettingName.NAMES.MuteSignInEmails).getValue(),
userSubscriptionUuid: '2-3-4',
userUuid: '1-2-3',
})
})
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'MFA_SECRET', allowSensitiveRetrieval: true }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'bar' },
it('should find a regular subscription only setting for user', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: SettingName.NAMES.FileUploadBytesLimit }),
).toEqual({
success: true,
userUuid: '1-2-3',
setting: { foo: 'sub-bar' },
})
expect(subscriptionSettingService.findSubscriptionSettingWithDecryptedValue).toHaveBeenCalledWith({
subscriptionSettingName: SettingName.create(SettingName.NAMES.FileUploadBytesLimit).getValue(),
userSubscriptionUuid: '1-2-3',
userUuid: '1-2-3',
})
})
})
})
@@ -1,32 +1,100 @@
import { SettingName } from '@standardnotes/settings'
import { inject, injectable } from 'inversify'
import { GetSettingDto } from './GetSettingDto'
import { GetSettingResponse } from './GetSettingResponse'
import { UseCaseInterface } from '../UseCaseInterface'
import TYPES from '../../../Bootstrap/Types'
import { SettingProjector } from '../../../Projection/SettingProjector'
import { SettingServiceInterface } from '../../Setting/SettingServiceInterface'
import { SubscriptionSettingServiceInterface } from '../../Setting/SubscriptionSettingServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { GetSettingDto } from './GetSettingDto'
import { GetSettingResponse } from './GetSettingResponse'
import { UserSubscription } from '../../Subscription/UserSubscription'
@injectable()
export class GetSetting implements UseCaseInterface {
constructor(
@inject(TYPES.SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@inject(TYPES.SettingService) private settingService: SettingServiceInterface,
@inject(TYPES.SubscriptionSettingService) private subscriptionSettingService: SubscriptionSettingServiceInterface,
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
) {}
async execute(dto: GetSettingDto): Promise<GetSettingResponse> {
const { userUuid, settingName } = dto
const settingNameOrError = SettingName.create(dto.settingName)
if (settingNameOrError.isFailed()) {
return {
success: false,
error: {
message: settingNameOrError.getError(),
},
}
}
const settingName = settingNameOrError.getValue()
if (settingName.isASubscriptionSetting()) {
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(dto.userUuid)
let subscription: UserSubscription | null
if (settingName.isARegularOnlySubscriptionSetting()) {
subscription = regularSubscription
} else {
subscription = sharedSubscription ?? regularSubscription
}
if (!subscription) {
return {
success: false,
error: {
message: 'No subscription found.',
},
}
}
const subscriptionSetting = await this.subscriptionSettingService.findSubscriptionSettingWithDecryptedValue({
userUuid: dto.userUuid,
subscriptionSettingName: settingName,
userSubscriptionUuid: subscription.uuid,
})
if (subscriptionSetting === null) {
return {
success: false,
error: {
message: `Subscription setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
}
if (subscriptionSetting.sensitive && !dto.allowSensitiveRetrieval) {
return {
success: true,
sensitive: true,
}
}
const simpleSubscriptionSetting = await this.subscriptionSettingProjector.projectSimple(subscriptionSetting)
return {
success: true,
userUuid: dto.userUuid,
setting: simpleSubscriptionSetting,
}
}
const setting = await this.settingService.findSettingWithDecryptedValue({
userUuid,
settingName: settingName as SettingName,
userUuid: dto.userUuid,
settingName,
})
if (setting === null) {
return {
success: false,
error: {
message: `Setting ${settingName} for user ${userUuid} not found!`,
message: `Setting ${settingName.value} for user ${dto.userUuid} not found!`,
},
}
}
@@ -42,7 +110,7 @@ export class GetSetting implements UseCaseInterface {
return {
success: true,
userUuid,
userUuid: dto.userUuid,
setting: simpleSetting,
}
}