Compare commits

...

27 Commits

Author SHA1 Message Date
standardci d660721f95 chore(release): publish new version
- @standardnotes/scheduler-server@1.14.1
2022-12-07 11:25:27 +00:00
Karol Sójko c52bb93d79 fix(scheduler): importing email contents 2022-12-07 12:23:29 +01:00
standardci ffb6bfd0c9 chore(release): publish new version
- @standardnotes/scheduler-server@1.14.0
2022-12-07 10:12:08 +00:00
Karol Sójko 6e0855f9b3 feat(scheduler): add scheduled emails contents 2022-12-07 11:10:13 +01:00
standardci ec9e9ec387 chore(release): publish new version
- @standardnotes/analytics@2.12.6
 - @standardnotes/api-gateway@1.39.10
 - @standardnotes/auth-server@1.63.2
 - @standardnotes/domain-events-infra@1.9.40
 - @standardnotes/domain-events@2.95.0
 - @standardnotes/event-store@1.6.36
 - @standardnotes/files-server@1.8.36
 - @standardnotes/revisions-server@1.9.9
 - @standardnotes/scheduler-server@1.13.37
 - @standardnotes/syncing-server@1.20.9
 - @standardnotes/websockets-server@1.4.37
 - @standardnotes/workspace-server@1.17.36
2022-12-07 09:53:15 +00:00
Karol Sójko fa75aa40f0 feat(domain-events): add email requested event 2022-12-07 10:51:22 +01:00
standardci b865953c22 chore(release): publish new version
- @standardnotes/analytics@2.12.5
 - @standardnotes/api-gateway@1.39.9
 - @standardnotes/auth-server@1.63.1
 - @standardnotes/domain-events-infra@1.9.39
 - @standardnotes/domain-events@2.94.1
 - @standardnotes/event-store@1.6.35
 - @standardnotes/files-server@1.8.35
 - @standardnotes/revisions-server@1.9.8
 - @standardnotes/scheduler-server@1.13.36
 - @standardnotes/syncing-server@1.20.8
 - @standardnotes/websockets-server@1.4.36
 - @standardnotes/workspace-server@1.17.35
2022-12-07 06:14:24 +00:00
Karol Sójko 2542cf6f9a fix(auth): remove not needed event from factory 2022-12-07 07:12:21 +01:00
Karol Sójko cb9499b87f fix(domain-events): remove not used event 2022-12-07 07:07:13 +01:00
standardci c351f01f67 chore(release): publish new version
- @standardnotes/analytics@2.12.4
 - @standardnotes/auth-server@1.63.0
 - @standardnotes/domain-core@1.9.0
 - @standardnotes/revisions-server@1.9.7
 - @standardnotes/syncing-server@1.20.7
2022-12-07 06:06:35 +00:00
Karol Sójko c87561fca7 feat(domain-core): rename email subscription rejection level to email level 2022-12-07 07:04:42 +01:00
standardci a363c143fa chore(release): publish new version
- @standardnotes/auth-server@1.62.1
2022-12-06 13:15:21 +00:00
Karol Sójko fb81d2b926 fix(auth): remove redundant specs and fix stream query 2022-12-06 14:12:54 +01:00
standardci 05b1b8f079 chore(release): publish new version
- @standardnotes/auth-server@1.62.0
2022-12-06 10:49:12 +00:00
Karol Sójko 7848dc06d4 feat(auth): add procedure for email subscriptions sync 2022-12-06 11:47:17 +01:00
standardci 3a005719b7 chore(release): publish new version
- @standardnotes/auth-server@1.61.0
2022-12-06 10:02:20 +00:00
Karol Sójko 6928988f78 feat(auth): add publishing mute emails setting changed event 2022-12-06 11:00:14 +01:00
standardci a521894d7c chore(release): publish new version
- @standardnotes/analytics@2.12.3
 - @standardnotes/api-gateway@1.39.8
 - @standardnotes/auth-server@1.60.17
 - @standardnotes/domain-events-infra@1.9.38
 - @standardnotes/domain-events@2.94.0
 - @standardnotes/event-store@1.6.34
 - @standardnotes/files-server@1.8.34
 - @standardnotes/revisions-server@1.9.6
 - @standardnotes/scheduler-server@1.13.35
 - @standardnotes/syncing-server@1.20.6
 - @standardnotes/websockets-server@1.4.35
 - @standardnotes/workspace-server@1.17.34
2022-12-06 09:30:07 +00:00
Karol Sójko b7fb1d9c08 feat(domain-events): add mute emails setting changed event 2022-12-06 10:28:04 +01:00
standardci 5f67e45911 chore(release): publish new version
- @standardnotes/analytics@2.12.2
 - @standardnotes/api-gateway@1.39.7
 - @standardnotes/auth-server@1.60.16
 - @standardnotes/domain-events-infra@1.9.37
 - @standardnotes/domain-events@2.93.0
 - @standardnotes/event-store@1.6.33
 - @standardnotes/files-server@1.8.33
 - @standardnotes/revisions-server@1.9.5
 - @standardnotes/scheduler-server@1.13.34
 - @standardnotes/syncing-server@1.20.5
 - @standardnotes/websockets-server@1.4.34
 - @standardnotes/workspace-server@1.17.33
2022-12-05 14:34:06 +00:00
Karol Sójko fddf9fccbd feat(domain-events): add email subscription sync requested event 2022-12-05 15:32:10 +01:00
standardci 2bedbd7bd2 chore(release): publish new version
- @standardnotes/analytics@2.12.1
 - @standardnotes/domain-core@1.8.0
 - @standardnotes/revisions-server@1.9.4
 - @standardnotes/syncing-server@1.20.4
2022-12-05 10:40:29 +00:00
Karol Sójko 02f3c85796 feat(domain-core): add email subscription rejection levels 2022-12-05 11:38:23 +01:00
standardci 3b5bd6a47f chore(release): publish new version
- @standardnotes/analytics@2.12.0
 - @standardnotes/domain-core@1.7.0
 - @standardnotes/revisions-server@1.9.3
 - @standardnotes/syncing-server@1.20.3
2022-12-05 09:25:02 +00:00
Karol Sójko 06fd404d44 feat(domain-core): distinguish between username and email 2022-12-05 10:22:59 +01:00
standardci d931c52508 chore(release): publish new version
- @standardnotes/analytics@2.11.17
 - @standardnotes/domain-core@1.6.0
 - @standardnotes/revisions-server@1.9.2
 - @standardnotes/syncing-server@1.20.2
