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
@@ -11,36 +11,84 @@ import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { User } from '../../User/User'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { UserSubscription } from '../../Subscription/UserSubscription'
import { UserSubscriptionType } from '../../Subscription/UserSubscriptionType'
describe('GetSettings', () => {
let settingRepository: SettingRepositoryInterface
let subscriptionSettingRepository: SubscriptionSettingRepositoryInterface
let userSubscriptionService: UserSubscriptionServiceInterface
let settingProjector: SettingProjector
let subscriptionSettingProjector: SubscriptionSettingProjector
let setting: Setting
let mfaSetting: Setting
let signInEmailsSetting: SubscriptionSetting
let userRepository: UserRepositoryInterface
let user: User
let crypter: CrypterInterface
let regularSubscription: UserSubscription
let sharedSubscription: UserSubscription
const createUseCase = () => new GetSettings(settingRepository, settingProjector, userRepository, crypter)
const createUseCase = () =>
new GetSettings(
settingRepository,
subscriptionSettingRepository,
userSubscriptionService,
settingProjector,
subscriptionSettingProjector,
userRepository,
crypter,
)
beforeEach(() => {
setting = {
name: 'test',
updatedAt: 345,
sensitive: false,
} as jest.Mocked<Setting>
setting = new Setting()
setting.name = 'test'
setting.updatedAt = 345
setting.sensitive = false
mfaSetting = {
name: SettingName.MfaSecret,
updatedAt: 122,
sensitive: true,
} as jest.Mocked<Setting>
mfaSetting = new Setting()
mfaSetting.name = SettingName.NAMES.MfaSecret
mfaSetting.updatedAt = 122
mfaSetting.sensitive = true
signInEmailsSetting = new SubscriptionSetting()
signInEmailsSetting.name = SettingName.NAMES.MuteSignInEmails
signInEmailsSetting.updatedAt = 122
signInEmailsSetting.sensitive = false
signInEmailsSetting.value = 'not_muted'
settingRepository = {} as jest.Mocked<SettingRepositoryInterface>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting, mfaSetting])
subscriptionSettingRepository = {} as jest.Mocked<SubscriptionSettingRepositoryInterface>
subscriptionSettingRepository.findAllBySubscriptionUuid = jest.fn().mockReturnValue([signInEmailsSetting])
regularSubscription = {
uuid: '1-2-3',
subscriptionType: UserSubscriptionType.Regular,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
sharedSubscription = {
uuid: '2-3-4',
subscriptionType: UserSubscriptionType.Shared,
user: Promise.resolve(user),
} as jest.Mocked<UserSubscription>
userSubscriptionService = {} as jest.Mocked<UserSubscriptionServiceInterface>
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription: null, sharedSubscription: null })
settingProjector = {} as jest.Mocked<SettingProjector>
settingProjector.projectManySimple = jest.fn().mockReturnValue([{ foo: 'bar' }])
settingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'bar' })
subscriptionSettingProjector = {} as jest.Mocked<SubscriptionSettingProjector>
subscriptionSettingProjector.projectSimple = jest.fn().mockReturnValue({ foo: 'sub-bar' })
user = {} as jest.Mocked<User>
@@ -51,83 +99,126 @@ describe('GetSettings', () => {
crypter.decryptForUser = jest.fn().mockReturnValue('decrypted')
})
it('should fail if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
describe('no subscription', () => {
it('should fail if a user is not found', async () => {
userRepository.findOneByUuid = jest.fn().mockReturnValue(null)
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
},
})
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: false,
error: {
message: 'User 1-2-3 not found.',
},
})
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting])
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
it('should return all setting with decrypted values', async () => {
setting = {
name: 'test',
updatedAt: 345,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting])
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
expect(subscriptionSettingProjector.projectSimple).not.toHaveBeenCalled()
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([
{
it('should return all setting with decrypted values', async () => {
setting = {
name: 'test',
updatedAt: 345,
value: 'encrypted',
serverEncryptionVersion: EncryptionVersion.Default,
} as jest.Mocked<Setting>
settingRepository.findAllByUserUuid = jest.fn().mockReturnValue([setting])
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
expect(settingProjector.projectSimple).toHaveBeenCalledWith({
name: 'test',
updatedAt: 345,
value: 'decrypted',
serverEncryptionVersion: 1,
},
])
})
it('should return all user settings of certain name', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting])
})
it('should return all user settings of certain name', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', settingName: 'test', allowSensitiveRetrieval: true }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
it('should return all user settings updated after', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting])
})
it('should return all user settings updated after', async () => {
expect(
await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true, updatedAfter: 123 }),
).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
})
it('should return all sensitive user settings if explicit', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }],
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
})
expect(settingProjector.projectManySimple).toHaveBeenCalledWith([setting, mfaSetting])
it('should return all sensitive user settings if explicit', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3', allowSensitiveRetrieval: true })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }, { foo: 'bar' }],
})
expect(settingProjector.projectSimple).toHaveBeenCalledTimes(2)
expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(1, setting)
expect(settingProjector.projectSimple).toHaveBeenNthCalledWith(2, mfaSetting)
})
})
describe('regular subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription: null })
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }, { foo: 'sub-bar' }],
})
expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('1-2-3')
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting)
})
})
describe('shared subscription', () => {
beforeEach(() => {
userSubscriptionService.findRegularSubscriptionForUserUuid = jest
.fn()
.mockReturnValue({ regularSubscription, sharedSubscription })
})
it('should return all user settings except mfa', async () => {
expect(await createUseCase().execute({ userUuid: '1-2-3' })).toEqual({
success: true,
userUuid: '1-2-3',
settings: [{ foo: 'bar' }, { foo: 'sub-bar' }],
})
expect(subscriptionSettingRepository.findAllBySubscriptionUuid).toHaveBeenCalledWith('2-3-4')
expect(settingProjector.projectSimple).toHaveBeenCalledWith(setting)
expect(subscriptionSettingProjector.projectSimple).toHaveBeenCalledWith(signInEmailsSetting)
})
})
})
@@ -9,12 +9,22 @@ import { Setting } from '../../Setting/Setting'
import { UserRepositoryInterface } from '../../User/UserRepositoryInterface'
import { CrypterInterface } from '../../Encryption/CrypterInterface'
import { EncryptionVersion } from '../../Encryption/EncryptionVersion'
import { UserSubscriptionServiceInterface } from '../../Subscription/UserSubscriptionServiceInterface'
import { SubscriptionSettingRepositoryInterface } from '../../Setting/SubscriptionSettingRepositoryInterface'
import { SubscriptionSetting } from '../../Setting/SubscriptionSetting'
import { SimpleSetting } from '../../Setting/SimpleSetting'
import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting'
import { SubscriptionSettingProjector } from '../../../Projection/SubscriptionSettingProjector'
@injectable()
export class GetSettings implements UseCaseInterface {
constructor(
@inject(TYPES.SettingRepository) private settingRepository: SettingRepositoryInterface,
@inject(TYPES.SubscriptionSettingRepository)
private subscriptionSettingRepository: SubscriptionSettingRepositoryInterface,
@inject(TYPES.UserSubscriptionService) private userSubscriptionService: UserSubscriptionServiceInterface,
@inject(TYPES.SettingProjector) private settingProjector: SettingProjector,
@inject(TYPES.SubscriptionSettingProjector) private subscriptionSettingProjector: SubscriptionSettingProjector,
@inject(TYPES.UserRepository) private userRepository: UserRepositoryInterface,
@inject(TYPES.Crypter) private crypter: CrypterInterface,
) {}
@@ -33,27 +43,43 @@ export class GetSettings implements UseCaseInterface {
}
}
let settings = await this.settingRepository.findAllByUserUuid(userUuid)
let settings: Array<Setting | SubscriptionSetting>
settings = await this.settingRepository.findAllByUserUuid(userUuid)
const { regularSubscription, sharedSubscription } =
await this.userSubscriptionService.findRegularSubscriptionForUserUuid(user.uuid)
const subscription = sharedSubscription ?? regularSubscription
if (subscription) {
const subscriptionSettings = await this.subscriptionSettingRepository.findAllBySubscriptionUuid(subscription.uuid)
settings = settings.concat(subscriptionSettings)
}
if (dto.settingName !== undefined) {
settings = settings.filter((setting: Setting) => setting.name === dto.settingName)
settings = settings.filter((setting: Setting | SubscriptionSetting) => setting.name === dto.settingName)
}
if (dto.updatedAfter !== undefined) {
settings = settings.filter((setting: Setting) => setting.updatedAt >= (dto.updatedAfter as number))
settings = settings.filter(
(setting: Setting | SubscriptionSetting) => setting.updatedAt >= (dto.updatedAfter as number),
)
}
if (!dto.allowSensitiveRetrieval) {
settings = settings.filter((setting: Setting) => !setting.sensitive)
settings = settings.filter((setting: Setting | SubscriptionSetting) => !setting.sensitive)
}
const simpleSettings: Array<SimpleSetting | SimpleSubscriptionSetting> = []
for (const setting of settings) {
if (setting.value !== null && setting.serverEncryptionVersion === EncryptionVersion.Default) {
setting.value = await this.crypter.decryptForUser(setting.value, user)
}
}
const simpleSettings = await this.settingProjector.projectManySimple(settings)
if (setting instanceof SubscriptionSetting) {
simpleSettings.push(await this.subscriptionSettingProjector.projectSimple(setting))
} else {
simpleSettings.push(await this.settingProjector.projectSimple(setting))
}
}
return {
success: true,
@@ -1,10 +1,11 @@
import { SimpleSetting } from '../../Setting/SimpleSetting'
import { SimpleSubscriptionSetting } from '../../Setting/SimpleSubscriptionSetting'
export type GetSettingsResponse =
| {
success: true
userUuid: string
settings: SimpleSetting[]
settings: Array<SimpleSetting | SimpleSubscriptionSetting>
}
| {
success: false