2022-12-02 08:33:51 +00:00
Karol Sójko 800fe9e4c8 feat(domain-core): add subscription plan name value object 2022-12-02 09:32:05 +01:00
109 changed files with 1017 additions and 2387 deletions
Generated
+2
View File
@@ -2649,6 +2649,7 @@ const RAW_RUNTIME_STATE =
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/api", "npm:1.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/features", "npm:1.53.1"],\
@@ -3055,6 +3056,7 @@ const RAW_RUNTIME_STATE =
["@newrelic/winston-enricher", "virtual:c66bf20e88479ada0172094776519a9f51acc4731d22079b60a295bcec7ea42d5545cbce58a77a50d932bf953298799135e99707486e343da6d99ba1d167bdbd#npm:4.0.0"],\
["@sentry/node", "npm:7.19.0"],\
["@standardnotes/common", "workspace:packages/common"],\
["@standardnotes/domain-core", "workspace:packages/domain-core"],\
["@standardnotes/domain-events", "workspace:packages/domain-events"],\
["@standardnotes/domain-events-infra", "workspace:packages/domain-events-infra"],\
["@standardnotes/predicates", "workspace:packages/predicates"],\
+34
View File
@@ -3,6 +3,40 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [2.12.6](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.5...@standardnotes/analytics@2.12.6) (2022-12-07)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.5](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.4...@standardnotes/analytics@2.12.5) (2022-12-07)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.4](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.3...@standardnotes/analytics@2.12.4) (2022-12-07)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.3](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.2...@standardnotes/analytics@2.12.3) (2022-12-06)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.2](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.1...@standardnotes/analytics@2.12.2) (2022-12-05)
**Note:** Version bump only for package @standardnotes/analytics
## [2.12.1](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.12.0...@standardnotes/analytics@2.12.1) (2022-12-05)
**Note:** Version bump only for package @standardnotes/analytics
# [2.12.0](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.17...@standardnotes/analytics@2.12.0) (2022-12-05)
### Features
* **domain-core:** distinguish between username and email ([06fd404](https://github.com/standardnotes/server/commit/06fd404d44b44a53733f889aabd4da63f21e2f36))
## [2.11.17](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.16...@standardnotes/analytics@2.11.17) (2022-12-02)
**Note:** Version bump only for package @standardnotes/analytics
## [2.11.16](https://github.com/standardnotes/server/compare/@standardnotes/analytics@2.11.15...@standardnotes/analytics@2.11.16) (2022-12-02)
**Note:** Version bump only for package @standardnotes/analytics
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/analytics",
"version": "2.11.16",
"version": "2.12.6",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -18,5 +18,5 @@ export class AnalyticsEntity {
nullable: true,
})
@Index('email')
declare userEmail: string
declare username: string
}
@@ -1,7 +1,7 @@
import { DomainEventHandlerInterface, SubscriptionCancelledEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Email } from '@standardnotes/domain-core'
import { Username } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -41,7 +41,7 @@ export class SubscriptionCancelledEventHandler implements DomainEventHandlerInte
payedAmount: event.payload.payAmount,
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
subscriptionId: event.payload.subscriptionId,
userEmail: Email.create(event.payload.userEmail).getValue(),
username: Username.create(event.payload.userEmail).getValue(),
userUuid,
})
@@ -1,7 +1,7 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionExpiredEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Email } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -45,7 +45,7 @@ export class SubscriptionExpiredEventHandler implements DomainEventHandlerInterf
payedAmount: event.payload.payAmount,
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
subscriptionId: event.payload.subscriptionId,
userEmail: Email.create(event.payload.userEmail).getValue(),
username: Username.create(event.payload.userEmail).getValue(),
userUuid,
})
@@ -1,7 +1,7 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionPurchasedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Email } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -69,7 +69,7 @@ export class SubscriptionPurchasedEventHandler implements DomainEventHandlerInte
payedAmount: event.payload.payAmount,
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
subscriptionId: event.payload.subscriptionId,
userEmail: Email.create(event.payload.userEmail).getValue(),
username: Username.create(event.payload.userEmail).getValue(),
userUuid,
})
@@ -1,7 +1,7 @@
import { Username } from '@standardnotes/domain-core'
import { DomainEventHandlerInterface, SubscriptionRefundedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Logger } from 'winston'
import { Email } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { AnalyticsActivity } from '../Analytics/AnalyticsActivity'
@@ -41,7 +41,7 @@ export class SubscriptionRefundedEventHandler implements DomainEventHandlerInter
payedAmount: event.payload.payAmount,
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
subscriptionId: event.payload.subscriptionId,
userEmail: Email.create(event.payload.userEmail).getValue(),
username: Username.create(event.payload.userEmail).getValue(),
userUuid,
})
@@ -1,6 +1,6 @@
import { DomainEventHandlerInterface, SubscriptionRenewedEvent } from '@standardnotes/domain-events'
import { inject, injectable } from 'inversify'
import { Email } from '@standardnotes/domain-core'
import { Username } from '@standardnotes/domain-core'
import TYPES from '../../Bootstrap/Types'
import { GetUserAnalyticsId } from '../UseCase/GetUserAnalyticsId/GetUserAnalyticsId'
@@ -41,7 +41,7 @@ export class SubscriptionRenewedEventHandler implements DomainEventHandlerInterf
payedAmount: event.payload.payAmount,
planName: SubscriptionPlanName.create(event.payload.subscriptionName).getValue(),
subscriptionId: event.payload.subscriptionId,
userEmail: Email.create(event.payload.userEmail).getValue(),
username: Username.create(event.payload.userEmail).getValue(),
userUuid,
})
@@ -18,7 +18,7 @@ export class UserRegisteredEventHandler implements DomainEventHandlerInterface {
async handle(event: UserRegisteredEvent): Promise<void> {
let analyticsEntity = new AnalyticsEntity()
analyticsEntity.userUuid = event.payload.userUuid
analyticsEntity.userEmail = event.payload.email
analyticsEntity.username = event.payload.email
analyticsEntity = await this.analyticsEntityRepository.save(analyticsEntity)
await this.analyticsStore.markActivity([AnalyticsActivity.Register], analyticsEntity.id, [
@@ -1,5 +1,5 @@
import { injectable } from 'inversify'
import { Email, MapperInterface, UniqueEntityId } from '@standardnotes/domain-core'
import { MapperInterface, UniqueEntityId, Username } from '@standardnotes/domain-core'
import { TypeORMRevenueModification } from '../../Infra/TypeORM/TypeORMRevenueModification'
import { MonthlyRevenue } from '../Revenue/MonthlyRevenue'
@@ -14,7 +14,7 @@ export class RevenueModificationMap implements MapperInterface<RevenueModificati
toDomain(persistence: TypeORMRevenueModification): RevenueModification {
const userOrError = User.create(
{
email: Email.create(persistence.userEmail).getValue(),
username: Username.create(persistence.username).getValue(),
},
new UniqueEntityId(persistence.userUuid),
)
@@ -70,7 +70,7 @@ export class RevenueModificationMap implements MapperInterface<RevenueModificati
persistence.previousMonthlyRevenue = domain.props.previousMonthlyRevenue.value
persistence.subscriptionId = subscription.id.toValue() as number
persistence.subscriptionPlan = subscription.props.planName.value
persistence.userEmail = user.props.email.value
persistence.username = user.props.username.value
persistence.userUuid = user.id.toString()
persistence.createdAt = domain.props.createdAt
@@ -1,4 +1,4 @@
import { Email } from '@standardnotes/domain-core'
import { Username } from '@standardnotes/domain-core'
import { Subscription } from '../Subscription/Subscription'
import { SubscriptionEventType } from '../Subscription/SubscriptionEventType'
@@ -19,7 +19,7 @@ describe('RevenueModification', () => {
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
}).getValue()
user = User.create({
email: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
}).getValue()
})
@@ -15,7 +15,7 @@ describe('GetUserAnalyticsId', () => {
analyticsEntity = {
id: 123,
userUuid: '84c0f8e8-544a-4c7e-9adf-26209303bc1d',
userEmail: 'test@test.te',
username: 'test@test.te',
} as jest.Mocked<AnalyticsEntity>
analyticsEntityRepository = {} as jest.Mocked<AnalyticsEntityRepositoryInterface>
@@ -1,5 +1,5 @@
import { inject, injectable } from 'inversify'
import { Email, Uuid } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
import TYPES from '../../../Bootstrap/Types'
import { AnalyticsEntityRepositoryInterface } from '../../Entity/AnalyticsEntityRepositoryInterface'
@@ -28,7 +28,7 @@ export class GetUserAnalyticsId implements UseCaseInterface {
return {
analyticsId: analyticsEntity.id,
userUuid: Uuid.create(analyticsEntity.userUuid).getValue(),
userEmail: Email.create(analyticsEntity.userEmail).getValue(),
username: Username.create(analyticsEntity.username).getValue(),
}
}
}
@@ -1,7 +1,7 @@
import { Email, Uuid } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
export type GetUserAnalyticsIdResponse = {
analyticsId: number
userEmail: Email
username: Username
userUuid: Uuid
}
@@ -1,7 +1,7 @@
import 'reflect-metadata'
import { TimerInterface } from '@standardnotes/time'
import { Email, Result, Uuid } from '@standardnotes/domain-core'
import { Result, Username, Uuid } from '@standardnotes/domain-core'
import { MonthlyRevenue } from '../../Revenue/MonthlyRevenue'
@@ -45,7 +45,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -63,7 +63,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -81,7 +81,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 2,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -101,7 +101,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -122,7 +122,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -142,7 +142,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -162,7 +162,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -182,7 +182,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -202,7 +202,7 @@ describe('SaveRevenueModification', () => {
payedAmount: 12.99,
planName: SubscriptionPlanName.create('PRO_PLAN').getValue(),
subscriptionId: 1234,
userEmail: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
userUuid: Uuid.create('84c0f8e8-544a-4c7e-9adf-26209303bc1d').getValue(),
})
@@ -23,7 +23,7 @@ export class SaveRevenueModification implements DomainUseCaseInterface<RevenueMo
async execute(dto: SaveRevenueModificationDTO): Promise<Result<RevenueModification>> {
const userOrError = User.create(
{
email: dto.userEmail,
username: dto.username,
},
new UniqueEntityId(dto.userUuid.value),
)
@@ -1,4 +1,4 @@
import { Email, Uuid } from '@standardnotes/domain-core'
import { Username, Uuid } from '@standardnotes/domain-core'
import { SubscriptionEventType } from '../../Subscription/SubscriptionEventType'
import { SubscriptionPlanName } from '../../Subscription/SubscriptionPlanName'
@@ -9,7 +9,7 @@ export interface SaveRevenueModificationDTO {
planName: SubscriptionPlanName
newSubscriber: boolean
userUuid: Uuid
userEmail: Email
username: Username
subscriptionId: number
billingFrequency: number
}
@@ -1,11 +1,11 @@
import { Email } from '@standardnotes/domain-core'
import { Username } from '@standardnotes/domain-core'
import { User } from './User'
describe('User', () => {
it('should create an entity', () => {
const user = User.create({
email: Email.create('test@test.te').getValue(),
username: Username.create('test@test.te').getValue(),
}).getValue()
expect(user.id.toString()).toHaveLength(36)
@@ -1,5 +1,5 @@
import { Email } from '@standardnotes/domain-core'
import { Username } from '@standardnotes/domain-core'
export interface UserProps {
email: Email
username: Username
}
@@ -18,7 +18,7 @@ export class TypeORMRevenueModification {
length: 255,
})
@Index('email')
declare userEmail: string
declare username: string
@Column({
name: 'user_uuid',
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.39.10](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.9...@standardnotes/api-gateway@1.39.10) (2022-12-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.9](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.8...@standardnotes/api-gateway@1.39.9) (2022-12-07)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.8](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.7...@standardnotes/api-gateway@1.39.8) (2022-12-06)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.7](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.6...@standardnotes/api-gateway@1.39.7) (2022-12-05)
**Note:** Version bump only for package @standardnotes/api-gateway
## [1.39.6](https://github.com/standardnotes/api-gateway/compare/@standardnotes/api-gateway@1.39.5...@standardnotes/api-gateway@1.39.6) (2022-11-30)
**Note:** Version bump only for package @standardnotes/api-gateway
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/api-gateway",
"version": "1.39.6",
"version": "1.39.10",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+42
View File
@@ -3,6 +3,48 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.63.2](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.1...@standardnotes/auth-server@1.63.2) (2022-12-07)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.63.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.63.0...@standardnotes/auth-server@1.63.1) (2022-12-07)
### Bug Fixes
* **auth:** remove not needed event from factory ([2542cf6](https://github.com/standardnotes/server/commit/2542cf6f9a40c3a5eb4e11ead3cbbc25afefae48))
# [1.63.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.62.1...@standardnotes/auth-server@1.63.0) (2022-12-07)
### Features
* **domain-core:** rename email subscription rejection level to email level ([c87561f](https://github.com/standardnotes/server/commit/c87561fca782883b84f58b4f0b9f85ecc279ca50))
## [1.62.1](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.62.0...@standardnotes/auth-server@1.62.1) (2022-12-06)
### Bug Fixes
* **auth:** remove redundant specs and fix stream query ([fb81d2b](https://github.com/standardnotes/server/commit/fb81d2b9260cf7bee3e3e6911d5a6e8eb1d650e3))
# [1.62.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.61.0...@standardnotes/auth-server@1.62.0) (2022-12-06)
### Features
* **auth:** add procedure for email subscriptions sync ([7848dc0](https://github.com/standardnotes/server/commit/7848dc06d4f4fe8c380ed45c32e23ac0e62014fa))
# [1.61.0](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.17...@standardnotes/auth-server@1.61.0) (2022-12-06)
### Features
* **auth:** add publishing mute emails setting changed event ([6928988](https://github.com/standardnotes/server/commit/6928988f7855c939f2365e35cb6cb0ff18e5c37a))
## [1.60.17](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.16...@standardnotes/auth-server@1.60.17) (2022-12-06)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.60.16](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.15...@standardnotes/auth-server@1.60.16) (2022-12-05)
**Note:** Version bump only for package @standardnotes/auth-server
## [1.60.15](https://github.com/standardnotes/server/compare/@standardnotes/auth-server@1.60.14...@standardnotes/auth-server@1.60.15) (2022-11-30)
**Note:** Version bump only for package @standardnotes/auth-server
+12 -12
View File
@@ -5,64 +5,64 @@ COMMAND=$1 && shift 1
case "$COMMAND" in
'start-local' )
echo "Starting Web..."
echo "[Docker] Starting Web..."
yarn workspace @standardnotes/auth-server start:local
;;
'start-web' )
echo "Starting Web..."
echo "[Docker] Starting Web..."
yarn workspace @standardnotes/auth-server start
;;
'start-worker' )
echo "Starting Worker..."
echo "[Docker] Starting Worker..."
yarn workspace @standardnotes/auth-server worker
;;
'email-daily-backup' )
echo "Starting Email Daily Backup..."
echo "[Docker] Starting Email Daily Backup..."
yarn workspace @standardnotes/auth-server daily-backup:email
;;
'email-weekly-backup' )
echo "Starting Email Weekly Backup..."
echo "[Docker] Starting Email Weekly Backup..."
yarn workspace @standardnotes/auth-server weekly-backup:email
;;
'email-backup' )
echo "Starting Email Backup For Single User..."
echo "[Docker] Starting Email Backup For Single User..."
EMAIL=$1 && shift 1
yarn workspace @standardnotes/auth-server user-email-backup $EMAIL
;;
'dropbox-daily-backup' )
echo "Starting Dropbox Daily Backup..."
echo "[Docker] Starting Dropbox Daily Backup..."
yarn workspace @standardnotes/auth-server daily-backup:dropbox
;;
'google-drive-daily-backup' )
echo "Starting Google Drive Daily Backup..."
echo "[Docker] Starting Google Drive Daily Backup..."
yarn workspace @standardnotes/auth-server daily-backup:google_drive
;;
'one-drive-daily-backup' )
echo "Starting One Drive Daily Backup..."
echo "[Docker] Starting One Drive Daily Backup..."
yarn workspace @standardnotes/auth-server daily-backup:one_drive
;;
'email-campaign' )
echo "Starting Email Campaign Sending..."
echo "[Docker] Starting Email Campaign Sending..."
MESSAGE_IDENTIFIER=$1 && shift 1
yarn workspace @standardnotes/auth-server email-campaign $MESSAGE_IDENTIFIER
;;
'content-recalculation' )
echo "Starting Content Size Recalculation..."
echo "[Docker] Starting Content Size Recalculation..."
yarn workspace @standardnotes/auth-server content-recalculation
;;
* )
echo "Unknown command"
echo "[Docker] Unknown command"
;;
esac
+1 -1
View File
@@ -7,6 +7,6 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', 'HealthCheckController', '/Infra/'],
coveragePathIgnorePatterns: ['/Bootstrap/', '/InversifyExpressUtils/', '/Infra/', '/Projection/'],
setupFilesAfterEnv: ['./test-setup.ts'],
}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/auth-server",
"version": "1.60.15",
"version": "1.63.2",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -35,6 +35,7 @@
"@sentry/node": "^7.19.0",
"@standardnotes/api": "^1.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/features": "^1.52.1",
@@ -20,6 +20,7 @@ import {
WebSocketMessageRequestedEvent,
ExitDiscountApplyRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
MuteEmailsSettingChangedEvent,
} from '@standardnotes/domain-events'
import { Predicate, PredicateVerificationResult } from '@standardnotes/predicates'
import { TimerInterface } from '@standardnotes/time'
@@ -32,6 +33,25 @@ import { DomainEventFactoryInterface } from './DomainEventFactoryInterface'
export class DomainEventFactory implements DomainEventFactoryInterface {
constructor(@inject(TYPES.Timer) private timer: TimerInterface) {}
createMuteEmailsSettingChangedEvent(dto: {
username: string
mute: boolean
emailSubscriptionRejectionLevel: string
}): MuteEmailsSettingChangedEvent {
return {
type: 'MUTE_EMAILS_SETTING_CHANGED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
userIdentifier: dto.username,
userIdentifierType: 'email',
},
origin: DomainEventService.Auth,
},
payload: dto,
}
}
createUserContentSizeRecalculationRequestedEvent(userUuid: string): UserContentSizeRecalculationRequestedEvent {
return {
type: 'USER_CONTENT_SIZE_RECALCULATION_REQUESTED',
@@ -18,6 +18,7 @@ import {
WebSocketMessageRequestedEvent,
ExitDiscountApplyRequestedEvent,
UserContentSizeRecalculationRequestedEvent,
MuteEmailsSettingChangedEvent,
} from '@standardnotes/domain-events'
import { InviteeIdentifierType } from '../SharedSubscription/InviteeIdentifierType'
@@ -91,4 +92,9 @@ export interface DomainEventFactoryInterface {
userEmail: string
discountCode: string
}): ExitDiscountApplyRequestedEvent
createMuteEmailsSettingChangedEvent(dto: {
username: string
mute: boolean
emailSubscriptionRejectionLevel: string
}): MuteEmailsSettingChangedEvent
}
@@ -2,11 +2,13 @@ import {
CloudBackupRequestedEvent,
DomainEventPublisherInterface,
EmailBackupRequestedEvent,
MuteEmailsSettingChangedEvent,
UserDisabledSessionUserAgentLoggingEvent,
} from '@standardnotes/domain-events'
import {
EmailBackupFrequency,
LogSessionUserAgentOption,
MuteMarketingEmailsOption,
OneDriveBackupFrequency,
SettingName,
} from '@standardnotes/settings'
@@ -57,6 +59,9 @@ describe('SettingInterpreter', () => {
domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<UserDisabledSessionUserAgentLoggingEvent>)
domainEventFactory.createMuteEmailsSettingChangedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<MuteEmailsSettingChangedEvent>)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
@@ -201,6 +206,23 @@ describe('SettingInterpreter', () => {
)
})
it('should trigger mute subscription emails rejection if mute setting changed', async () => {
const setting = {
name: SettingName.MuteMarketingEmails,
value: MuteMarketingEmailsOption.Muted,
} as jest.Mocked<Setting>
settingRepository.findOneByNameAndUserUuid = jest.fn().mockReturnValue(null)
await createInterpreter().interpretSettingUpdated(setting, user, MuteMarketingEmailsOption.Muted)
expect(domainEventPublisher.publish).toHaveBeenCalled()
expect(domainEventFactory.createMuteEmailsSettingChangedEvent).toHaveBeenCalledWith({
emailSubscriptionRejectionLevel: 'MARKETING',
mute: true,
username: 'test@test.te',
})
})
it('should trigger cloud backup if backup frequency setting is updated and a backup token setting is present', async () => {
settingRepository.findLastByNameAndUserUuid = jest.fn().mockReturnValueOnce({
name: SettingName.OneDriveBackupToken,
@@ -1,4 +1,5 @@
import { DomainEventPublisherInterface } from '@standardnotes/domain-events'
import { EmailLevel } from '@standardnotes/domain-core'
import {
DropboxBackupFrequency,
EmailBackupFrequency,
@@ -39,6 +40,13 @@ export class SettingInterpreter implements SettingInterpreterInterface {
OneDriveBackupFrequency.Disabled,
]
private readonly emailSettingToSubscriptionRejectionLevelMap: Map<SettingName, string> = new Map([
[SettingName.MuteFailedBackupsEmails, EmailLevel.LEVELS.FailedEmailBackup],
[SettingName.MuteFailedCloudBackupsEmails, EmailLevel.LEVELS.FailedCloudBackup],
[SettingName.MuteMarketingEmails, EmailLevel.LEVELS.Marketing],
[SettingName.MuteSignInEmails, EmailLevel.LEVELS.SignIn],
])
constructor(
@inject(TYPES.DomainEventPublisher) private domainEventPublisher: DomainEventPublisherInterface,
@inject(TYPES.DomainEventFactory) private domainEventFactory: DomainEventFactoryInterface,
@@ -48,6 +56,10 @@ export class SettingInterpreter implements SettingInterpreterInterface {
) {}
async interpretSettingUpdated(updatedSetting: Setting, user: User, unencryptedValue: string | null): Promise<void> {
if (this.isChangingMuteEmailsSetting(updatedSetting)) {
await this.triggerEmailSubscriptionChange(user, updatedSetting.name as SettingName, unencryptedValue)
}
if (this.isEnablingEmailBackupSetting(updatedSetting)) {
await this.triggerEmailBackup(user.uuid)
}
@@ -78,6 +90,15 @@ export class SettingInterpreter implements SettingInterpreterInterface {
)
}
private isChangingMuteEmailsSetting(setting: Setting): boolean {
return [
SettingName.MuteFailedBackupsEmails,
SettingName.MuteFailedCloudBackupsEmails,
SettingName.MuteMarketingEmails,
SettingName.MuteSignInEmails,
].includes(setting.name as SettingName)
}
private isEnablingEmailBackupSetting(setting: Setting): boolean {
return setting.name === SettingName.EmailBackupFrequency && setting.value !== EmailBackupFrequency.Disabled
}
@@ -96,6 +117,20 @@ export class SettingInterpreter implements SettingInterpreterInterface {
return SettingName.LogSessionUserAgent === setting.name && LogSessionUserAgentOption.Disabled === setting.value
}
private async triggerEmailSubscriptionChange(
user: User,
settingName: SettingName,
unencryptedValue: string | null,
): Promise<void> {
await this.domainEventPublisher.publish(
this.domainEventFactory.createMuteEmailsSettingChangedEvent({
username: user.email,
mute: unencryptedValue === 'muted',
emailSubscriptionRejectionLevel: this.emailSettingToSubscriptionRejectionLevelMap.get(settingName) as string,
}),
)
}
private async triggerSessionUserAgentCleanup(user: User) {
await this.domainEventPublisher.publish(
this.domainEventFactory.createUserDisabledSessionUserAgentLoggingEvent({
@@ -1,57 +0,0 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { OfflineSetting } from '../../Domain/Setting/OfflineSetting'
import { OfflineSettingName } from '../../Domain/Setting/OfflineSettingName'
import { MySQLOfflineSettingRepository } from './MySQLOfflineSettingRepository'
describe('MySQLOfflineSettingRepository', () => {
let queryBuilder: SelectQueryBuilder<OfflineSetting>
let offlineSetting: OfflineSetting
let ormRepository: Repository<OfflineSetting>
const createRepository = () => new MySQLOfflineSettingRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<OfflineSetting>>
offlineSetting = {} as jest.Mocked<OfflineSetting>
ormRepository = {} as jest.Mocked<Repository<OfflineSetting>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
})
it('should save', async () => {
await createRepository().save(offlineSetting)
expect(ormRepository.save).toHaveBeenCalledWith(offlineSetting)
})
it('should find one setting by name and user email', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(offlineSetting)
const result = await createRepository().findOneByNameAndEmail(OfflineSettingName.FeaturesToken, 'test@test.com')
expect(queryBuilder.where).toHaveBeenCalledWith('offline_setting.name = :name AND offline_setting.email = :email', {
name: 'FEATURES_TOKEN',
email: 'test@test.com',
})
expect(result).toEqual(offlineSetting)
})
it('should find one setting by name and value', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(offlineSetting)
const result = await createRepository().findOneByNameAndValue(OfflineSettingName.FeaturesToken, 'features-token')
expect(queryBuilder.where).toHaveBeenCalledWith('offline_setting.name = :name AND offline_setting.value = :value', {
name: 'FEATURES_TOKEN',
value: 'features-token',
})
expect(result).toEqual(offlineSetting)
})
})
@@ -1,189 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
import { MySQLOfflineUserSubscriptionRepository } from './MySQLOfflineUserSubscriptionRepository'
import { OfflineUserSubscription } from '../../Domain/Subscription/OfflineUserSubscription'
describe('MySQLOfflineUserSubscriptionRepository', () => {
let selectQueryBuilder: SelectQueryBuilder<OfflineUserSubscription>
let updateQueryBuilder: UpdateQueryBuilder<OfflineUserSubscription>
let offlineSubscription: OfflineUserSubscription
let ormRepository: Repository<OfflineUserSubscription>
const createRepository = () => new MySQLOfflineUserSubscriptionRepository(ormRepository)
beforeEach(() => {
selectQueryBuilder = {} as jest.Mocked<SelectQueryBuilder<OfflineUserSubscription>>
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<OfflineUserSubscription>>
offlineSubscription = {
planName: SubscriptionName.ProPlan,
cancelled: false,
email: 'test@test.com',
} as jest.Mocked<OfflineUserSubscription>
ormRepository = {} as jest.Mocked<Repository<OfflineUserSubscription>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
ormRepository.save = jest.fn()
})
it('should save', async () => {
await createRepository().save(offlineSubscription)
expect(ormRepository.save).toHaveBeenCalledWith(offlineSubscription)
})
it('should find one longest lasting uncanceled subscription by user email if there are canceled ones', async () => {
const canceledSubscription = {
planName: SubscriptionName.ProPlan,
cancelled: true,
email: 'test@test.com',
} as jest.Mocked<OfflineUserSubscription>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, offlineSubscription])
const result = await createRepository().findOneByEmail('test@test.com')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
email: 'test@test.com',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual(offlineSubscription)
})
it('should find one, longest lasting subscription by user email if there are no canceled ones', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([offlineSubscription])
const result = await createRepository().findOneByEmail('test@test.com')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
email: 'test@test.com',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual(offlineSubscription)
})
it('should find one, longest lasting subscription by user email if there are no ucanceled ones', async () => {
offlineSubscription.cancelled = true
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([offlineSubscription])
const result = await createRepository().findOneByEmail('test@test.com')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
email: 'test@test.com',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual(offlineSubscription)
})
it('should find none if there are no subscriptions for the user', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([])
const result = await createRepository().findOneByEmail('test@test.com')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email', {
email: 'test@test.com',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toBeNull()
})
it('should find multiple by user email active after', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([offlineSubscription])
const result = await createRepository().findByEmail('test@test.com', 123)
expect(selectQueryBuilder.where).toHaveBeenCalledWith('email = :email AND ends_at > :endsAt', {
email: 'test@test.com',
endsAt: 123,
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([offlineSubscription])
})
it('should update cancelled by subscription id', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().updateCancelled(1, true, 1000)
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
updatedAt: expect.any(Number),
cancelled: true,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 1,
})
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should update ends at by subscription id', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().updateEndsAt(1, 1000, 1000)
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
updatedAt: expect.any(Number),
endsAt: 1000,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 1,
})
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should find one offline user subscription by user subscription id', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getOne = jest.fn().mockReturnValue(offlineSubscription)
const result = await createRepository().findOneBySubscriptionId(123)
expect(selectQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 123,
})
expect(selectQueryBuilder.getOne).toHaveBeenCalled()
expect(result).toEqual(offlineSubscription)
})
})
@@ -1,78 +0,0 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
import { RevokedSession } from '../../Domain/Session/RevokedSession'
import { MySQLRevokedSessionRepository } from './MySQLRevokedSessionRepository'
describe('MySQLRevokedSessionRepository', () => {
let ormRepository: Repository<RevokedSession>
let queryBuilder: SelectQueryBuilder<RevokedSession>
let updateQueryBuilder: UpdateQueryBuilder<RevokedSession>
let session: RevokedSession
const createRepository = () => new MySQLRevokedSessionRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<RevokedSession>>
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<RevokedSession>>
session = {} as jest.Mocked<RevokedSession>
ormRepository = {} as jest.Mocked<Repository<RevokedSession>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
ormRepository.remove = jest.fn()
})
it('should save', async () => {
await createRepository().save(session)
expect(ormRepository.save).toHaveBeenCalledWith(session)
})
it('should remove', async () => {
await createRepository().remove(session)
expect(ormRepository.remove).toHaveBeenCalledWith(session)
})
it('should clear user agent data on all user sessions', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().clearUserAgentByUserUuid('1-2-3')
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
userAgent: null,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should find one session by id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(session)
const result = await createRepository().findOneByUuid('123')
expect(queryBuilder.where).toHaveBeenCalledWith('revoked_session.uuid = :uuid', { uuid: '123' })
expect(result).toEqual(session)
})
it('should find all revoked sessions by user id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([session])
const result = await createRepository().findAllByUserUuid('123')
expect(queryBuilder.where).toHaveBeenCalledWith('revoked_session.user_uuid = :user_uuid', { user_uuid: '123' })
expect(result).toEqual([session])
})
})
@@ -1,49 +0,0 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { Role } from '../../Domain/Role/Role'
import { MySQLRoleRepository } from './MySQLRoleRepository'
describe('MySQLRoleRepository', () => {
let ormRepository: Repository<Role>
let queryBuilder: SelectQueryBuilder<Role>
let role: Role
const createRepository = () => new MySQLRoleRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Role>>
queryBuilder.cache = jest.fn().mockReturnThis()
role = {} as jest.Mocked<Role>
ormRepository = {} as jest.Mocked<Repository<Role>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
})
it('should find latest version of a role by name', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.take = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([role])
const result = await createRepository().findOneByName('test')
expect(queryBuilder.where).toHaveBeenCalledWith('role.name = :name', { name: 'test' })
expect(queryBuilder.take).toHaveBeenCalledWith(1)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('version', 'DESC')
expect(result).toEqual(role)
})
it('should return null if not found the latest version of a role by name', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.take = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([])
const result = await createRepository().findOneByName('test')
expect(result).toBeNull()
})
})
@@ -1,174 +0,0 @@
import 'reflect-metadata'
import * as dayjs from 'dayjs'
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
import { Session } from '../../Domain/Session/Session'
import { MySQLSessionRepository } from './MySQLSessionRepository'
describe('MySQLSessionRepository', () => {
let ormRepository: Repository<Session>
let queryBuilder: SelectQueryBuilder<Session>
let updateQueryBuilder: UpdateQueryBuilder<Session>
let session: Session
const createRepository = () => new MySQLSessionRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Session>>
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<Session>>
session = {} as jest.Mocked<Session>
ormRepository = {} as jest.Mocked<Repository<Session>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
ormRepository.remove = jest.fn()
})
it('should save', async () => {
await createRepository().save(session)
expect(ormRepository.save).toHaveBeenCalledWith(session)
})
it('should remove', async () => {
await createRepository().remove(session)
expect(ormRepository.remove).toHaveBeenCalledWith(session)
})
it('should clear user agent data on all user sessions', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().clearUserAgentByUserUuid('1-2-3')
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
userAgent: null,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :userUuid', { userUuid: '1-2-3' })
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should update hashed tokens on a session', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().updateHashedTokens('123', '234', '345')
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
hashedAccessToken: '234',
hashedRefreshToken: '345',
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: '123' })
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should update token expiration dates on a session', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().updatedTokenExpirationDates(
'123',
dayjs.utc('2020-11-26 13:34').toDate(),
dayjs.utc('2020-11-26 14:34').toDate(),
)
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
accessExpiration: dayjs.utc('2020-11-26T13:34:00.000Z').toDate(),
refreshExpiration: dayjs.utc('2020-11-26T14:34:00.000Z').toDate(),
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: '123' })
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should find active sessions by user id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([session])
const result = await createRepository().findAllByRefreshExpirationAndUserUuid('123')
expect(queryBuilder.where).toHaveBeenCalledWith(
'session.refresh_expiration > :refresh_expiration AND session.user_uuid = :user_uuid',
{ refresh_expiration: expect.any(Date), user_uuid: '123' },
)
expect(result).toEqual([session])
})
it('should find all sessions by user id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([session])
const result = await createRepository().findAllByUserUuid('123')
expect(queryBuilder.where).toHaveBeenCalledWith('session.user_uuid = :user_uuid', { user_uuid: '123' })
expect(result).toEqual([session])
})
it('should find one session by id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(session)
const result = await createRepository().findOneByUuid('123')
expect(queryBuilder.where).toHaveBeenCalledWith('session.uuid = :uuid', { uuid: '123' })
expect(result).toEqual(session)
})
it('should find one session by id and user id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(session)
const result = await createRepository().findOneByUuidAndUserUuid('123', '234')
expect(queryBuilder.where).toHaveBeenCalledWith('session.uuid = :uuid AND session.user_uuid = :user_uuid', {
uuid: '123',
user_uuid: '234',
})
expect(result).toEqual(session)
})
it('should delete all session for a user except the current one', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.delete = jest.fn().mockReturnThis()
queryBuilder.execute = jest.fn()
await createRepository().deleteAllByUserUuid('123', '234')
expect(queryBuilder.delete).toHaveBeenCalled()
expect(queryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid AND uuid != :current_session_uuid', {
user_uuid: '123',
current_session_uuid: '234',
})
expect(queryBuilder.execute).toHaveBeenCalled()
})
it('should delete one session by id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.delete = jest.fn().mockReturnThis()
queryBuilder.execute = jest.fn()
await createRepository().deleteOneByUuid('123')
expect(queryBuilder.delete).toHaveBeenCalled()
expect(queryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', { uuid: '123' })
expect(queryBuilder.execute).toHaveBeenCalled()
})
})
@@ -1,140 +0,0 @@
import 'reflect-metadata'
import { ReadStream } from 'fs'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { Setting } from '../../Domain/Setting/Setting'
import { MySQLSettingRepository } from './MySQLSettingRepository'
import { EmailBackupFrequency, SettingName } from '@standardnotes/settings'
describe('MySQLSettingRepository', () => {
let ormRepository: Repository<Setting>
let queryBuilder: SelectQueryBuilder<Setting>
let setting: Setting
const createRepository = () => new MySQLSettingRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<Setting>>
setting = {} as jest.Mocked<Setting>
ormRepository = {} as jest.Mocked<Repository<Setting>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
})
it('should save', async () => {
await createRepository().save(setting)
expect(ormRepository.save).toHaveBeenCalledWith(setting)
})
it('should stream all settings by name and value', async () => {
const stream = {} as jest.Mocked<ReadStream>
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.stream = jest.fn().mockReturnValue(stream)
const result = await createRepository().streamAllByNameAndValue(
SettingName.EmailBackupFrequency,
EmailBackupFrequency.Daily,
)
expect(result).toEqual(stream)
})
it('should find one setting by uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
const result = await createRepository().findOneByUuid('1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith('setting.uuid = :uuid', { uuid: '1-2-3' })
expect(result).toEqual(setting)
})
it('should find one setting by name and user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
const result = await createRepository().findOneByNameAndUserUuid('test', '1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith('setting.name = :name AND setting.user_uuid = :user_uuid', {
name: 'test',
user_uuid: '1-2-3',
})
expect(result).toEqual(setting)
})
it('should find one setting by name and uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
const result = await createRepository().findOneByUuidAndNames('1-2-3', ['test' as SettingName])
expect(queryBuilder.where).toHaveBeenCalledWith('setting.uuid = :uuid AND setting.name IN (:...names)', {
names: ['test'],
uuid: '1-2-3',
})
expect(result).toEqual(setting)
})
it('should find last setting by name and user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.limit = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([setting])
const result = await createRepository().findLastByNameAndUserUuid('test', '1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith('setting.name = :name AND setting.user_uuid = :user_uuid', {
name: 'test',
user_uuid: '1-2-3',
})
expect(queryBuilder.orderBy).toHaveBeenCalledWith('updated_at', 'DESC')
expect(queryBuilder.limit).toHaveBeenCalledWith(1)
expect(result).toEqual(setting)
})
it('should return null if not found last setting by name and user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.limit = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([])
const result = await createRepository().findLastByNameAndUserUuid('test', '1-2-3')
expect(result).toBeNull()
})
it('should find all by user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
const settings = [setting]
queryBuilder.getMany = jest.fn().mockReturnValue(settings)
const userUuid = '123'
const result = await createRepository().findAllByUserUuid(userUuid)
expect(queryBuilder.where).toHaveBeenCalledWith('setting.user_uuid = :user_uuid', { user_uuid: userUuid })
expect(result).toEqual(settings)
})
it('should delete setting if it does exist', async () => {
const queryBuilder = {
delete: () => queryBuilder,
where: () => queryBuilder,
execute: () => undefined,
}
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
const result = await createRepository().deleteByUserUuid({
userUuid: 'userUuid',
settingName: 'settingName',
})
expect(result).toEqual(undefined)
})
})
@@ -1,100 +0,0 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { MySQLSharedSubscriptionInvitationRepository } from './MySQLSharedSubscriptionInvitationRepository'
import { SharedSubscriptionInvitation } from '../../Domain/SharedSubscription/SharedSubscriptionInvitation'
import { InvitationStatus } from '../../Domain/SharedSubscription/InvitationStatus'
describe('MySQLSharedSubscriptionInvitationRepository', () => {
let ormRepository: Repository<SharedSubscriptionInvitation>
let queryBuilder: SelectQueryBuilder<SharedSubscriptionInvitation>
let invitation: SharedSubscriptionInvitation
const createRepository = () => new MySQLSharedSubscriptionInvitationRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<SharedSubscriptionInvitation>>
invitation = {} as jest.Mocked<SharedSubscriptionInvitation>
ormRepository = {} as jest.Mocked<Repository<SharedSubscriptionInvitation>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
})
it('should save', async () => {
await createRepository().save(invitation)
expect(ormRepository.save).toHaveBeenCalledWith(invitation)
})
it('should get invitations by inviter email', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([])
const result = await createRepository().findByInviterEmail('test@test.te')
expect(queryBuilder.where).toHaveBeenCalledWith('invitation.inviter_identifier = :inviterEmail', {
inviterEmail: 'test@test.te',
})
expect(result).toEqual([])
})
it('should count invitations by inviter email and statuses', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getCount = jest.fn().mockReturnValue(3)
const result = await createRepository().countByInviterEmailAndStatus('test@test.te', [InvitationStatus.Sent])
expect(queryBuilder.where).toHaveBeenCalledWith(
'invitation.inviter_identifier = :inviterEmail AND invitation.status IN (:...statuses)',
{ inviterEmail: 'test@test.te', statuses: ['sent'] },
)
expect(result).toEqual(3)
})
it('should find one invitation by name and uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
const result = await createRepository().findOneByUuidAndStatus('1-2-3', InvitationStatus.Sent)
expect(queryBuilder.where).toHaveBeenCalledWith('invitation.uuid = :uuid AND invitation.status = :status', {
uuid: '1-2-3',
status: 'sent',
})
expect(result).toEqual(invitation)
})
it('should find one invitation by invitee and inviter email', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
const result = await createRepository().findOneByInviteeAndInviterEmail('invitee@test.te', 'inviter@test.te')
expect(queryBuilder.where).toHaveBeenCalledWith(
'invitation.inviter_identifier = :inviterEmail AND invitation.invitee_identifier = :inviteeEmail',
{
inviterEmail: 'inviter@test.te',
inviteeEmail: 'invitee@test.te',
},
)
expect(result).toEqual(invitation)
})
it('should find one invitation by uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(invitation)
const result = await createRepository().findOneByUuid('1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith('invitation.uuid = :uuid', { uuid: '1-2-3' })
expect(result).toEqual(invitation)
})
})
@@ -1,68 +0,0 @@
import 'reflect-metadata'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { SubscriptionSetting } from '../../Domain/Setting/SubscriptionSetting'
import { MySQLSubscriptionSettingRepository } from './MySQLSubscriptionSettingRepository'
describe('MySQLSubscriptionSettingRepository', () => {
let ormRepository: Repository<SubscriptionSetting>
let queryBuilder: SelectQueryBuilder<SubscriptionSetting>
let setting: SubscriptionSetting
const createRepository = () => new MySQLSubscriptionSettingRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<SubscriptionSetting>>
setting = {} as jest.Mocked<SubscriptionSetting>
ormRepository = {} as jest.Mocked<Repository<SubscriptionSetting>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
})
it('should save', async () => {
await createRepository().save(setting)
expect(ormRepository.save).toHaveBeenCalledWith(setting)
})
it('should find one setting by uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(setting)
const result = await createRepository().findOneByUuid('1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith('setting.uuid = :uuid', { uuid: '1-2-3' })
expect(result).toEqual(setting)
})
it('should find last setting by name and user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.limit = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([setting])
const result = await createRepository().findLastByNameAndUserSubscriptionUuid('test', '1-2-3')
expect(queryBuilder.where).toHaveBeenCalledWith(
'setting.name = :name AND setting.user_subscription_uuid = :userSubscriptionUuid',
{ name: 'test', userSubscriptionUuid: '1-2-3' },
)
expect(queryBuilder.orderBy).toHaveBeenCalledWith('updated_at', 'DESC')
expect(queryBuilder.limit).toHaveBeenCalledWith(1)
expect(result).toEqual(setting)
})
it('should return null if not found last setting by name and user uuid', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.orderBy = jest.fn().mockReturnThis()
queryBuilder.limit = jest.fn().mockReturnThis()
queryBuilder.getMany = jest.fn().mockReturnValue([])
const result = await createRepository().findLastByNameAndUserSubscriptionUuid('test', '1-2-3')
expect(result).toBeNull()
})
})
@@ -1,69 +0,0 @@
import 'reflect-metadata'
import { ReadStream } from 'fs'
import { Repository, SelectQueryBuilder } from 'typeorm'
import { User } from '../../Domain/User/User'
import { MySQLUserRepository } from './MySQLUserRepository'
describe('MySQLUserRepository', () => {
let ormRepository: Repository<User>
let queryBuilder: SelectQueryBuilder<User>
let user: User
const createRepository = () => new MySQLUserRepository(ormRepository)
beforeEach(() => {
queryBuilder = {} as jest.Mocked<SelectQueryBuilder<User>>
queryBuilder.cache = jest.fn().mockReturnThis()
user = {} as jest.Mocked<User>
ormRepository = {} as jest.Mocked<Repository<User>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => queryBuilder)
ormRepository.save = jest.fn()
ormRepository.remove = jest.fn()
})
it('should save', async () => {
await createRepository().save(user)
expect(ormRepository.save).toHaveBeenCalledWith(user)
})
it('should remove', async () => {
await createRepository().remove(user)
expect(ormRepository.remove).toHaveBeenCalledWith(user)
})
it('should find one user by id', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(user)
const result = await createRepository().findOneByUuid('123')
expect(queryBuilder.where).toHaveBeenCalledWith('user.uuid = :uuid', { uuid: '123' })
expect(result).toEqual(user)
})
it('should stream all users', async () => {
const stream = {} as jest.Mocked<ReadStream>
queryBuilder.stream = jest.fn().mockReturnValue(stream)
const result = await createRepository().streamAll()
expect(result).toEqual(stream)
})
it('should find one user by email', async () => {
queryBuilder.where = jest.fn().mockReturnThis()
queryBuilder.getOne = jest.fn().mockReturnValue(user)
const result = await createRepository().findOneByEmail('test@test.te')
expect(queryBuilder.where).toHaveBeenCalledWith('user.email = :email', { email: 'test@test.te' })
expect(result).toEqual(user)
})
})
@@ -22,7 +22,10 @@ export class MySQLUserRepository implements UserRepositoryInterface {
}
async streamAll(): Promise<ReadStream> {
return this.ormRepository.createQueryBuilder('user').stream()
return this.ormRepository
.createQueryBuilder('user')
.where('created_at < :createdAt', { createdAt: new Date().toISOString() })
.stream()
}
async streamTeam(memberEmail?: string): Promise<ReadStream> {
@@ -1,287 +0,0 @@
import 'reflect-metadata'
import { SubscriptionName } from '@standardnotes/common'
import { Repository, SelectQueryBuilder, UpdateQueryBuilder } from 'typeorm'
import { UserSubscription } from '../../Domain/Subscription/UserSubscription'
import { MySQLUserSubscriptionRepository } from './MySQLUserSubscriptionRepository'
import { UserSubscriptionType } from '../../Domain/Subscription/UserSubscriptionType'
import { TimerInterface } from '@standardnotes/time'
describe('MySQLUserSubscriptionRepository', () => {
let ormRepository: Repository<UserSubscription>
let selectQueryBuilder: SelectQueryBuilder<UserSubscription>
let updateQueryBuilder: UpdateQueryBuilder<UserSubscription>
let subscription: UserSubscription
let timer: TimerInterface
const createRepository = () => new MySQLUserSubscriptionRepository(ormRepository, timer)
beforeEach(() => {
selectQueryBuilder = {} as jest.Mocked<SelectQueryBuilder<UserSubscription>>
updateQueryBuilder = {} as jest.Mocked<UpdateQueryBuilder<UserSubscription>>
subscription = {
planName: SubscriptionName.ProPlan,
cancelled: false,
} as jest.Mocked<UserSubscription>
ormRepository = {} as jest.Mocked<Repository<UserSubscription>>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
ormRepository.save = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(123)
})
it('should save', async () => {
await createRepository().save(subscription)
expect(ormRepository.save).toHaveBeenCalledWith(subscription)
})
it('should find all subscriptions by user uuid', async () => {
const canceledSubscription = {
planName: SubscriptionName.ProPlan,
cancelled: true,
} as jest.Mocked<UserSubscription>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, subscription])
const result = await createRepository().findByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([canceledSubscription, subscription])
})
it('should count all active subscriptions', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.groupBy = jest.fn().mockReturnThis()
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
const result = await createRepository().countActiveSubscriptions()
expect(selectQueryBuilder.where).toHaveBeenCalledWith('ends_at > :timestamp', {
timestamp: 123,
})
expect(selectQueryBuilder.groupBy).toHaveBeenCalledWith('user_uuid')
expect(selectQueryBuilder.getCount).toHaveBeenCalled()
expect(result).toEqual(2)
})
it('should find one longest lasting uncanceled subscription by user uuid if there are canceled ones', async () => {
const canceledSubscription = {
planName: SubscriptionName.ProPlan,
cancelled: true,
} as jest.Mocked<UserSubscription>
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([canceledSubscription, subscription])
const result = await createRepository().findOneByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual(subscription)
})
it('should find one, longest lasting subscription by user uuid if there are no canceled ones', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findOneByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual(subscription)
})
it('should count by user uuid', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getCount = jest.fn().mockReturnValue(2)
const result = await createRepository().countByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.getCount).toHaveBeenCalled()
expect(result).toEqual(2)
})
it('should find one, longest lasting subscription by user uuid if there are no ucanceled ones', async () => {
subscription.cancelled = true
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findOneByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual(subscription)
})
it('should find none if there are no subscriptions for the user', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([])
const result = await createRepository().findOneByUserUuid('123')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('user_uuid = :user_uuid', {
user_uuid: '123',
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('ends_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toBeNull()
})
it('should update ends at by subscription id', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().updateEndsAt(1, 1000, 1000)
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
updatedAt: 1000,
renewedAt: 1000,
endsAt: 1000,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 1,
})
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should update cancelled by subscription id', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => updateQueryBuilder)
updateQueryBuilder.update = jest.fn().mockReturnThis()
updateQueryBuilder.set = jest.fn().mockReturnThis()
updateQueryBuilder.where = jest.fn().mockReturnThis()
updateQueryBuilder.execute = jest.fn()
await createRepository().updateCancelled(1, true, 1000)
expect(updateQueryBuilder.update).toHaveBeenCalled()
expect(updateQueryBuilder.set).toHaveBeenCalledWith({
updatedAt: expect.any(Number),
cancelled: true,
})
expect(updateQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 1,
})
expect(updateQueryBuilder.execute).toHaveBeenCalled()
})
it('should find subscriptions by id', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findBySubscriptionId(123)
expect(selectQueryBuilder.where).toHaveBeenCalledWith('subscription_id = :subscriptionId', {
subscriptionId: 123,
})
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([subscription])
})
it('should find subscriptions by id and type', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.orderBy = jest.fn().mockReturnThis()
selectQueryBuilder.getMany = jest.fn().mockReturnValue([subscription])
const result = await createRepository().findBySubscriptionIdAndType(123, UserSubscriptionType.Regular)
expect(selectQueryBuilder.where).toHaveBeenCalledWith(
'subscription_id = :subscriptionId AND subscription_type = :type',
{
subscriptionId: 123,
type: 'regular',
},
)
expect(selectQueryBuilder.orderBy).toHaveBeenCalledWith('created_at', 'DESC')
expect(selectQueryBuilder.getMany).toHaveBeenCalled()
expect(result).toEqual([subscription])
})
it('should find one subscription by id and user uuid', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getOne = jest.fn().mockReturnValue(subscription)
const result = await createRepository().findOneByUserUuidAndSubscriptionId('1-2-3', 5)
expect(selectQueryBuilder.where).toHaveBeenCalledWith(
'user_uuid = :userUuid AND subscription_id = :subscriptionId',
{
subscriptionId: 5,
userUuid: '1-2-3',
},
)
expect(selectQueryBuilder.getOne).toHaveBeenCalled()
expect(result).toEqual(subscription)
})
it('should find one subscription by uuid', async () => {
ormRepository.createQueryBuilder = jest.fn().mockImplementation(() => selectQueryBuilder)
selectQueryBuilder.where = jest.fn().mockReturnThis()
selectQueryBuilder.getOne = jest.fn().mockReturnValue(subscription)
const result = await createRepository().findOneByUuid('1-2-3')
expect(selectQueryBuilder.where).toHaveBeenCalledWith('uuid = :uuid', {
uuid: '1-2-3',
})
expect(selectQueryBuilder.getOne).toHaveBeenCalled()
expect(result).toEqual(subscription)
})
})
@@ -1,92 +0,0 @@
import 'reflect-metadata'
import * as IORedis from 'ioredis'
import { LockRepository } from './LockRepository'
describe('LockRepository', () => {
let redisClient: IORedis.Redis
const maxLoginAttempts = 3
const failedLoginLockout = 120
const createRepository = () => new LockRepository(redisClient, maxLoginAttempts, failedLoginLockout)
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.expire = jest.fn()
redisClient.del = jest.fn()
redisClient.get = jest.fn()
redisClient.setex = jest.fn()
})
it('should lock a successfully used OTP for the lockout period', async () => {
await createRepository().lockSuccessfullOTP('test@test.te', '123456')
expect(redisClient.setex).toHaveBeenCalledWith('otp-lock:test@test.te', 60, '123456')
})
it('should indicate if an OTP was already used in the lockout period', async () => {
redisClient.get = jest.fn().mockReturnValue('123456')
expect(await createRepository().isOTPLocked('test@test.te', '123456')).toEqual(true)
})
it('should indicate if an OTP was not already used in the lockout period', async () => {
redisClient.get = jest.fn().mockReturnValue('654321')
expect(await createRepository().isOTPLocked('test@test.te', '123456')).toEqual(false)
})
it('should lock a user for the lockout period', async () => {
await createRepository().lockUser('123')
expect(redisClient.expire).toHaveBeenCalledWith('lock:123', 120)
})
it('should tell a user is locked if his counter is above threshold', async () => {
redisClient.get = jest.fn().mockReturnValue('4')
expect(await createRepository().isUserLocked('123')).toBeTruthy()
})
it('should tell a user is locked if his counter is at the threshold', async () => {
redisClient.get = jest.fn().mockReturnValue('3')
expect(await createRepository().isUserLocked('123')).toBeTruthy()
})
it('should tell a user is not locked if his counter is below threshold', async () => {
redisClient.get = jest.fn().mockReturnValue('2')
expect(await createRepository().isUserLocked('123')).toBeFalsy()
})
it('should tell a user is not locked if he has no counter', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
expect(await createRepository().isUserLocked('123')).toBeFalsy()
})
it('should tell what the user lock counter is', async () => {
redisClient.get = jest.fn().mockReturnValue('3')
expect(await createRepository().getLockCounter('123')).toStrictEqual(3)
})
it('should tell that the user lock counter is 0 when there is no counter', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
expect(await createRepository().getLockCounter('123')).toStrictEqual(0)
})
it('should reset a lock counter', async () => {
await createRepository().resetLockCounter('123')
expect(redisClient.del).toHaveBeenCalledWith('lock:123')
})
it('should update a lock counter', async () => {
await createRepository().updateLockCounter('123', 3)
expect(redisClient.setex).toHaveBeenCalledWith('lock:123', 120, 3)
})
})
@@ -1,152 +0,0 @@
import 'reflect-metadata'
import * as IORedis from 'ioredis'
import { RedisEphemeralSessionRepository } from './RedisEphemeralSessionRepository'
import { EphemeralSession } from '../../Domain/Session/EphemeralSession'
describe('RedisEphemeralSessionRepository', () => {
let redisClient: IORedis.Redis
let pipeline: IORedis.Pipeline
const createRepository = () => new RedisEphemeralSessionRepository(redisClient, 3600)
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.get = jest.fn()
redisClient.smembers = jest.fn()
pipeline = {} as jest.Mocked<IORedis.Pipeline>
pipeline.setex = jest.fn()
pipeline.expire = jest.fn()
pipeline.sadd = jest.fn()
pipeline.del = jest.fn()
pipeline.srem = jest.fn()
pipeline.exec = jest.fn()
redisClient.pipeline = jest.fn().mockReturnValue(pipeline)
})
it('should delete an ephemeral', async () => {
await createRepository().deleteOne('1-2-3', '2-3-4')
expect(pipeline.del).toHaveBeenCalledWith('session:1-2-3:2-3-4')
expect(pipeline.del).toHaveBeenCalledWith('session:1-2-3')
expect(pipeline.srem).toHaveBeenCalledWith('user-sessions:2-3-4', '1-2-3')
})
it('should save an ephemeral session', async () => {
const ephemeralSession = new EphemeralSession()
ephemeralSession.uuid = '1-2-3'
ephemeralSession.userUuid = '2-3-4'
ephemeralSession.userAgent = 'Mozilla Firefox'
ephemeralSession.createdAt = new Date(1)
ephemeralSession.updatedAt = new Date(2)
await createRepository().save(ephemeralSession)
expect(pipeline.setex).toHaveBeenCalledWith(
'session:1-2-3:2-3-4',
3600,
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
)
expect(pipeline.sadd).toHaveBeenCalledWith('user-sessions:2-3-4', '1-2-3')
expect(pipeline.expire).toHaveBeenCalledWith('user-sessions:2-3-4', 3600)
})
it('should find all ephemeral sessions by user uuid', async () => {
redisClient.smembers = jest.fn().mockReturnValue(['1-2-3', '2-3-4', '3-4-5'])
redisClient.get = jest
.fn()
.mockReturnValueOnce(
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
)
.mockReturnValueOnce(
'{"uuid":"2-3-4","userUuid":"2-3-4","userAgent":"Google Chrome","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
)
.mockReturnValueOnce(null)
const ephemeralSessions = await createRepository().findAllByUserUuid('2-3-4')
expect(ephemeralSessions.length).toEqual(2)
expect(ephemeralSessions[1].userAgent).toEqual('Google Chrome')
})
it('should find an ephemeral session by uuid', async () => {
redisClient.get = jest
.fn()
.mockReturnValue(
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
)
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuid('1-2-3')
expect(ephemeralSession).not.toBeUndefined()
expect(ephemeralSession.userAgent).toEqual('Mozilla Firefox')
})
it('should find an ephemeral session by uuid and user uuid', async () => {
redisClient.get = jest
.fn()
.mockReturnValue(
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
)
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuidAndUserUuid('1-2-3', '2-3-4')
expect(ephemeralSession).not.toBeUndefined()
expect(ephemeralSession.userAgent).toEqual('Mozilla Firefox')
})
it('should return undefined if session is not found', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuid('1-2-3')
expect(ephemeralSession).toBeNull()
})
it('should return undefined if ephemeral session is not found', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
const ephemeralSession = <EphemeralSession>await createRepository().findOneByUuidAndUserUuid('1-2-3', '2-3-4')
expect(ephemeralSession).toBeNull()
})
it('should update tokens and expirations dates', async () => {
redisClient.get = jest
.fn()
.mockReturnValue(
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z"}',
)
await createRepository().updateTokensAndExpirationDates(
'1-2-3',
'dummy_access_token',
'dummy_refresh_token',
new Date(3),
new Date(4),
)
expect(pipeline.setex).toHaveBeenCalledWith(
'session:1-2-3:2-3-4',
3600,
'{"uuid":"1-2-3","userUuid":"2-3-4","userAgent":"Mozilla Firefox","createdAt":"1970-01-01T00:00:00.001Z","updatedAt":"1970-01-01T00:00:00.002Z","hashedAccessToken":"dummy_access_token","hashedRefreshToken":"dummy_refresh_token","accessExpiration":"1970-01-01T00:00:00.003Z","refreshExpiration":"1970-01-01T00:00:00.004Z"}',
)
})
it('should not update tokens and expirations dates if the ephemeral session does not exist', async () => {
await createRepository().updateTokensAndExpirationDates(
'1-2-3',
'dummy_access_token',
'dummy_refresh_token',
new Date(3),
new Date(4),
)
expect(pipeline.setex).not.toHaveBeenCalled()
})
})
@@ -1,59 +0,0 @@
import 'reflect-metadata'
import * as IORedis from 'ioredis'
import { TimerInterface } from '@standardnotes/time'
import { RedisOfflineSubscriptionTokenRepository } from './RedisOfflineSubscriptionTokenRepository'
import { OfflineSubscriptionToken } from '../../Domain/Auth/OfflineSubscriptionToken'
import { Logger } from 'winston'
describe('RedisOfflineSubscriptionTokenRepository', () => {
let redisClient: IORedis.Redis
let timer: TimerInterface
let logger: Logger
const createRepository = () => new RedisOfflineSubscriptionTokenRepository(redisClient, timer, logger)
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.set = jest.fn()
redisClient.get = jest.fn()
redisClient.expireat = jest.fn()
timer = {} as jest.Mocked<TimerInterface>
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
})
it('should get a user uuid in exchange for an dashboard token', async () => {
redisClient.get = jest.fn().mockReturnValue('test@test.com')
expect(await createRepository().getUserEmailByToken('random-string')).toEqual('test@test.com')
expect(redisClient.get).toHaveBeenCalledWith('offline-subscription-token:random-string')
})
it('should return undefined if a user uuid is not exchanged for an dashboard token', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
expect(await createRepository().getUserEmailByToken('random-string')).toBeUndefined()
expect(redisClient.get).toHaveBeenCalledWith('offline-subscription-token:random-string')
})
it('should save an dashboard token', async () => {
const offlineSubscriptionToken: OfflineSubscriptionToken = {
userEmail: 'test@test.com',
token: 'random-string',
expiresAt: 123,
}
await createRepository().save(offlineSubscriptionToken)
expect(redisClient.set).toHaveBeenCalledWith('offline-subscription-token:random-string', 'test@test.com')
expect(redisClient.expireat).toHaveBeenCalledWith('offline-subscription-token:random-string', 1)
})
})
@@ -1,34 +0,0 @@
import 'reflect-metadata'
import * as IORedis from 'ioredis'
import { Logger } from 'winston'
import { RedisPKCERepository } from './RedisPKCERepository'
describe('RedisPKCERepository', () => {
let redisClient: IORedis.Redis
let logger: Logger
const createRepository = () => new RedisPKCERepository(redisClient, logger)
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.setex = jest.fn()
redisClient.del = jest.fn().mockReturnValue(1)
logger = {} as jest.Mocked<Logger>
logger.debug = jest.fn()
})
it('should store a code challenge', async () => {
await createRepository().storeCodeChallenge('test')
expect(redisClient.setex).toHaveBeenCalledWith('pkce:test', 3600, 'test')
})
it('should remove a code challenge and notify of success', async () => {
expect(await createRepository().removeCodeChallenge('test')).toBeTruthy()
expect(redisClient.del).toHaveBeenCalledWith('pkce:test')
})
})
@@ -1,70 +0,0 @@
import 'reflect-metadata'
import * as IORedis from 'ioredis'
import { TimerInterface } from '@standardnotes/time'
import { RedisSubscriptionTokenRepository } from './RedisSubscriptionTokenRepository'
import { SubscriptionToken } from '../../Domain/Subscription/SubscriptionToken'
describe('RedisSubscriptionTokenRepository', () => {
let redisClient: IORedis.Redis
let timer: TimerInterface
const createRepository = () => new RedisSubscriptionTokenRepository(redisClient, timer)
beforeEach(() => {
redisClient = {} as jest.Mocked<IORedis.Redis>
redisClient.set = jest.fn().mockReturnValue('OK')
redisClient.get = jest.fn()
redisClient.expireat = jest.fn().mockReturnValue(1)
timer = {} as jest.Mocked<TimerInterface>
timer.convertMicrosecondsToSeconds = jest.fn().mockReturnValue(1)
})
it('should get a user uuid in exchange for an subscription token', async () => {
redisClient.get = jest.fn().mockReturnValue('1-2-3')
expect(await createRepository().getUserUuidByToken('random-string')).toEqual('1-2-3')
expect(redisClient.get).toHaveBeenCalledWith('subscription-token:random-string')
})
it('should return undefined if a user uuid is not exchanged for an subscription token', async () => {
redisClient.get = jest.fn().mockReturnValue(null)
expect(await createRepository().getUserUuidByToken('random-string')).toBeUndefined()
expect(redisClient.get).toHaveBeenCalledWith('subscription-token:random-string')
})
it('should save an subscription token', async () => {
const subscriptionToken: SubscriptionToken = {
userUuid: '1-2-3',
token: 'random-string',
expiresAt: 123,
}
expect(await createRepository().save(subscriptionToken)).toBeTruthy()
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
})
it('should indicate subscription token was not saved', async () => {
redisClient.set = jest.fn().mockReturnValue(null)
const subscriptionToken: SubscriptionToken = {
userUuid: '1-2-3',
token: 'random-string',
expiresAt: 123,
}
expect(await createRepository().save(subscriptionToken)).toBeFalsy()
expect(redisClient.set).toHaveBeenCalledWith('subscription-token:random-string', '1-2-3')
expect(redisClient.expireat).toHaveBeenCalledWith('subscription-token:random-string', 1)
})
})
@@ -1,53 +0,0 @@
import 'reflect-metadata'
import {
DomainEventPublisherInterface,
UserRolesChangedEvent,
WebSocketMessageRequestedEvent,
} from '@standardnotes/domain-events'
import { RoleName } from '@standardnotes/common'
import { User } from '../../Domain/User/User'
import { WebSocketsClientService } from './WebSocketsClientService'
import { DomainEventFactoryInterface } from '../../Domain/Event/DomainEventFactoryInterface'
describe('WebSocketsClientService', () => {
let user: User
let event: UserRolesChangedEvent
let domainEventFactory: DomainEventFactoryInterface
let domainEventPublisher: DomainEventPublisherInterface
const createService = () => new WebSocketsClientService(domainEventFactory, domainEventPublisher)
beforeEach(() => {
user = {
uuid: '123',
email: 'test@test.com',
roles: Promise.resolve([
{
name: RoleName.ProUser,
},
]),
} as jest.Mocked<User>
event = {} as jest.Mocked<UserRolesChangedEvent>
domainEventFactory = {} as jest.Mocked<DomainEventFactoryInterface>
domainEventFactory.createUserRolesChangedEvent = jest.fn().mockReturnValue(event)
domainEventFactory.createWebSocketMessageRequestedEvent = jest
.fn()
.mockReturnValue({} as jest.Mocked<WebSocketMessageRequestedEvent>)
domainEventPublisher = {} as jest.Mocked<DomainEventPublisherInterface>
domainEventPublisher.publish = jest.fn()
})
it('should request a message about a user role changed', async () => {
await createService().sendUserRolesChangedEvent(user)
expect(domainEventFactory.createUserRolesChangedEvent).toHaveBeenCalledWith('123', 'test@test.com', [
RoleName.ProUser,
])
expect(domainEventPublisher.publish).toHaveBeenCalled()
})
})
@@ -1,47 +0,0 @@
import 'reflect-metadata'
import { Permission } from '../Domain/Permission/Permission'
import { PermissionProjector } from './PermissionProjector'
describe('PermissionProjector', () => {
let permission: Permission
const createProjector = () => new PermissionProjector()
beforeEach(() => {
permission = new Permission()
permission.uuid = '123'
permission.name = 'permission1'
permission.createdAt = new Date(1)
permission.updatedAt = new Date(2)
})
it('should create a simple projection', () => {
const projection = createProjector().projectSimple(permission)
expect(projection).toMatchObject({
uuid: '123',
name: 'permission1',
})
})
it('should throw error on custom projection', () => {
let error = null
try {
createProjector().projectCustom('test', permission)
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should throw error on not implemetned full projection', () => {
let error = null
try {
createProjector().projectFull(permission)
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
})
@@ -1,47 +0,0 @@
import 'reflect-metadata'
import { Role } from '../Domain/Role/Role'
import { RoleProjector } from './RoleProjector'
describe('RoleProjector', () => {
let role: Role
const createProjector = () => new RoleProjector()
beforeEach(() => {
role = new Role()
role.uuid = '123'
role.name = 'role1'
role.createdAt = new Date(1)
role.updatedAt = new Date(2)
})
it('should create a simple projection', () => {
const projection = createProjector().projectSimple(role)
expect(projection).toMatchObject({
uuid: '123',
name: 'role1',
})
})
it('should throw error on custom projection', () => {
let error = null
try {
createProjector().projectCustom('test', role)
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should throw error on not implemetned full projection', () => {
let error = null
try {
createProjector().projectFull(role)
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
})
@@ -1,109 +0,0 @@
import 'reflect-metadata'
import { SessionServiceInterface } from '../Domain/Session/SessionServiceInterface'
import { SessionProjector } from './SessionProjector'
import { Session } from '../Domain/Session/Session'
import { TimerInterface } from '@standardnotes/time'
describe('SessionProjector', () => {
let session: Session
let currentSession: Session
let sessionService: SessionServiceInterface
let timer: TimerInterface
const createProjector = () => new SessionProjector(sessionService, timer)
beforeEach(() => {
session = new Session()
session.uuid = '123'
session.hashedAccessToken = 'hashed access token'
session.userUuid = '234'
session.apiVersion = '004'
session.createdAt = new Date(1)
session.updatedAt = new Date(1)
session.accessExpiration = new Date(1)
session.refreshExpiration = new Date(1)
session.readonlyAccess = false
currentSession = new Session()
currentSession.uuid = '234'
sessionService = {} as jest.Mocked<SessionServiceInterface>
sessionService.getDeviceInfo = jest.fn().mockReturnValue('Some Device Info')
timer = {} as jest.Mocked<TimerInterface>
timer.convertDateToISOString = jest.fn().mockReturnValue('2020-11-26T13:34:00.000Z')
})
it('should create a simple projection of a session', () => {
const projection = createProjector().projectSimple(session)
expect(projection).toMatchObject({
uuid: '123',
api_version: '004',
created_at: '2020-11-26T13:34:00.000Z',
updated_at: '2020-11-26T13:34:00.000Z',
device_info: 'Some Device Info',
readonly_access: false,
access_expiration: '2020-11-26T13:34:00.000Z',
refresh_expiration: '2020-11-26T13:34:00.000Z',
})
})
it('should create a custom projection of a session', () => {
const projection = createProjector().projectCustom(
SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
session,
currentSession,
)
expect(projection).toMatchObject({
uuid: '123',
api_version: '004',
created_at: '2020-11-26T13:34:00.000Z',
updated_at: '2020-11-26T13:34:00.000Z',
device_info: 'Some Device Info',
current: false,
readonly_access: false,
})
})
it('should create a custom projection of a current session', () => {
currentSession.uuid = '123'
const projection = createProjector().projectCustom(
SessionProjector.CURRENT_SESSION_PROJECTION.toString(),
session,
currentSession,
)
expect(projection).toMatchObject({
uuid: '123',
api_version: '004',
created_at: '2020-11-26T13:34:00.000Z',
updated_at: '2020-11-26T13:34:00.000Z',
device_info: 'Some Device Info',
current: true,
readonly_access: false,
})
})
it('should throw error on unknown custom projection', () => {
let error = null
try {
createProjector().projectCustom('test', session, currentSession)
} catch (e) {
error = e
}
expect((error as Error).message).toEqual('Not supported projection type: test')
})
it('should throw error on not implemetned full projection', () => {
let error = null
try {
createProjector().projectFull(session)
} catch (e) {
error = e
}
expect((error as Error).message).toEqual('not implemented')
})
})
@@ -1,48 +0,0 @@
import 'reflect-metadata'
import { Setting } from '../Domain/Setting/Setting'
import { SettingProjector } from './SettingProjector'
describe('SettingProjector', () => {
let setting: Setting
const createProjector = () => new SettingProjector()
beforeEach(() => {
setting = {
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
serverEncryptionVersion: 1,
createdAt: 1,
updatedAt: 2,
sensitive: false,
} as jest.Mocked<Setting>
})
it('should create a simple projection of a setting', async () => {
const projection = await createProjector().projectSimple(setting)
expect(projection).toStrictEqual({
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
})
})
it('should create a simple projection of list of settings', async () => {
const projection = await createProjector().projectManySimple([setting])
expect(projection).toStrictEqual([
{
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
},
])
})
})
@@ -1,48 +0,0 @@
import 'reflect-metadata'
import { SubscriptionSetting } from '../Domain/Setting/SubscriptionSetting'
import { SubscriptionSettingProjector } from './SubscriptionSettingProjector'
describe('SubscriptionSettingProjector', () => {
let setting: SubscriptionSetting
const createProjector = () => new SubscriptionSettingProjector()
beforeEach(() => {
setting = {
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
serverEncryptionVersion: 1,
createdAt: 1,
updatedAt: 2,
sensitive: false,
} as jest.Mocked<SubscriptionSetting>
})
it('should create a simple projection of a setting', async () => {
const projection = await createProjector().projectSimple(setting)
expect(projection).toStrictEqual({
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
})
})
it('should create a simple projection of list of settings', async () => {
const projection = await createProjector().projectManySimple([setting])
expect(projection).toStrictEqual([
{
uuid: 'setting-uuid',
name: 'setting-name',
value: 'setting-value',
createdAt: 1,
updatedAt: 2,
sensitive: false,
},
])
})
})
@@ -1,45 +0,0 @@
import 'reflect-metadata'
import { UserProjector } from './UserProjector'
import { User } from '../Domain/User/User'
describe('UserProjector', () => {
let user: User
const createProjector = () => new UserProjector()
beforeEach(() => {
user = new User()
user.uuid = '123'
user.email = 'test@test.te'
user.encryptedPassword = '123qwe345'
})
it('should create a simple projection of a user', () => {
const projection = createProjector().projectSimple(user)
expect(projection).toMatchObject({
uuid: '123',
email: 'test@test.te',
})
})
it('should throw error on custom projection', () => {
let error = null
try {
createProjector().projectCustom('test', user)
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
it('should throw error on not implemetned full projection', () => {
let error = null
try {
createProjector().projectFull(user)
} catch (e) {
error = e
}
expect(error).not.toBeNull()
})
})
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [1.9.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.8.0...@standardnotes/domain-core@1.9.0) (2022-12-07)
### Features
* **domain-core:** rename email subscription rejection level to email level ([c87561f](https://github.com/standardnotes/server/commit/c87561fca782883b84f58b4f0b9f85ecc279ca50))
# [1.8.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.7.0...@standardnotes/domain-core@1.8.0) (2022-12-05)
### Features
* **domain-core:** add email subscription rejection levels ([02f3c85](https://github.com/standardnotes/server/commit/02f3c85796ade7cb69edbdda2367c0d91ac1bdf0))
# [1.7.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.6.0...@standardnotes/domain-core@1.7.0) (2022-12-05)
### Features
* **domain-core:** distinguish between username and email ([06fd404](https://github.com/standardnotes/server/commit/06fd404d44b44a53733f889aabd4da63f21e2f36))
# [1.6.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.5.2...@standardnotes/domain-core@1.6.0) (2022-12-02)
### Features
* **domain-core:** add subscription plan name value object ([800fe9e](https://github.com/standardnotes/server/commit/800fe9e4c80c33f2da8097b5a153f470a23b7984))
## [1.5.2](https://github.com/standardnotes/server/compare/@standardnotes/domain-core@1.5.1...@standardnotes/domain-core@1.5.2) (2022-12-02)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-core",
"version": "1.5.2",
"version": "1.9.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -9,7 +9,7 @@ describe('Email', () => {
})
it('should not create an invalid value object', () => {
const valueOrError = Email.create('')
const valueOrError = Email.create('foobar')
expect(valueOrError.isFailed()).toBeTruthy()
})
@@ -1,6 +1,7 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { EmailProps } from './EmailProps'
import { Validator } from '../Core/Validator'
export class Email extends ValueObject<EmailProps> {
get value(): string {
@@ -12,10 +13,11 @@ export class Email extends ValueObject<EmailProps> {
}
static create(email: string): Result<Email> {
if (!!email === false || email.length === 0) {
return Result.fail<Email>('Email cannot be empty')
} else {
return Result.ok<Email>(new Email({ value: email }))
const emailValidation = Validator.isValidEmail(email)
if (emailValidation.isFailed()) {
return Result.fail<Email>(emailValidation.getError())
}
return Result.ok<Email>(new Email({ value: email }))
}
}
@@ -2,7 +2,7 @@ import { RoleName } from './RoleName'
describe('RoleName', () => {
it('should create a value object', () => {
const valueOrError = RoleName.create('PRO_USER')
const valueOrError = RoleName.create(RoleName.NAMES.ProUser)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('PRO_USER')
@@ -17,9 +17,9 @@ describe('RoleName', () => {
})
it('should say if a role has more power or equal power to another role', () => {
const proUserRole = RoleName.create('PRO_USER').getValue()
const plusUserRole = RoleName.create('PLUS_USER').getValue()
const coreUser = RoleName.create('CORE_USER').getValue()
const proUserRole = RoleName.create(RoleName.NAMES.ProUser).getValue()
const plusUserRole = RoleName.create(RoleName.NAMES.PlusUser).getValue()
const coreUser = RoleName.create(RoleName.NAMES.CoreUser).getValue()
expect(proUserRole.hasMoreOrEqualPowerTo(proUserRole)).toBeTruthy()
expect(proUserRole.hasMoreOrEqualPowerTo(plusUserRole)).toBeTruthy()
@@ -32,5 +32,7 @@ describe('RoleName', () => {
expect(coreUser.hasMoreOrEqualPowerTo(proUserRole)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(plusUserRole)).toBeFalsy()
expect(coreUser.hasMoreOrEqualPowerTo(coreUser)).toBeTruthy()
expect(RoleName.create(RoleName.NAMES.FilesBetaUser).getValue().hasMoreOrEqualPowerTo(coreUser)).toBeFalsy()
})
})
@@ -3,7 +3,7 @@ import { Result } from '../Core/Result'
import { RoleNameProps } from './RoleNameProps'
export class RoleName extends ValueObject<RoleNameProps> {
private static readonly NAMES = {
static readonly NAMES = {
CoreUser: 'CORE_USER',
PlusUser: 'PLUS_USER',
ProUser: 'PRO_USER',
@@ -3,7 +3,7 @@ import { RoleNameCollection } from './RoleNameCollection'
describe('RoleNameCollection', () => {
it('should create a value object', () => {
const role1 = RoleName.create('PRO_USER').getValue()
const role1 = RoleName.create(RoleName.NAMES.ProUser).getValue()
const valueOrError = RoleNameCollection.create([role1])
@@ -12,23 +12,38 @@ describe('RoleNameCollection', () => {
})
it('should tell if collections are not equal', () => {
const roles1 = [RoleName.create('PRO_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles2 = RoleNameCollection.create([
RoleName.create('PRO_USER').getValue(),
RoleName.create('CORE_USER').getValue(),
let roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
]).getValue()
const valueOrError = RoleNameCollection.create(roles1)
let valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
roles2 = RoleNameCollection.create([
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
RoleName.create(RoleName.NAMES.CoreUser).getValue(),
]).getValue()
valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().equals(roles2)).toBeFalsy()
})
it('should tell if collections are equal', () => {
const roles1 = [RoleName.create('PRO_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const roles2 = RoleNameCollection.create([
RoleName.create('PRO_USER').getValue(),
RoleName.create('PLUS_USER').getValue(),
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]).getValue()
const valueOrError = RoleNameCollection.create(roles1)
@@ -36,42 +51,62 @@ describe('RoleNameCollection', () => {
})
it('should tell if collection includes element', () => {
const roles1 = [RoleName.create('PRO_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().includes(RoleName.create('PRO_USER').getValue())).toBeTruthy()
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeTruthy()
})
it('should tell if collection does not includes element', () => {
const roles1 = [RoleName.create('PRO_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
const roles1 = [
RoleName.create(RoleName.NAMES.ProUser).getValue(),
RoleName.create(RoleName.NAMES.PlusUser).getValue(),
]
const valueOrError = RoleNameCollection.create(roles1)
expect(valueOrError.getValue().includes(RoleName.create('CORE_USER').getValue())).toBeFalsy()
expect(valueOrError.getValue().includes(RoleName.create(RoleName.NAMES.CoreUser).getValue())).toBeFalsy()
})
it('should tell if collection has a role with more or equal power to', () => {
let roles = [RoleName.create('CORE_USER').getValue()]
let roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue()]
let valueOrError = RoleNameCollection.create(roles)
let roleNames = valueOrError.getValue()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('PLUS_USER').getValue())).toBeFalsy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('PRO_USER').getValue())).toBeFalsy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('CORE_USER').getValue())).toBeTruthy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.PlusUser).getValue()),
).toBeFalsy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeFalsy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
roles = [RoleName.create('CORE_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
roles = [RoleName.create(RoleName.NAMES.CoreUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
valueOrError = RoleNameCollection.create(roles)
roleNames = valueOrError.getValue()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('PLUS_USER').getValue())).toBeTruthy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('PRO_USER').getValue())).toBeFalsy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('CORE_USER').getValue())).toBeTruthy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.PlusUser).getValue()),
).toBeTruthy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.ProUser).getValue())).toBeFalsy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
roles = [RoleName.create('PRO_USER').getValue(), RoleName.create('PLUS_USER').getValue()]
roles = [RoleName.create(RoleName.NAMES.ProUser).getValue(), RoleName.create(RoleName.NAMES.PlusUser).getValue()]
valueOrError = RoleNameCollection.create(roles)
roleNames = valueOrError.getValue()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('PLUS_USER').getValue())).toBeTruthy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('PRO_USER').getValue())).toBeTruthy()
expect(roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create('CORE_USER').getValue())).toBeTruthy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.PlusUser).getValue()),
).toBeTruthy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.ProUser).getValue()),
).toBeTruthy()
expect(
roleNames.hasARoleNameWithMoreOrEqualPowerTo(RoleName.create(RoleName.NAMES.CoreUser).getValue()),
).toBeTruthy()
})
})
@@ -0,0 +1,16 @@
import { Username } from './Username'
describe('Username', () => {
it('should create a value object', () => {
const valueOrError = Username.create('test@test.te')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('test@test.te')
})
it('should not create an invalid value object', () => {
const valueOrError = Username.create('')
expect(valueOrError.isFailed()).toBeTruthy()
})
})
@@ -0,0 +1,22 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { UsernameProps } from './UsernameProps'
import { Validator } from '../Core/Validator'
export class Username extends ValueObject<UsernameProps> {
get value(): string {
return this.props.value
}
private constructor(props: UsernameProps) {
super(props)
}
static create(username: string): Result<Username> {
if (Validator.isNotEmpty(username).isFailed()) {
return Result.fail<Username>('Username cannot be empty')
}
return Result.ok<Username>(new Username({ value: username }))
}
}
@@ -0,0 +1,3 @@
export interface UsernameProps {
value: string
}
@@ -18,6 +18,31 @@ describe('Validator', () => {
'../../escaped.sh',
]
const validEmails = [
'something@something.com',
'someone@localhost.localdomain',
'a/b@domain.com',
'{}@domain.com',
'karol+test@standardnotes.com',
"m*'!%@something.sa",
'tu!!7n7.ad##0!!!@company.ca',
'%@com.com',
"!#$%&'*+/=?^_`{|}~.-@com.com",
'someone@do-ma-in.com',
'""testlah""@example.com',
]
const invalidEmails = [
'someone@127.0.0.1',
'a@b.b',
'',
null,
'.wooly@example.com',
'wo..oly@example.com',
'somebody@example',
'a @p.com',
]
it('should validate proper uuids', () => {
for (const validUuid of validUuids) {
expect(Validator.isValidUuid(validUuid).isFailed()).toBeFalsy()
@@ -29,4 +54,28 @@ describe('Validator', () => {
expect(Validator.isValidUuid(invalidUuid as string).isFailed()).toBeTruthy()
}
})
it('should validate proper emails', () => {
for (const validEmail of validEmails) {
expect(Validator.isValidEmail(validEmail).isFailed()).toBeFalsy()
}
})
it('should not validate invalid emails', () => {
for (const invalidEmail of invalidEmails) {
expect(Validator.isValidEmail(invalidEmail as string).isFailed()).toBeTruthy()
}
})
it('should validate value if not empty', () => {
for (const value of [1, 'foobar', {}, 0]) {
expect(Validator.isNotEmpty(value).isFailed()).toBeFalsy()
}
})
it('should not validate value if empty', () => {
for (const value of [null, undefined, '', []]) {
expect(Validator.isNotEmpty(value).isFailed()).toBeTruthy()
}
})
})
@@ -2,6 +2,8 @@ import { Result } from './Result'
export class Validator {
private static readonly UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i
private static readonly EMAIL_REGEX =
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
static isValidUuid(value: string): Result<string> {
const matchesUuidRegex = String(value).toLowerCase().match(Validator.UUID_REGEX) !== null
@@ -11,4 +13,25 @@ export class Validator {
return Result.fail(`Given value is not a valid uuid: ${value}`)
}
static isValidEmail(value: string): Result<string> {
const matchesUuidRegex = String(value).toLowerCase().match(Validator.EMAIL_REGEX) !== null
if (matchesUuidRegex) {
return Result.ok()
}
return Result.fail(`Given value is not a valid email address: ${value}`)
}
static isNotEmpty(value: unknown): Result<string> {
if (value instanceof Array && value.length === 0) {
return Result.fail(`Given value is empty: ${value}`)
}
if (value === null || value === undefined || value === '') {
return Result.fail(`Given value is empty: ${value}`)
}
return Result.ok()
}
}
@@ -0,0 +1,18 @@
import { EmailLevel } from './EmailLevel'
describe('EmailLevel', () => {
it('should create a value object', () => {
const valueOrError = EmailLevel.create(EmailLevel.LEVELS.SignIn)
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('SIGN_IN')
})
it('should not create an invalid value object', () => {
for (const value of ['', undefined, null, 0, 'FOOBAR']) {
const valueOrError = EmailLevel.create(value as string)
expect(valueOrError.isFailed()).toBeTruthy()
}
})
})
@@ -0,0 +1,31 @@
import { Result } from '../Core/Result'
import { ValueObject } from '../Core/ValueObject'
import { EmailLevelProps } from './EmailLevelProps'
export class EmailLevel extends ValueObject<EmailLevelProps> {
static readonly LEVELS = {
System: 'SYSTEM',
SignIn: 'SIGN_IN',
Marketing: 'MARKETING',
FailedCloudBackup: 'FAILED_CLOUD_BACKUP',
FailedEmailBackup: 'FAILED_EMAIL_BACKUP',
}
get value(): string {
return this.props.value
}
private constructor(props: EmailLevelProps) {
super(props)
}
static create(name: string): Result<EmailLevel> {
const isValidName = Object.values(this.LEVELS).includes(name)
if (!isValidName) {
return Result.fail<EmailLevel>(`Invalid subscription rejection level: ${name}`)
} else {
return Result.ok<EmailLevel>(new EmailLevel({ value: name }))
}
}
}
@@ -0,0 +1,3 @@
export interface EmailLevelProps {
value: string
}
@@ -0,0 +1,18 @@
import { SubscriptionPlanName } from './SubscriptionPlanName'
describe('SubscriptionPlanName', () => {
it('should create a value object', () => {
const valueOrError = SubscriptionPlanName.create('PRO_PLAN')
expect(valueOrError.isFailed()).toBeFalsy()
expect(valueOrError.getValue().value).toEqual('PRO_PLAN')
})
it('should not create an invalid value object', () => {
for (const value of ['', undefined, null, 0, 'SOME_PLAN']) {
const valueOrError = SubscriptionPlanName.create(value as string)
expect(valueOrError.isFailed()).toBeTruthy()
}
})
})
@@ -0,0 +1,27 @@
import { ValueObject } from '../Core/ValueObject'
import { Result } from '../Core/Result'
import { SubscriptionPlanNameProps } from './SubscriptionPlanNameProps'
export class SubscriptionPlanName extends ValueObject<SubscriptionPlanNameProps> {
static readonly NAMES = {
PlusPlan: 'PLUS_PLAN',
ProPlan: 'PRO_PLAN',
}
get value(): string {
return this.props.value
}
private constructor(props: SubscriptionPlanNameProps) {
super(props)
}
static create(name: string): Result<SubscriptionPlanName> {
const isValidName = Object.values(this.NAMES).includes(name)
if (!isValidName) {
return Result.fail<SubscriptionPlanName>(`Invalid subscription plan name: ${name}`)
} else {
return Result.ok<SubscriptionPlanName>(new SubscriptionPlanName({ value: name }))
}
}
}
@@ -0,0 +1,3 @@
export interface SubscriptionPlanNameProps {
value: string
}
+8
View File
@@ -6,6 +6,8 @@ export * from './Common/RoleName'
export * from './Common/RoleNameProps'
export * from './Common/RoleNameCollection'
export * from './Common/RoleNameCollectionProps'
export * from './Common/Username'
export * from './Common/UsernameProps'
export * from './Common/Uuid'
export * from './Common/UuidProps'
@@ -18,6 +20,12 @@ export * from './Core/Validator'
export * from './Core/ValueObject'
export * from './Core/ValueObjectProps'
export * from './Email/EmailLevel'
export * from './Email/EmailLevelProps'
export * from './Mapping/MapperInterface'
export * from './Subscription/SubscriptionPlanName'
export * from './Subscription/SubscriptionPlanNameProps'
export * from './UseCase/UseCaseInterface'
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.40](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.39...@standardnotes/domain-events-infra@1.9.40) (2022-12-07)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.39](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.38...@standardnotes/domain-events-infra@1.9.39) (2022-12-07)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.38](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.37...@standardnotes/domain-events-infra@1.9.38) (2022-12-06)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.37](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.36...@standardnotes/domain-events-infra@1.9.37) (2022-12-05)
**Note:** Version bump only for package @standardnotes/domain-events-infra
## [1.9.36](https://github.com/standardnotes/server/compare/@standardnotes/domain-events-infra@1.9.35...@standardnotes/domain-events-infra@1.9.36) (2022-11-30)
**Note:** Version bump only for package @standardnotes/domain-events-infra
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events-infra",
"version": "1.9.36",
"version": "1.9.40",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+24
View File
@@ -3,6 +3,30 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
# [2.95.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.94.1...@standardnotes/domain-events@2.95.0) (2022-12-07)
### Features
* **domain-events:** add email requested event ([fa75aa4](https://github.com/standardnotes/server/commit/fa75aa40f036dc3b9b4ed1364bffdbba6dec4da4))
## [2.94.1](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.94.0...@standardnotes/domain-events@2.94.1) (2022-12-07)
### Bug Fixes
* **domain-events:** remove not used event ([cb9499b](https://github.com/standardnotes/server/commit/cb9499b87f89ade166905fd6639ee330386c50b1))
# [2.94.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.93.0...@standardnotes/domain-events@2.94.0) (2022-12-06)
### Features
* **domain-events:** add mute emails setting changed event ([b7fb1d9](https://github.com/standardnotes/server/commit/b7fb1d9c08f66b0366f9af9cea8241f4c5ea9a18))
# [2.93.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.92.0...@standardnotes/domain-events@2.93.0) (2022-12-05)
### Features
* **domain-events:** add email subscription sync requested event ([fddf9fc](https://github.com/standardnotes/server/commit/fddf9fccbd92fa4279e97bcd2420ec9e270fedbb))
# [2.92.0](https://github.com/standardnotes/server/compare/@standardnotes/domain-events@2.91.0...@standardnotes/domain-events@2.92.0) (2022-11-30)
### Features
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/domain-events",
"version": "2.92.0",
"version": "2.95.0",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -0,0 +1,7 @@
import { DomainEventInterface } from './DomainEventInterface'
import { EmailRequestedEventPayload } from './EmailRequestedEventPayload'
export interface EmailRequestedEvent extends DomainEventInterface {
type: 'EMAIL_REQUESTED'
payload: EmailRequestedEventPayload
}
@@ -0,0 +1,7 @@
export interface EmailRequestedEventPayload {
userEmail: string
messageIdentifier: string
level: string
subject: string
body: string
}
@@ -0,0 +1,8 @@
import { DomainEventInterface } from './DomainEventInterface'
import { MuteEmailsSettingChangedEventPayload } from './MuteEmailsSettingChangedEventPayload'
export interface MuteEmailsSettingChangedEvent extends DomainEventInterface {
type: 'MUTE_EMAILS_SETTING_CHANGED'
payload: MuteEmailsSettingChangedEventPayload
}
@@ -0,0 +1,5 @@
export interface MuteEmailsSettingChangedEventPayload {
username: string
mute: boolean
emailSubscriptionRejectionLevel: string
}
@@ -30,6 +30,8 @@ export * from './Event/EmailBackupRequestedEvent'
export * from './Event/EmailBackupRequestedEventPayload'
export * from './Event/EmailMessageRequestedEvent'
export * from './Event/EmailMessageRequestedEventPayload'
export * from './Event/EmailRequestedEvent'
export * from './Event/EmailRequestedEventPayload'
export * from './Event/ExitDiscountAppliedEvent'
export * from './Event/ExitDiscountAppliedEventPayload'
export * from './Event/ExitDiscountApplyRequestedEvent'
@@ -58,6 +60,8 @@ export * from './Event/ListedAccountDeletedEvent'
export * from './Event/ListedAccountDeletedEventPayload'
export * from './Event/ListedAccountRequestedEvent'
export * from './Event/ListedAccountRequestedEventPayload'
export * from './Event/MuteEmailsSettingChangedEvent'
export * from './Event/MuteEmailsSettingChangedEventPayload'
export * from './Event/OfflineSubscriptionTokenCreatedEvent'
export * from './Event/OfflineSubscriptionTokenCreatedEventPayload'
export * from './Event/OneDriveBackupFailedEvent'
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.6.36](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.35...@standardnotes/event-store@1.6.36) (2022-12-07)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.35](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.34...@standardnotes/event-store@1.6.35) (2022-12-07)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.34](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.33...@standardnotes/event-store@1.6.34) (2022-12-06)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.33](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.32...@standardnotes/event-store@1.6.33) (2022-12-05)
**Note:** Version bump only for package @standardnotes/event-store
## [1.6.32](https://github.com/standardnotes/server/compare/@standardnotes/event-store@1.6.31...@standardnotes/event-store@1.6.32) (2022-11-30)
**Note:** Version bump only for package @standardnotes/event-store
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/event-store",
"version": "1.6.32",
"version": "1.6.36",
"description": "Event Store Service",
"private": true,
"main": "dist/src/index.js",
+16
View File
@@ -3,6 +3,22 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.8.36](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.35...@standardnotes/files-server@1.8.36) (2022-12-07)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.35](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.34...@standardnotes/files-server@1.8.35) (2022-12-07)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.34](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.33...@standardnotes/files-server@1.8.34) (2022-12-06)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.33](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.32...@standardnotes/files-server@1.8.33) (2022-12-05)
**Note:** Version bump only for package @standardnotes/files-server
## [1.8.32](https://github.com/standardnotes/files/compare/@standardnotes/files-server@1.8.31...@standardnotes/files-server@1.8.32) (2022-11-30)
**Note:** Version bump only for package @standardnotes/files-server
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/files-server",
"version": "1.8.32",
"version": "1.8.36",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+32
View File
@@ -3,6 +3,38 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.9.9](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.8...@standardnotes/revisions-server@1.9.9) (2022-12-07)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.8](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.7...@standardnotes/revisions-server@1.9.8) (2022-12-07)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.7](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.6...@standardnotes/revisions-server@1.9.7) (2022-12-07)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.6](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.5...@standardnotes/revisions-server@1.9.6) (2022-12-06)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.5](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.4...@standardnotes/revisions-server@1.9.5) (2022-12-05)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.4](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.3...@standardnotes/revisions-server@1.9.4) (2022-12-05)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.3](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.2...@standardnotes/revisions-server@1.9.3) (2022-12-05)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.2](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.1...@standardnotes/revisions-server@1.9.2) (2022-12-02)
**Note:** Version bump only for package @standardnotes/revisions-server
## [1.9.1](https://github.com/standardnotes/server/compare/@standardnotes/revisions-server@1.9.0...@standardnotes/revisions-server@1.9.1) (2022-12-02)
### Bug Fixes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/revisions-server",
"version": "1.9.1",
"version": "1.9.9",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
+28
View File
@@ -3,6 +3,34 @@
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [1.14.1](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.14.0...@standardnotes/scheduler-server@1.14.1) (2022-12-07)
### Bug Fixes
* **scheduler:** importing email contents ([c52bb93](https://github.com/standardnotes/server/commit/c52bb93d794447f04d3ea173f0aac9f26e4eba20))
# [1.14.0](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.37...@standardnotes/scheduler-server@1.14.0) (2022-12-07)
### Features
* **scheduler:** add scheduled emails contents ([6e0855f](https://github.com/standardnotes/server/commit/6e0855f9b32c230c9ad5594fb6af6dd460300fc1))
## [1.13.37](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.36...@standardnotes/scheduler-server@1.13.37) (2022-12-07)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.36](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.35...@standardnotes/scheduler-server@1.13.36) (2022-12-07)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.35](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.34...@standardnotes/scheduler-server@1.13.35) (2022-12-06)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.34](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.33...@standardnotes/scheduler-server@1.13.34) (2022-12-05)
**Note:** Version bump only for package @standardnotes/scheduler-server
## [1.13.33](https://github.com/standardnotes/server/compare/@standardnotes/scheduler-server@1.13.32...@standardnotes/scheduler-server@1.13.33) (2022-11-30)
**Note:** Version bump only for package @standardnotes/scheduler-server
+1
View File
@@ -7,4 +7,5 @@ module.exports = {
transform: {
...tsjPreset.transform,
},
coveragePathIgnorePatterns: ['/Bootstrap/', '/Infra/', '/Domain/Email/', '/Domain/Event/'],
}
+2 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@standardnotes/scheduler-server",
"version": "1.13.33",
"version": "1.14.1",
"engines": {
"node": ">=18.0.0 <19.0.0"
},
@@ -27,6 +27,7 @@
"@newrelic/winston-enricher": "^4.0.0",
"@sentry/node": "^7.19.0",
"@standardnotes/common": "workspace:*",
"@standardnotes/domain-core": "workspace:^",
"@standardnotes/domain-events": "workspace:*",
"@standardnotes/domain-events-infra": "workspace:*",
"@standardnotes/predicates": "workspace:*",
@@ -0,0 +1,9 @@
import { html } from './encourage-email-backups.html'
export function getSubject(): string {
return 'Enable email backups for your account'
}
export function getBody(): string {
return html
}
@@ -0,0 +1,11 @@
import { html } from './encourage-subscription-purchasing.html'
export function getSubject(): string {
return 'Checking in after one month with Standard Notes'
}
export function getBody(registrationDate: string): string {
const body = html
return body.replace('%%REGISTRATION_DATE%%', registrationDate)
}
@@ -0,0 +1,9 @@
import { html } from './exit-interview.html'
export function getSubject(): string {
return 'Can we ask why you canceled?'
}
export function getBody(): string {
return html
}
@@ -0,0 +1,18 @@
export const html = `<div>
<p>
Did you know you can enable daily email backups for your account? This <strong>free</strong> feature sends an
email to your inbox with an encrypted backup file including all your notes and tags.
</p>
<p>
Email backups are an important feature that help protect you against worst-case scenarios. Your backups can be
used to restore your account to a previous state, or to import old versions of notes into your present
account.
</p>
<p>
To enable free email backups, use the Standard Notes web or desktop app, and open Preferences > Backups > Email Backups.
</p>
<a href="https://standardnotes.com/help/28/how-do-i-enable-daily-email-backups">
Learn more about daily email backups →
</a>
</div>`
@@ -0,0 +1,84 @@
export const html = `<div>
<p>Hi there,</p>
<p>
We hope you've been finding great use out of Standard Notes. We built Standard Notes to be a secure place for
your most sensitive notes and files.
</p>
<p>
As a reminder,
<strong>
<em>you signed up for the Standard Notes free plan on %%REGISTRATION_DATE%%</em>
</strong>
Your free account comes with standard features like end-to-end encryption, multiple-device sync, and
two-factor authentication.
</p>
<p>
If you're ready to advance your usage of Standard Notes, we recommend upgrading to one of our more powerful
plans.
</p>
<ul>
<li>
<p>
<strong>Productivity</strong> <strong>($59/year)</strong> powers up your editing experience with unique
and special-built note-types for markdown, rich text, spreadsheets, todo, and more.
</p>
</li>
<li>
<p>
<strong>Professional</strong> <strong>($99/year)</strong> gives you all the power of Productivity plus
100GB of end-to-end encrypted file storage for your private photos, videos, and documents, plus family
subscription sharing with up to 5 people.
</p>
</li>
</ul>
<p>
Professional comes with a 90-day money back guarantee, so if you're not completely satisfied, we're happy to
refund your full purchase amount.
</p>
<p>
<strong>
<a href="https://standardnotes.com/plans">Upgrade your plan →</a>
</strong>
</p>
<p>
<strong>
<a href="https://standardnotes.com/features">Learn more about the features →</a>
</strong>
</p>
<p>
<strong>Questions & Answers</strong>
</p>
<p>
<em>How does Standard Notes compare with conventional note-taking apps?</em>
</p>
<p>
Data you store with Standard Notes is encrypted with end-to-end encryption using a key only you know. Because
of this, we can't read your notes, and neither can anyone else.
</p>
<p>
<em>What kind of notes should I store in Standard Notes?</em>
</p>
<p>
This question can be reframed as: "What shouldn't I store in non-private services?" This would include
sensitive/sensual data related to your health and wellness, secrets and keys, notes and documents with
personally identifiable information that, if leaked, would lead to the theft of your identity, and business,
financial, or legal information which cover non-public or confidential information.
</p>
<p>
<em>Where can I access my notes?</em>
</p>
<p>
Providing you with easy access to your notes and files on all your devices is a key focus for us. We provide
secure and well-designed applications for your web browser, desktop (macOS, Windows, Linux,) and mobile
(Android and iOS).
</p>
<p>
<em>I have more questions.</em>
</p>
<p>
We love questions. Head over to our Help page to see if your question is answered there. If not, reply
directly to this email or send an email to <a href="help@standardnotes.com">help@standardnotes.com</a> and
we'd be happy to help.
</p>
</div>
`
@@ -0,0 +1,29 @@
export const html = `<div>
<p>
We're truly sad to see you leave. Our mission is simple: build the best, most private, and most secure
note-taking app available. It's clear we've fallen short of your expectations somewhere along the way.
</p>
<p>
We just want you to know—if price was the reason you canceled, we're not willing to lose you. That's no issue
for us and we're happy to work out something that fits better with your budget. If price is your primary
concern, please click the link below, and we'll get in touch with some options.
</p>
<a href="https://app.standardnotes.com/?user-request=exit-discount">Apply For A Limited Discount Offer →</a>
<p>
If you canceled for another reason, such as a missing feature, or a feature that wasn't behaving or working as
you expected, please let us know! We build this product for you, and feedback from customers like yourself who
are willing to pay for a product is most crucial for us as we continue to evolve and iterate on Standard
Notes.
</p>
<p>If you have a minute, please fill out this brief exit interview: </p>
<a href="https://standardnotes.typeform.com/to/dX5lzPtm">Short Exit Interview →</a>
<p>
Our team reads every single response, and your feedback will be shared with the relevant department within our
team.
</p>
<p>
If you have any other thoughts or questions, please feel free to reply directly to this email, and a member of
our support team will be in touch with you.
</p>
</div>
`
@@ -1,223 +0,0 @@
import 'reflect-metadata'
import { EmailMessageIdentifier } from '@standardnotes/common'
import { TimerInterface } from '@standardnotes/time'
import { DomainEventFactory } from './DomainEventFactory'
import { PredicateAuthority, PredicateName } from '@standardnotes/predicates'
import { Job } from '../Job/Job'
import { Predicate } from '../Predicate/Predicate'
describe('DomainEventFactory', () => {
let timer: TimerInterface
const createFactory = () => new DomainEventFactory(timer)
beforeEach(() => {
timer = {} as jest.Mocked<TimerInterface>
timer.getTimestampInMicroseconds = jest.fn().mockReturnValue(1)
timer.getUTCDate = jest.fn().mockReturnValue(new Date(1))
})
it('should create a DISCOUNT_APPLY_REQUESTED event', () => {
expect(
createFactory().createDiscountApplyRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'off-10',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'off-10',
},
type: 'DISCOUNT_APPLY_REQUESTED',
})
})
it('should create a DISCOUNT_WITHDRAW_REQUESTED event', () => {
expect(
createFactory().createDiscountWithdrawRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'off-10',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'off-10',
},
type: 'DISCOUNT_WITHDRAW_REQUESTED',
})
})
it('should create a EXIT_DISCOUNT_WITHDRAW_REQUESTED event', () => {
expect(
createFactory().createExitDiscountWithdrawRequestedEvent({
userEmail: 'test@test.te',
discountCode: 'exit-20',
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
userEmail: 'test@test.te',
discountCode: 'exit-20',
},
type: 'EXIT_DISCOUNT_WITHDRAW_REQUESTED',
})
})
it('should create a EMAIL_MESSAGE_REQUESTED event', () => {
expect(
createFactory().createEmailMessageRequestedEvent({
userEmail: 'test@test.te',
messageIdentifier: EmailMessageIdentifier.ENCOURAGE_EMAIL_BACKUPS,
context: {
foo: 'bar',
},
}),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: 'test@test.te',
userIdentifierType: 'email',
},
origin: 'scheduler',
},
payload: {
messageIdentifier: 'ENCOURAGE_EMAIL_BACKUPS',
userEmail: 'test@test.te',
context: {
foo: 'bar',
},
},
type: 'EMAIL_MESSAGE_REQUESTED',
})
})
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for auth', () => {
expect(
createFactory().createPredicateVerificationRequestedEvent(
{
uuid: '1-2-3',
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>,
{
authority: PredicateAuthority.Auth,
name: PredicateName.EmailBackupsEnabled,
status: 'pending',
} as jest.Mocked<Predicate>,
),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'scheduler',
target: 'auth',
},
payload: {
predicate: {
authority: 'auth',
jobUuid: '1-2-3',
name: 'email-backups-enabled',
},
},
type: 'PREDICATE_VERIFICATION_REQUESTED',
})
})
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for syncing server', () => {
expect(
createFactory().createPredicateVerificationRequestedEvent(
{
uuid: '1-2-3',
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>,
{
authority: PredicateAuthority.SyncingServer,
name: PredicateName.EmailBackupsEnabled,
status: 'pending',
} as jest.Mocked<Predicate>,
),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'scheduler',
target: 'syncing-server',
},
payload: {
predicate: {
authority: 'syncing-server',
jobUuid: '1-2-3',
name: 'email-backups-enabled',
},
},
type: 'PREDICATE_VERIFICATION_REQUESTED',
})
})
it('should create a PREDICATE_VERIFICATION_REQUESTED event dedicated for unknown target', () => {
expect(
createFactory().createPredicateVerificationRequestedEvent(
{
uuid: '1-2-3',
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
} as jest.Mocked<Job>,
{
authority: 'foobar' as PredicateAuthority,
name: PredicateName.EmailBackupsEnabled,
status: 'pending',
} as jest.Mocked<Predicate>,
),
).toEqual({
createdAt: expect.any(Date),
meta: {
correlation: {
userIdentifier: '2-3-4',
userIdentifierType: 'uuid',
},
origin: 'scheduler',
},
payload: {
predicate: {
authority: 'foobar',
jobUuid: '1-2-3',
name: 'email-backups-enabled',
},
},
type: 'PREDICATE_VERIFICATION_REQUESTED',
})
})
})
@@ -1,9 +1,8 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
DomainEventService,
EmailMessageRequestedEvent,
EmailRequestedEvent,
ExitDiscountWithdrawRequestedEvent,
PredicateVerificationRequestedEvent,
} from '@standardnotes/domain-events'
@@ -70,13 +69,15 @@ export class DomainEventFactory implements DomainEventFactoryInterface {
}
}
createEmailMessageRequestedEvent(dto: {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent {
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent {
return {
type: 'EMAIL_MESSAGE_REQUESTED',
type: 'EMAIL_REQUESTED',
createdAt: this.timer.getUTCDate(),
meta: {
correlation: {
@@ -1,8 +1,7 @@
import { EmailMessageIdentifier } from '@standardnotes/common'
import {
DiscountApplyRequestedEvent,
DiscountWithdrawRequestedEvent,
EmailMessageRequestedEvent,
EmailRequestedEvent,
ExitDiscountWithdrawRequestedEvent,
PredicateVerificationRequestedEvent,
} from '@standardnotes/domain-events'
@@ -12,11 +11,13 @@ import { Predicate } from '../Predicate/Predicate'
export interface DomainEventFactoryInterface {
createPredicateVerificationRequestedEvent(job: Job, predicate: Predicate): PredicateVerificationRequestedEvent
createEmailMessageRequestedEvent(dto: {
createEmailRequestedEvent(dto: {
userEmail: string
messageIdentifier: EmailMessageIdentifier
context: Record<string, unknown>
}): EmailMessageRequestedEvent
messageIdentifier: string
level: string
body: string
subject: string
}): EmailRequestedEvent
createDiscountApplyRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountApplyRequestedEvent
createDiscountWithdrawRequestedEvent(dto: { userEmail: string; discountCode: string }): DiscountWithdrawRequestedEvent
createExitDiscountWithdrawRequestedEvent(dto: {

Some files were not shown because too many files have changed in this diff Show More